diff options
413 files changed, 7629 insertions, 10574 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 9ee74e377f90..1c6df75a4f02 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -56,6 +56,7 @@ aconfig_srcjars = [ ":android.service.notification.flags-aconfig-java{.generated_srcjars}", ":android.service.voice.flags-aconfig-java{.generated_srcjars}", ":android.speech.flags-aconfig-java{.generated_srcjars}", + ":android.systemserver.flags-aconfig-java{.generated_srcjars}", ":android.tracing.flags-aconfig-java{.generated_srcjars}", ":android.view.accessibility.flags-aconfig-java{.generated_srcjars}", ":android.view.contentcapture.flags-aconfig-java{.generated_srcjars}", @@ -1159,3 +1160,16 @@ java_aconfig_library { host_supported: true, defaults: ["framework-minus-apex-aconfig-java-defaults"], } + +// System Server +aconfig_declarations { + name: "android.systemserver.flags-aconfig", + package: "android.server", + srcs: ["services/java/com/android/server/flags.aconfig"], +} + +java_aconfig_library { + name: "android.systemserver.flags-aconfig-java", + aconfig_declarations: "android.systemserver.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 324d8cafaefc..7284f479df35 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -23,7 +23,6 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; -import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.util.TimeUtils.formatDuration; import android.annotation.BytesLong; @@ -50,9 +49,7 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; -import android.os.Process; import android.os.Trace; -import android.os.UserHandle; import android.util.ArraySet; import android.util.Log; @@ -206,6 +203,8 @@ public class JobInfo implements Parcelable { /* Minimum flex for a periodic job, in milliseconds. */ private static final long MIN_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes + private static final long MIN_ALLOWED_TIME_WINDOW_MILLIS = MIN_PERIOD_MILLIS; + /** * Minimum backoff interval for a job, in milliseconds * @hide @@ -1881,11 +1880,12 @@ public class JobInfo implements Parcelable { } /** - * Set deadline which is the maximum scheduling latency. The job will be run by this - * deadline even if other requirements (including a delay set through - * {@link #setMinimumLatency(long)}) are not met. + * Set a deadline after which all other functional requested constraints will be ignored. + * After the deadline has passed, the job can run even if other requirements (including + * a delay set through {@link #setMinimumLatency(long)}) are not met. * {@link JobParameters#isOverrideDeadlineExpired()} will return {@code true} if the job's - * deadline has passed. + * deadline has passed. The job's execution may be delayed beyond the set deadline by + * other factors such as Doze mode and system health signals. * * <p> * Because it doesn't make sense setting this property on a periodic job, doing so will @@ -1894,30 +1894,23 @@ public class JobInfo implements Parcelable { * * <p class="note"> * Since a job will run once the deadline has passed regardless of the status of other - * constraints, setting a deadline of 0 with other constraints makes those constraints - * meaningless when it comes to execution decisions. Avoid doing this. - * </p> - * - * <p> - * Short deadlines hinder the system's ability to optimize scheduling behavior and may - * result in running jobs at inopportune times. Therefore, starting in Android version - * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, minimum time windows will be - * enforced to help make it easier to better optimize job execution. Time windows are + * constraints, setting a deadline of 0 (or a {@link #setMinimumLatency(long) delay} equal + * to the deadline) with other constraints makes those constraints + * meaningless when it comes to execution decisions. Since doing so is indicative of an + * error in the logic, starting in Android version + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, jobs with extremely short + * time windows will fail to build. Time windows are * defined as the time between a job's {@link #setMinimumLatency(long) minimum latency} * and its deadline. If the minimum latency is not set, it is assumed to be 0. - * The following minimums will be enforced: - * <ul> - * <li> - * Jobs with {@link #PRIORITY_DEFAULT} or higher priorities have a minimum time - * window of one hour. - * </li> - * <li>Jobs with {@link #PRIORITY_LOW} have a minimum time window of 6 hours.</li> - * <li>Jobs with {@link #PRIORITY_MIN} have a minimum time window of 12 hours.</li> - * </ul> * * Work that must happen immediately should use {@link #setExpedited(boolean)} or * {@link #setUserInitiated(boolean)} in the appropriate manner. * + * <p> + * This API aimed to guarantee execution of the job by the deadline only on Android version + * {@link android.os.Build.VERSION_CODES#LOLLIPOP}. That aim and guarantee has not existed + * since {@link android.os.Build.VERSION_CODES#M}. + * * @see JobInfo#getMaxExecutionDelayMillis() */ public Builder setOverrideDeadline(long maxExecutionDelayMillis) { @@ -2347,35 +2340,36 @@ public class JobInfo implements Parcelable { throw new IllegalArgumentException("Invalid priority level provided: " + mPriority); } - if (enforceMinimumTimeWindows - && Flags.enforceMinimumTimeWindows() - // TODO(312197030): remove exemption for the system - && !UserHandle.isCore(Process.myUid()) - && hasLateConstraint && !isPeriodic) { - final long windowStart = hasEarlyConstraint ? minLatencyMillis : 0; - if (mPriority >= PRIORITY_DEFAULT) { - if (maxExecutionDelayMillis - windowStart < HOUR_IN_MILLIS) { - throw new IllegalArgumentException( - getPriorityString(mPriority) - + " cannot have a time window less than 1 hour." - + " Delay=" + windowStart - + ", deadline=" + maxExecutionDelayMillis); - } - } else if (mPriority >= PRIORITY_LOW) { - if (maxExecutionDelayMillis - windowStart < 6 * HOUR_IN_MILLIS) { - throw new IllegalArgumentException( - getPriorityString(mPriority) - + " cannot have a time window less than 6 hours." - + " Delay=" + windowStart - + ", deadline=" + maxExecutionDelayMillis); - } + final boolean hasFunctionalConstraint = networkRequest != null + || constraintFlags != 0 + || (triggerContentUris != null && triggerContentUris.length > 0); + if (hasLateConstraint && !isPeriodic) { + if (!hasFunctionalConstraint) { + Log.w(TAG, "Job '" + service.flattenToShortString() + "#" + jobId + "'" + + " has a deadline with no functional constraints." + + " The deadline won't improve job execution latency." + + " Consider removing the deadline."); } else { - if (maxExecutionDelayMillis - windowStart < 12 * HOUR_IN_MILLIS) { - throw new IllegalArgumentException( - getPriorityString(mPriority) - + " cannot have a time window less than 12 hours." - + " Delay=" + windowStart - + ", deadline=" + maxExecutionDelayMillis); + final long windowStart = hasEarlyConstraint ? minLatencyMillis : 0; + if (maxExecutionDelayMillis - windowStart < MIN_ALLOWED_TIME_WINDOW_MILLIS) { + if (enforceMinimumTimeWindows + && Flags.enforceMinimumTimeWindows()) { + throw new IllegalArgumentException("Jobs with a deadline and" + + " functional constraints cannot have a time window less than " + + MIN_ALLOWED_TIME_WINDOW_MILLIS + " ms." + + " Job '" + service.flattenToShortString() + "#" + jobId + "'" + + " has delay=" + windowStart + + ", deadline=" + maxExecutionDelayMillis); + } else { + Log.w(TAG, "Job '" + service.flattenToShortString() + "#" + jobId + "'" + + " has a deadline with functional constraints and an extremely" + + " short time window of " + + (maxExecutionDelayMillis - windowStart) + " ms" + + " (delay=" + windowStart + + ", deadline=" + maxExecutionDelayMillis + ")." + + " The functional constraints are not likely to be satisfied when" + + " the job runs."); + } } } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index 2ea980d40287..a3a686fdc5c8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -2053,6 +2053,11 @@ public final class JobStatus { case CONSTRAINT_WITHIN_QUOTA: return JobParameters.STOP_REASON_QUOTA; + // This can change from true to false, but should never change when a job is already + // running, so there's no reason to log a message or create a new stop reason. + case CONSTRAINT_FLEXIBLE: + return JobParameters.STOP_REASON_UNDEFINED; + // These should never be stop reasons since they can never go from true to false. case CONSTRAINT_CONTENT_TRIGGER: case CONSTRAINT_DEADLINE: diff --git a/core/api/current.txt b/core/api/current.txt index c896d5c04173..bca15bd20657 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -765,7 +765,7 @@ package android { field public static final int endY = 16844051; // 0x1010513 field @Deprecated public static final int endYear = 16843133; // 0x101017d field public static final int enforceNavigationBarContrast = 16844293; // 0x1010605 - field public static final int enforceStatusBarContrast = 16844292; // 0x1010604 + field @Deprecated public static final int enforceStatusBarContrast = 16844292; // 0x1010604 field public static final int enterFadeDuration = 16843532; // 0x101030c field public static final int entries = 16842930; // 0x10100b2 field public static final int entryValues = 16843256; // 0x10101f8 @@ -1196,8 +1196,8 @@ package android { field public static final int multiprocess = 16842771; // 0x1010013 field public static final int name = 16842755; // 0x1010003 field public static final int nativeHeapZeroInitialized = 16844325; // 0x1010625 - field public static final int navigationBarColor = 16843858; // 0x1010452 - field public static final int navigationBarDividerColor = 16844141; // 0x101056d + field @Deprecated public static final int navigationBarColor = 16843858; // 0x1010452 + field @Deprecated public static final int navigationBarDividerColor = 16844141; // 0x101056d field public static final int navigationContentDescription = 16843969; // 0x10104c1 field public static final int navigationIcon = 16843968; // 0x10104c0 field public static final int navigationMode = 16843471; // 0x10102cf @@ -1568,7 +1568,7 @@ package android { field public static final int state_single = 16842915; // 0x10100a3 field public static final int state_window_focused = 16842909; // 0x101009d field public static final int staticWallpaperPreview = 16843569; // 0x1010331 - field public static final int statusBarColor = 16843857; // 0x1010451 + field @Deprecated public static final int statusBarColor = 16843857; // 0x1010451 field public static final int stepSize = 16843078; // 0x1010146 field public static final int stopWithTask = 16843626; // 0x101036a field public static final int streamType = 16843273; // 0x1010209 @@ -1890,6 +1890,7 @@ package android { field public static final int windowNoDisplay = 16843294; // 0x101021e field public static final int windowNoMoveAnimation = 16844421; // 0x1010685 field public static final int windowNoTitle = 16842838; // 0x1010056 + field @FlaggedApi("com.android.window.flags.enforce_edge_to_edge") public static final int windowOptOutEdgeToEdgeEnforcement; field @Deprecated public static final int windowOverscan = 16843727; // 0x10103cf field public static final int windowReenterTransition = 16843951; // 0x10104af field public static final int windowReturnTransition = 16843950; // 0x10104ae @@ -7894,6 +7895,7 @@ package android.app.admin { field public static final String AUTO_TIME_POLICY = "autoTime"; field public static final String BACKUP_SERVICE_POLICY = "backupService"; field public static final String CAMERA_DISABLED_POLICY = "cameraDisabled"; + field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final String CONTENT_PROTECTION_POLICY = "contentProtection"; field public static final String KEYGUARD_DISABLED_FEATURES_POLICY = "keyguardDisabledFeatures"; field public static final String LOCK_TASK_POLICY = "lockTask"; field public static final String PACKAGES_SUSPENDED_POLICY = "packagesSuspended"; @@ -7944,6 +7946,7 @@ package android.app.admin { method public boolean getBluetoothContactSharingDisabled(@NonNull android.content.ComponentName); method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_CAMERA, conditional=true) public boolean getCameraDisabled(@Nullable android.content.ComponentName); method @Deprecated @Nullable public String getCertInstallerPackage(@NonNull android.content.ComponentName) throws java.lang.SecurityException; + method @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional=true) public int getContentProtectionPolicy(@Nullable android.content.ComponentName); method @Nullable public android.app.admin.PackagePolicy getCredentialManagerPolicy(); method @Deprecated @Nullable public java.util.Set<java.lang.String> getCrossProfileCalendarPackages(@NonNull android.content.ComponentName); method @Deprecated public boolean getCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName); @@ -8100,6 +8103,7 @@ package android.app.admin { method @Deprecated public void setCertInstallerPackage(@NonNull android.content.ComponentName, @Nullable String) throws java.lang.SecurityException; method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE, conditional=true) public void setCommonCriteriaModeEnabled(@Nullable android.content.ComponentName, boolean); method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_WIFI, conditional=true) public void setConfiguredNetworksLockdownState(@Nullable android.content.ComponentName, boolean); + method @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional=true) public void setContentProtectionPolicy(@Nullable android.content.ComponentName, int); method public void setCredentialManagerPolicy(@Nullable android.app.admin.PackagePolicy); method @Deprecated public void setCrossProfileCalendarPackages(@NonNull android.content.ComponentName, @Nullable java.util.Set<java.lang.String>); method @Deprecated public void setCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName, boolean); @@ -8525,6 +8529,7 @@ package android.app.admin { field public static final int TAG_ADB_SHELL_CMD = 210002; // 0x33452 field public static final int TAG_ADB_SHELL_INTERACTIVE = 210001; // 0x33451 field public static final int TAG_APP_PROCESS_START = 210005; // 0x33455 + field @FlaggedApi("android.app.admin.flags.backup_service_security_log_event_enabled") public static final int TAG_BACKUP_SERVICE_TOGGLED = 210044; // 0x3347c field public static final int TAG_BLUETOOTH_CONNECTION = 210039; // 0x33477 field public static final int TAG_BLUETOOTH_DISCONNECTION = 210040; // 0x33478 field public static final int TAG_CAMERA_POLICY_SET = 210034; // 0x33472 @@ -24706,6 +24711,7 @@ package android.media { } public class Ringtone { + method protected void finalize(); method public android.media.AudioAttributes getAudioAttributes(); method @Deprecated public int getStreamType(); method public String getTitle(android.content.Context); @@ -46298,6 +46304,7 @@ package android.telephony.euicc { method @NonNull public android.telephony.euicc.EuiccManager createForCardId(int); method @RequiresPermission("android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS") public void deleteSubscription(int, android.app.PendingIntent); method @RequiresPermission("android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS") public void downloadSubscription(android.telephony.euicc.DownloadableSubscription, boolean, android.app.PendingIntent); + method @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public long getAvailableMemoryInBytes(); method @Nullable public String getEid(); method @Nullable public android.telephony.euicc.EuiccInfo getEuiccInfo(); method public boolean isEnabled(); @@ -46330,6 +46337,7 @@ package android.telephony.euicc { field public static final int ERROR_SIM_MISSING = 10008; // 0x2718 field public static final int ERROR_TIME_OUT = 10005; // 0x2715 field public static final int ERROR_UNSUPPORTED_VERSION = 10007; // 0x2717 + field @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") public static final long EUICC_MEMORY_FIELD_UNAVAILABLE = -1L; // 0xffffffffffffffffL field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_DETAILED_CODE"; field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTION = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTION"; field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_ERROR_CODE = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_ERROR_CODE"; @@ -53618,8 +53626,8 @@ package android.view { method @NonNull public abstract android.view.LayoutInflater getLayoutInflater(); method protected final int getLocalFeatures(); method public android.media.session.MediaController getMediaController(); - method @ColorInt public abstract int getNavigationBarColor(); - method @ColorInt public int getNavigationBarDividerColor(); + method @Deprecated @ColorInt public abstract int getNavigationBarColor(); + method @Deprecated @ColorInt public int getNavigationBarDividerColor(); method @NonNull public android.window.OnBackInvokedDispatcher getOnBackInvokedDispatcher(); method public android.transition.Transition getReenterTransition(); method public android.transition.Transition getReturnTransition(); @@ -53629,7 +53637,7 @@ package android.view { method public android.transition.Transition getSharedElementReenterTransition(); method public android.transition.Transition getSharedElementReturnTransition(); method public boolean getSharedElementsUseOverlay(); - method @ColorInt public abstract int getStatusBarColor(); + method @Deprecated @ColorInt public abstract int getStatusBarColor(); method @NonNull public java.util.List<android.graphics.Rect> getSystemGestureExclusionRects(); method public long getTransitionBackgroundFadeDuration(); method public android.transition.TransitionManager getTransitionManager(); @@ -53645,7 +53653,7 @@ package android.view { method public abstract boolean isFloating(); method public boolean isNavigationBarContrastEnforced(); method public abstract boolean isShortcutKey(int, android.view.KeyEvent); - method public boolean isStatusBarContrastEnforced(); + method @Deprecated public boolean isStatusBarContrastEnforced(); method public boolean isWideColorGamut(); method public final void makeActive(); method protected abstract void onActive(); @@ -53677,7 +53685,7 @@ package android.view { method public abstract void setContentView(android.view.View); method public abstract void setContentView(android.view.View, android.view.ViewGroup.LayoutParams); method public abstract void setDecorCaptionShade(int); - method public void setDecorFitsSystemWindows(boolean); + method @Deprecated public void setDecorFitsSystemWindows(boolean); method protected void setDefaultWindowFormat(int); method @FlaggedApi("com.android.graphics.hwui.flags.limited_hdr") public void setDesiredHdrHeadroom(@FloatRange(from=0.0f, to=10000.0) float); method public void setDimAmount(float); @@ -53699,9 +53707,9 @@ package android.view { method public void setLocalFocus(boolean, boolean); method public void setLogo(@DrawableRes int); method public void setMediaController(android.media.session.MediaController); - method public abstract void setNavigationBarColor(@ColorInt int); + method @Deprecated public abstract void setNavigationBarColor(@ColorInt int); method public void setNavigationBarContrastEnforced(boolean); - method public void setNavigationBarDividerColor(@ColorInt int); + method @Deprecated public void setNavigationBarDividerColor(@ColorInt int); method public void setPreferMinimalPostProcessing(boolean); method public void setReenterTransition(android.transition.Transition); method public abstract void setResizingCaptionDrawable(android.graphics.drawable.Drawable); @@ -53713,8 +53721,8 @@ package android.view { method public void setSharedElementReturnTransition(android.transition.Transition); method public void setSharedElementsUseOverlay(boolean); method public void setSoftInputMode(int); - method public abstract void setStatusBarColor(@ColorInt int); - method public void setStatusBarContrastEnforced(boolean); + method @Deprecated public abstract void setStatusBarColor(@ColorInt int); + method @Deprecated public void setStatusBarContrastEnforced(boolean); method public void setSustainedPerformanceMode(boolean); method public void setSystemGestureExclusionRects(@NonNull java.util.List<android.graphics.Rect>); method public abstract void setTitle(CharSequence); diff --git a/core/api/lint-baseline.txt b/core/api/lint-baseline.txt index 162f54cc6d5a..e901f00d5f5f 100644 --- a/core/api/lint-baseline.txt +++ b/core/api/lint-baseline.txt @@ -389,6 +389,12 @@ InvalidNullabilityOverride: android.media.midi.MidiUmpDeviceService#onBind(andro Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated. +KotlinOperator: android.graphics.Matrix44#get(int, int): + Method can be invoked with an indexing operator from Kotlin: `get` (this is usually desirable; just make sure it makes sense for this type of object) +KotlinOperator: android.graphics.Matrix44#set(int, int, float): + Method can be invoked with an indexing operator from Kotlin: `set` (this is usually desirable; just make sure it makes sense for this type of object) + + RequiresPermission: android.accounts.AccountManager#getAccountsByTypeAndFeatures(String, String[], android.accounts.AccountManagerCallback<android.accounts.Account[]>, android.os.Handler): Method 'getAccountsByTypeAndFeatures' documentation mentions permissions without declaring @RequiresPermission RequiresPermission: android.accounts.AccountManager#hasFeatures(android.accounts.Account, String[], android.accounts.AccountManagerCallback<java.lang.Boolean>, android.os.Handler): @@ -477,6 +483,8 @@ RequiresPermission: android.app.admin.DevicePolicyManager#clearResetPasswordToke Method 'clearResetPasswordToken' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#generateKeyPair(android.content.ComponentName, String, android.security.keystore.KeyGenParameterSpec, int): Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission +RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName): + Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName): Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName): @@ -513,6 +521,8 @@ RequiresPermission: android.app.admin.DevicePolicyManager#removeCrossProfileWidg Method 'removeCrossProfileWidgetProvider' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setAlwaysOnVpnPackage(android.content.ComponentName, String, boolean): Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission +RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int): + Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskFeatures(android.content.ComponentName, int): Method 'setLockTaskFeatures' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskPackages(android.content.ComponentName, String[]): diff --git a/core/api/module-lib-lint-baseline.txt b/core/api/module-lib-lint-baseline.txt index a2179bc59707..42c4efc139ca 100644 --- a/core/api/module-lib-lint-baseline.txt +++ b/core/api/module-lib-lint-baseline.txt @@ -611,6 +611,8 @@ RequiresPermission: android.app.admin.DevicePolicyManager#generateKeyPair(androi Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getApplicationExemptions(String): Method 'getApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission +RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName): + Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName): Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName): @@ -657,6 +659,8 @@ RequiresPermission: android.app.admin.DevicePolicyManager#setAlwaysOnVpnPackage( Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setApplicationExemptions(String, java.util.Set<java.lang.Integer>): Method 'setApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission +RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int): + Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceProvisioningConfigApplied(): Method 'setDeviceProvisioningConfigApplied' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskFeatures(android.content.ComponentName, int): diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 515d01786b90..57489af658fd 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -11,6 +11,7 @@ package android { field public static final String ACCESS_DRM_CERTIFICATES = "android.permission.ACCESS_DRM_CERTIFICATES"; field @Deprecated public static final String ACCESS_FM_RADIO = "android.permission.ACCESS_FM_RADIO"; field public static final String ACCESS_FPS_COUNTER = "android.permission.ACCESS_FPS_COUNTER"; + field @FlaggedApi("android.multiuser.flags.enable_permission_to_access_hidden_profiles") public static final String ACCESS_HIDDEN_PROFILES_FULL = "android.permission.ACCESS_HIDDEN_PROFILES_FULL"; field public static final String ACCESS_INSTANT_APPS = "android.permission.ACCESS_INSTANT_APPS"; field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ACCESS_LAST_KNOWN_CELL_ID = "android.permission.ACCESS_LAST_KNOWN_CELL_ID"; field public static final String ACCESS_LOCUS_ID_USAGE_STATS = "android.permission.ACCESS_LOCUS_ID_USAGE_STATS"; @@ -3153,7 +3154,9 @@ package android.app.wearable { public class WearableSensingManager { method @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideData(@NonNull android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideDataStream(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideWearableConnection(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); field public static final int STATUS_ACCESS_DENIED = 5; // 0x5 + field @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") public static final int STATUS_CHANNEL_ERROR = 7; // 0x7 field public static final int STATUS_SERVICE_UNAVAILABLE = 3; // 0x3 field public static final int STATUS_SUCCESS = 1; // 0x1 field public static final int STATUS_UNKNOWN = 0; // 0x0 @@ -4249,7 +4252,6 @@ package android.content.pm { public final class UserProperties implements android.os.Parcelable { method public int describeContents(); method public int getCrossProfileContentSharingStrategy(); - method @FlaggedApi("android.multiuser.support_hiding_profiles") @NonNull public int getProfileApiVisibility(); method public int getShowInQuietMode(); method public int getShowInSharingSurfaces(); method public boolean isCredentialShareableWithParent(); @@ -4259,9 +4261,6 @@ package android.content.pm { field public static final int CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT = 1; // 0x1 field public static final int CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION = 0; // 0x0 field public static final int CROSS_PROFILE_CONTENT_SHARING_UNKNOWN = -1; // 0xffffffff - field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_HIDDEN = 1; // 0x1 - field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_UNKNOWN = -1; // 0xffffffff - field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_VISIBLE = 0; // 0x0 field public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; // 0x2 field public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; // 0x1 field public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; // 0x0 @@ -4415,8 +4414,9 @@ package android.credentials.selection { } @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class CancelSelectionRequest implements android.os.Parcelable { + ctor public CancelSelectionRequest(@NonNull android.credentials.selection.RequestToken, boolean, @NonNull String); method public int describeContents(); - method @NonNull public String getAppPackageName(); + method @NonNull public String getPackageName(); method @NonNull public android.credentials.selection.RequestToken getRequestToken(); method public boolean shouldShowCancellationExplanation(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -4498,10 +4498,10 @@ package android.credentials.selection { @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class RequestInfo implements android.os.Parcelable { method public int describeContents(); - method @NonNull public String getAppPackageName(); method @Nullable public android.credentials.CreateCredentialRequest getCreateCredentialRequest(); method @NonNull public java.util.List<java.lang.String> getDefaultProviderIds(); method @Nullable public android.credentials.GetCredentialRequest getGetCredentialRequest(); + method @NonNull public String getPackageName(); method @NonNull public java.util.List<java.lang.String> getRegistryProviderIds(); method @NonNull public android.credentials.selection.RequestToken getRequestToken(); method @NonNull public String getType(); @@ -6954,6 +6954,8 @@ package android.media { @FlaggedApi("android.media.audiopolicy.enable_fade_manager_configuration") public final class FadeManagerConfiguration implements android.os.Parcelable { method public int describeContents(); method @NonNull public java.util.List<android.media.AudioAttributes> getAudioAttributesWithVolumeShaperConfigs(); + method public static long getDefaultFadeInDurationMillis(); + method public static long getDefaultFadeOutDurationMillis(); method public long getFadeInDelayForOffenders(); method public long getFadeInDurationForAudioAttributes(@NonNull android.media.AudioAttributes); method public long getFadeInDurationForUsage(int); @@ -6979,7 +6981,6 @@ package android.media { field @NonNull public static final android.os.Parcelable.Creator<android.media.FadeManagerConfiguration> CREATOR; field public static final long DURATION_NOT_SET = 0L; // 0x0L field public static final int FADE_STATE_DISABLED = 0; // 0x0 - field public static final int FADE_STATE_ENABLED_AUTO = 2; // 0x2 field public static final int FADE_STATE_ENABLED_DEFAULT = 1; // 0x1 field public static final String TAG = "FadeManagerConfiguration"; field public static final int VOLUME_SHAPER_SYSTEM_FADE_ID = 2; // 0x2 @@ -12404,6 +12405,7 @@ package android.service.euicc { method @Deprecated public int onDownloadSubscription(int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean, boolean); method @Deprecated public abstract int onEraseSubscriptions(int); method public int onEraseSubscriptions(int, int); + method @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") public long onGetAvailableMemoryInBytes(int); method public abstract android.service.euicc.GetDefaultDownloadableSubscriptionListResult onGetDefaultDownloadableSubscriptionList(int, boolean); method public abstract android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, android.telephony.euicc.DownloadableSubscription, boolean); method @NonNull public android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean); @@ -13433,6 +13435,7 @@ package android.service.wearable { method @BinderThread public abstract void onDataProvided(@NonNull android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.function.Consumer<java.lang.Integer>); method @BinderThread public abstract void onDataStreamProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @BinderThread public abstract void onQueryServiceStatus(@NonNull java.util.Set<java.lang.Integer>, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>); + method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @BinderThread public void onSecureWearableConnectionProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @BinderThread public abstract void onStartDetection(@NonNull android.app.ambientcontext.AmbientContextEventRequest, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionResult>); method public abstract void onStopDetection(@NonNull String); field public static final String SERVICE_INTERFACE = "android.service.wearable.WearableSensingService"; @@ -13608,6 +13611,13 @@ package android.telecom { method public final void addExistingConnection(@NonNull android.telecom.PhoneAccountHandle, @NonNull android.telecom.Connection, @NonNull android.telecom.Conference); } + public final class DisconnectCause implements android.os.Parcelable { + ctor @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public DisconnectCause(int, @NonNull CharSequence, @NonNull CharSequence, @NonNull String, int, int, int, @Nullable android.telephony.ims.ImsReasonInfo); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable public android.telephony.ims.ImsReasonInfo getImsReasonInfo(); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public int getTelephonyDisconnectCause(); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public int getTelephonyPreciseDisconnectCause(); + } + public abstract class InCallService extends android.app.Service { method @Deprecated public android.telecom.Phone getPhone(); method @Deprecated public void onPhoneCreated(android.telecom.Phone); @@ -14180,12 +14190,12 @@ package android.telephony { method @NonNull public android.telephony.DataThrottlingRequest.Builder setDataThrottlingAction(int); } - @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public class DomainSelectionService extends android.app.Service { + @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public abstract class DomainSelectionService extends android.app.Service { ctor public DomainSelectionService(); method public void onBarringInfoUpdated(int, int, @NonNull android.telephony.BarringInfo); - method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent); + method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent); method @NonNull public java.util.concurrent.Executor onCreateExecutor(); - method public void onDomainSelection(@NonNull android.telephony.DomainSelectionService.SelectionAttributes, @NonNull android.telephony.TransportSelectorCallback); + method public abstract void onDomainSelection(@NonNull android.telephony.DomainSelectionService.SelectionAttributes, @NonNull android.telephony.TransportSelectorCallback); method public void onServiceStateUpdated(int, int, @NonNull android.telephony.ServiceState); field public static final int SCAN_TYPE_FULL_SERVICE = 2; // 0x2 field public static final int SCAN_TYPE_LIMITED_SERVICE = 1; // 0x1 @@ -14199,7 +14209,7 @@ package android.telephony { method @Nullable public android.net.Uri getAddress(); method @Nullable public String getCallId(); method public int getCsDisconnectCause(); - method @Nullable public android.telephony.EmergencyRegResult getEmergencyRegResult(); + method @Nullable public android.telephony.EmergencyRegistrationResult getEmergencyRegistrationResult(); method @Nullable public android.telephony.ims.ImsReasonInfo getPsDisconnectCause(); method public int getSelectorType(); method public int getSlotIndex(); @@ -14215,13 +14225,13 @@ package android.telephony { @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final class DomainSelectionService.SelectionAttributes.Builder { ctor public DomainSelectionService.SelectionAttributes.Builder(int, int, int); method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes build(); - method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setAddress(@NonNull android.net.Uri); - method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setCallId(@NonNull String); + method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setAddress(@Nullable android.net.Uri); + method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setCallId(@Nullable String); method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setCsDisconnectCause(int); method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setEmergency(boolean); - method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setEmergencyRegResult(@NonNull android.telephony.EmergencyRegResult); + method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setEmergencyRegistrationResult(@Nullable android.telephony.EmergencyRegistrationResult); method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setExitedFromAirplaneMode(boolean); - method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setPsDisconnectCause(@NonNull android.telephony.ims.ImsReasonInfo); + method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setPsDisconnectCause(@Nullable android.telephony.ims.ImsReasonInfo); method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setTestEmergencyNumber(boolean); method @NonNull public android.telephony.DomainSelectionService.SelectionAttributes.Builder setVideoCall(boolean); } @@ -14231,7 +14241,7 @@ package android.telephony { method public void reselectDomain(@NonNull android.telephony.DomainSelectionService.SelectionAttributes); } - @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public final class EmergencyRegResult implements android.os.Parcelable { + @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public final class EmergencyRegistrationResult implements android.os.Parcelable { method public int describeContents(); method public int getAccessNetwork(); method @NonNull public String getCountryIso(); @@ -14244,7 +14254,7 @@ package android.telephony { method public boolean isEmcBearerSupported(); method public boolean isVopsSupported(); method public void writeToParcel(@NonNull android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.telephony.EmergencyRegResult> CREATOR; + field @NonNull public static final android.os.Parcelable.Creator<android.telephony.EmergencyRegistrationResult> CREATOR; } public final class ImsiEncryptionInfo implements android.os.Parcelable { @@ -15316,7 +15326,7 @@ package android.telephony { @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public interface WwanSelectorCallback { method public void onDomainSelected(int, boolean); - method public void onRequestEmergencyNetworkScan(@NonNull java.util.List<java.lang.Integer>, int, boolean, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.telephony.EmergencyRegResult>); + method public void onRequestEmergencyNetworkScan(@NonNull java.util.List<java.lang.Integer>, int, boolean, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.telephony.EmergencyRegistrationResult>); } } @@ -17384,6 +17394,19 @@ package android.telephony.satellite { field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.AntennaPosition> CREATOR; } + @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public class EnableRequestAttributes { + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public boolean isDemoMode(); + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public boolean isEmergencyMode(); + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public boolean isEnabled(); + } + + @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final class EnableRequestAttributes.Builder { + ctor @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public EnableRequestAttributes.Builder(boolean); + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public android.telephony.satellite.EnableRequestAttributes build(); + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public android.telephony.satellite.EnableRequestAttributes.Builder setDemoMode(boolean); + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public android.telephony.satellite.EnableRequestAttributes.Builder setEmergencyMode(boolean); + } + @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class NtnSignalStrength implements android.os.Parcelable { ctor @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public NtnSignalStrength(@Nullable android.telephony.satellite.NtnSignalStrength); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public int describeContents(); @@ -17449,10 +17472,11 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void removeAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestAttachEnabledForCarrier(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestCapabilities(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telephony.satellite.SatelliteCapabilities,android.telephony.satellite.SatelliteManager.SatelliteException>); - method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestEnabled(boolean, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestEnabled(@NonNull android.telephony.satellite.EnableRequestAttributes, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsAttachEnabledForCarrier(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsCommunicationAllowedForCurrentLocation(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsDemoModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsEmergencyModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsProvisioned(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void requestIsSupported(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); @@ -17514,6 +17538,7 @@ package android.telephony.satellite { field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6; // 0x6 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_BUSY = 22; // 0x16 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_ERROR = 4; // 0x4 + field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24; // 0x18 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_NETWORK_ERROR = 5; // 0x5 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_NETWORK_TIMEOUT = 17; // 0x11 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_NOT_AUTHORIZED = 19; // 0x13 diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt index 6c83fd04b76b..8a485d2a8449 100644 --- a/core/api/system-lint-baseline.txt +++ b/core/api/system-lint-baseline.txt @@ -525,6 +525,10 @@ KotlinKeyword: android.app.Notification#when: Avoid field names that are Kotlin hard keywords ("when"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords +KotlinOperator: android.hardware.camera2.extension.CharacteristicsMap#get(String): + Method can be invoked with an indexing operator from Kotlin: `get` (this is usually desirable; just make sure it makes sense for this type of object) + + ListenerLast: android.telephony.satellite.SatelliteManager#stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>) parameter #1: Listeners should always be at end of argument list (method `stopSatelliteTransmissionUpdates`) ListenerLast: android.telephony.satellite.SatelliteManager#stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>) parameter #2: @@ -685,6 +689,8 @@ RequiresPermission: android.app.admin.DevicePolicyManager#generateKeyPair(androi Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getApplicationExemptions(String): Method 'getApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission +RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName): + Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName): Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName): @@ -731,6 +737,8 @@ RequiresPermission: android.app.admin.DevicePolicyManager#setAlwaysOnVpnPackage( Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setApplicationExemptions(String, java.util.Set<java.lang.Integer>): Method 'setApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission +RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int): + Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceProvisioningConfigApplied(): Method 'setDeviceProvisioningConfigApplied' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskFeatures(android.content.ComponentName, int): @@ -2305,14 +2313,14 @@ UnflaggedApi: android.telephony.satellite.SatelliteManager.SatelliteException#Sa New API must be flagged with @FlaggedApi: constructor android.telephony.satellite.SatelliteManager.SatelliteException(int) UnflaggedApi: android.telephony.satellite.SatelliteManager.SatelliteException#getErrorCode(): New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.SatelliteException.getErrorCode() -UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback: - New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteProvisionStateCallback -UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback#onSatelliteProvisionStateChanged(boolean): - New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteProvisionStateCallback.onSatelliteProvisionStateChanged(boolean) UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback: New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteModemStateCallback UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback#onSatelliteModemStateChanged(int): New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteModemStateCallback.onSatelliteModemStateChanged(int) +UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback: + New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteProvisionStateCallback +UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback#onSatelliteProvisionStateChanged(boolean): + New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteProvisionStateCallback.onSatelliteProvisionStateChanged(boolean) UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback: New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteTransmissionUpdateCallback UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback#onReceiveDatagramStateChanged(int, int, int): diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 4a048bd315a0..b17f85356106 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -624,6 +624,7 @@ package android.app.admin { field public static final int OPERATION_SET_APPLICATION_HIDDEN = 15; // 0xf field public static final int OPERATION_SET_APPLICATION_RESTRICTIONS = 16; // 0x10 field public static final int OPERATION_SET_CAMERA_DISABLED = 31; // 0x1f + field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int OPERATION_SET_CONTENT_PROTECTION_POLICY = 41; // 0x29 field public static final int OPERATION_SET_FACTORY_RESET_PROTECTION_POLICY = 32; // 0x20 field public static final int OPERATION_SET_GLOBAL_PRIVATE_DNS = 33; // 0x21 field public static final int OPERATION_SET_KEEP_UNINSTALLED_PACKAGES = 17; // 0x11 @@ -1282,10 +1283,6 @@ package android.credentials.selection { field public static final int RESULT_CODE_DIALOG_USER_CANCELED = 0; // 0x0 } - @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class CancelSelectionRequest implements android.os.Parcelable { - ctor @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public CancelSelectionRequest(@NonNull android.os.IBinder, boolean, @NonNull String); - } - @FlaggedApi("android.credentials.flags.configurable_selector_ui_enabled") public final class CreateCredentialProviderData extends android.credentials.selection.ProviderData implements android.os.Parcelable { ctor public CreateCredentialProviderData(@NonNull String, @NonNull java.util.List<android.credentials.selection.Entry>, @Nullable android.credentials.selection.Entry); method @Nullable public android.credentials.selection.Entry getRemoteEntry(); diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt index 5e904ef947c8..b938f0ffef76 100644 --- a/core/api/test-lint-baseline.txt +++ b/core/api/test-lint-baseline.txt @@ -673,6 +673,8 @@ RequiresPermission: android.app.admin.DevicePolicyManager#generateKeyPair(androi Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getApplicationExemptions(String): Method 'getApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission +RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName): + Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName): Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName): @@ -721,6 +723,8 @@ RequiresPermission: android.app.admin.DevicePolicyManager#setAlwaysOnVpnPackage( Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setApplicationExemptions(String, java.util.Set<java.lang.Integer>): Method 'setApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission +RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int): + Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceOwner(android.content.ComponentName, int): Method 'setDeviceOwner' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceProvisioningConfigApplied(): diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 0a34d36f204e..aa9de814b4c5 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -23,6 +23,7 @@ import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICO import static android.app.admin.DevicePolicyResources.UNDEFINED; import static android.graphics.drawable.Icon.TYPE_URI; import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP; +import static android.app.Flags.evenlyDividedCallStyleActionLayout; import static java.util.Objects.requireNonNull; @@ -3540,15 +3541,12 @@ public class Notification implements Parcelable * Sets the token used for background operations for the pending intents associated with this * notification. * - * This token is automatically set during deserialization for you, you usually won't need to - * call this unless you want to change the existing token, if any. - * * @hide */ - public void clearAllowlistToken() { - mAllowlistToken = null; + public void overrideAllowlistToken(IBinder token) { + mAllowlistToken = token; if (publicVersion != null) { - publicVersion.clearAllowlistToken(); + publicVersion.overrideAllowlistToken(token); } } @@ -5957,7 +5955,7 @@ public class Notification implements Parcelable // there is enough space to do so (and fall back to the left edge if not). big.setInt(R.id.actions, "setCollapsibleIndentDimen", R.dimen.call_notification_collapsible_indent); - if (CallStyle.USE_NEW_ACTION_LAYOUT) { + if (evenlyDividedCallStyleActionLayout()) { if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { Log.d(TAG, "setting evenly divided mode on action list"); } @@ -6439,7 +6437,7 @@ public class Notification implements Parcelable title = ContrastColorUtil.ensureColorSpanContrast(title, buttonFillColor); } final CharSequence label = ensureColorSpanContrast(title, p); - if (p.mCallStyleActions && CallStyle.USE_NEW_ACTION_LAYOUT) { + if (p.mCallStyleActions && evenlyDividedCallStyleActionLayout()) { if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { Log.d(TAG, "new action layout enabled, gluing instead of setting text"); } @@ -6463,7 +6461,7 @@ public class Notification implements Parcelable button.setColorStateList(R.id.action0, "setButtonBackground", ColorStateList.valueOf(buttonFillColor)); if (p.mCallStyleActions) { - if (CallStyle.USE_NEW_ACTION_LAYOUT) { + if (evenlyDividedCallStyleActionLayout()) { if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) { Log.d(TAG, "new action layout enabled, gluing instead of setting icon"); } @@ -9600,11 +9598,6 @@ public class Notification implements Parcelable /** * @hide */ - public static final boolean USE_NEW_ACTION_LAYOUT = false; - - /** - * @hide - */ public static final boolean DEBUG_NEW_ACTION_LAYOUT = true; /** diff --git a/core/java/android/app/QueuedWork.java b/core/java/android/app/QueuedWork.java index 6a114f908a22..60b61f35e591 100644 --- a/core/java/android/app/QueuedWork.java +++ b/core/java/android/app/QueuedWork.java @@ -27,6 +27,7 @@ import android.os.StrictMode; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ExponentiallyBucketedHistogram; import java.util.LinkedList; @@ -114,6 +115,20 @@ public class QueuedWork { } /** + * Tear down the handler. + */ + @VisibleForTesting + public static void resetHandler() { + synchronized (sLock) { + if (sHandler == null) { + return; + } + sHandler.getLooper().quitSafely(); + sHandler = null; + } + } + + /** * Remove all Messages from the Handler with the given code. * * This method intentionally avoids creating the Handler if it doesn't diff --git a/core/java/android/app/admin/DevicePolicyIdentifiers.java b/core/java/android/app/admin/DevicePolicyIdentifiers.java index b0bec783555b..d7aafa010e1d 100644 --- a/core/java/android/app/admin/DevicePolicyIdentifiers.java +++ b/core/java/android/app/admin/DevicePolicyIdentifiers.java @@ -22,7 +22,6 @@ import android.annotation.TestApi; import android.app.admin.flags.Flags; import android.os.UserManager; - import java.util.Objects; /** @@ -163,6 +162,12 @@ public final class DevicePolicyIdentifiers { public static final String CROSS_PROFILE_WIDGET_PROVIDER_POLICY = "crossProfileWidgetProvider"; /** + * String identifier for {@link DevicePolicyManager#setContentProtectionPolicy}. + */ + @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED) + public static final String CONTENT_PROTECTION_POLICY = "contentProtection"; + + /** * String identifier for {@link DevicePolicyManager#setUsbDataSignalingEnabled}. */ @FlaggedApi(Flags.FLAG_POLICY_ENGINE_MIGRATION_V2_ENABLED) diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 86d0125fd7a2..1ef434633612 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -24,6 +24,7 @@ import static android.Manifest.permission.MANAGE_DEVICE_POLICY_APPS_CONTROL; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CAMERA; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CERTIFICATES; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE; +import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_FACTORY_RESET; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_INPUT_METHODS; @@ -53,7 +54,6 @@ import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM; import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; -import static android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; @@ -3825,6 +3825,10 @@ public class DevicePolicyManager { /** @hide */ @TestApi public static final int OPERATION_UNINSTALL_CA_CERT = 40; + /** @hide */ + @TestApi + @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED) + public static final int OPERATION_SET_CONTENT_PROTECTION_POLICY = 41; private static final String PREFIX_OPERATION = "OPERATION_"; @@ -3869,7 +3873,8 @@ public class DevicePolicyManager { OPERATION_SET_PERMISSION_GRANT_STATE, OPERATION_SET_PERMISSION_POLICY, OPERATION_SET_RESTRICTIONS_PROVIDER, - OPERATION_UNINSTALL_CA_CERT + OPERATION_UNINSTALL_CA_CERT, + OPERATION_SET_CONTENT_PROTECTION_POLICY }) @Retention(RetentionPolicy.SOURCE) public static @interface DevicePolicyOperation { @@ -4095,15 +4100,15 @@ public class DevicePolicyManager { } /** Indicates that content protection is not controlled by policy, allowing user to choose. */ - @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED) + @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED) public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0; - /** Indicates that content protection is controlled and disabled by a policy. */ - @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED) + /** Indicates that content protection is controlled and disabled by a policy (default). */ + @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED) public static final int CONTENT_PROTECTION_DISABLED = 1; /** Indicates that content protection is controlled and enabled by a policy. */ - @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED) + @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED) public static final int CONTENT_PROTECTION_ENABLED = 2; /** @hide */ @@ -4118,6 +4123,86 @@ public class DevicePolicyManager { public @interface ContentProtectionPolicy {} /** + * Sets the content protection policy which controls scanning for deceptive apps. + * <p> + * This function can only be called by the device owner, a profile owner of an affiliated user + * or profile, or the profile owner when no device owner is set or holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CONTENT_PROTECTION}. See + * {@link #isAffiliatedUser}. + * Any policy set via this method will be cleared if the user becomes unaffiliated. + * <p> + * After the content protection policy has been set, + * {@link PolicyUpdateReceiver#onPolicySetResult(Context, String, Bundle, TargetUser, + * PolicyUpdateResult)} will notify the admin on whether the policy was successfully set or not. + * This callback will contain: + * <ul> + * <li> The policy identifier {@link DevicePolicyIdentifiers#CONTENT_PROTECTION_POLICY} + * <li> The {@link TargetUser} that this policy relates to + * <li> The {@link PolicyUpdateResult}, which will be + * {@link PolicyUpdateResult#RESULT_POLICY_SET} if the policy was successfully set or the + * reason the policy failed to be set + * (e.g. {@link PolicyUpdateResult#RESULT_FAILURE_CONFLICTING_ADMIN_POLICY}) + * </ul> + * If there has been a change to the policy, + * {@link PolicyUpdateReceiver#onPolicyChanged(Context, String, Bundle, TargetUser, + * PolicyUpdateResult)} will notify the admin of this change. This callback will contain the + * same parameters as PolicyUpdateReceiver#onPolicySetResult and the {@link PolicyUpdateResult} + * will contain the reason why the policy changed. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the + * caller is not a device admin. + * @param policy The content protection policy to set. One of {@link + * #CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY}, + * {@link #CONTENT_PROTECTION_DISABLED} or {@link #CONTENT_PROTECTION_ENABLED}. + * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an + * affiliated user or profile, or the profile owner when no device owner is set or holder of the + * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CONTENT_PROTECTION}. + * @see #isAffiliatedUser + */ + @RequiresPermission(value = MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional = true) + @SupportsCoexistence + @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED) + public void setContentProtectionPolicy( + @Nullable ComponentName admin, @ContentProtectionPolicy int policy) { + throwIfParentInstance("setContentProtectionPolicy"); + if (mService != null) { + try { + mService.setContentProtectionPolicy(admin, mContext.getPackageName(), policy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Returns the current content protection policy. + * <p> + * The returned policy will be the current resolved policy rather than the policy set by the + * calling admin. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the + * caller is not a device admin. + * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an + * affiliated user or profile, or the profile owner when no device owner is set or holder of the + * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CONTENT_PROTECTION}. + * @see #isAffiliatedUser + * @see #setContentProtectionPolicy + */ + @RequiresPermission(value = MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional = true) + @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED) + public @ContentProtectionPolicy int getContentProtectionPolicy(@Nullable ComponentName admin) { + throwIfParentInstance("getContentProtectionPolicy"); + if (mService != null) { + try { + return mService.getContentProtectionPolicy(admin, mContext.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return CONTENT_PROTECTION_DISABLED; + } + + /** * This object is a single place to tack on invalidation and disable calls. All * binder caches in this class derive from this Config, so all can be invalidated or * disabled through this Config. diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 575fa4cac0b8..efcf5633515d 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -610,4 +610,7 @@ interface IDevicePolicyManager { String getFinancedDeviceKioskRoleHolder(String callerPackageName); void calculateHasIncompatibleAccounts(); + + void setContentProtectionPolicy(in ComponentName who, String callerPackageName, int policy); + int getContentProtectionPolicy(in ComponentName who, String callerPackageName); } diff --git a/core/java/android/app/admin/SecurityLog.java b/core/java/android/app/admin/SecurityLog.java index ca2e97edc432..ed1b8ca9b5bd 100644 --- a/core/java/android/app/admin/SecurityLog.java +++ b/core/java/android/app/admin/SecurityLog.java @@ -17,12 +17,14 @@ package android.app.admin; import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; +import android.app.admin.flags.Flags; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.os.Build; @@ -99,6 +101,7 @@ public class SecurityLog { TAG_PACKAGE_INSTALLED, TAG_PACKAGE_UPDATED, TAG_PACKAGE_UNINSTALLED, + TAG_BACKUP_SERVICE_TOGGLED, }) public @interface SecurityLogTag {} @@ -599,6 +602,18 @@ public class SecurityLog { public static final int TAG_PACKAGE_UNINSTALLED = SecurityLogTags.SECURITY_PACKAGE_UNINSTALLED; /** + * Indicates that an admin has enabled or disabled backup service. The log entry contains the + * following information about the event encapsulated in an {@link Object} array, accessible + * via {@link SecurityEvent#getData()}: + * <li> [0] admin package name ({@code String}) + * <li> [1] admin user ID ({@code Integer}) + * <li> [2] backup service state ({@code Integer}, 1 for enabled, 0 for disabled) + * @see DevicePolicyManager#setBackupServiceEnabled(ComponentName, boolean) + */ + @FlaggedApi(Flags.FLAG_BACKUP_SERVICE_SECURITY_LOG_EVENT_ENABLED) + public static final int TAG_BACKUP_SERVICE_TOGGLED = + SecurityLogTags.SECURITY_BACKUP_SERVICE_TOGGLED; + /** * Event severity level indicating that the event corresponds to normal workflow. */ public static final int LEVEL_INFO = 1; diff --git a/core/java/android/app/admin/SecurityLogTags.logtags b/core/java/android/app/admin/SecurityLogTags.logtags index e4af8dd91583..7b3aa7b589b7 100644 --- a/core/java/android/app/admin/SecurityLogTags.logtags +++ b/core/java/android/app/admin/SecurityLogTags.logtags @@ -47,4 +47,5 @@ option java_package android.app.admin 210040 security_bluetooth_disconnection (addr|3),(reason|3) 210041 security_package_installed (package_name|3),(version_code|1),(user_id|1) 210042 security_package_updated (package_name|3),(version_code|1),(user_id|1) -210043 security_package_uninstalled (package_name|3),(version_code|1),(user_id|1)
\ No newline at end of file +210043 security_package_uninstalled (package_name|3),(version_code|1),(user_id|1) +210044 security_backup_service_toggled (package|3),(admin_user|1),(enabled|1)
\ No newline at end of file diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index b3ecd92c56c9..561eb00ac7eb 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -62,3 +62,10 @@ flag { description: "Exempt the default sms app of the context user for suspension when calling setPersonalAppsSuspended" bug: "309183330" } + +flag { + name: "backup_service_security_log_event_enabled" + namespace: "enterprise" + description: "Emit a security log event when DPM.setBackupServiceEnabled is called" + bug: "304999634" +} diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index c40b23ed3cd0..274d02a79270 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -56,4 +56,14 @@ flag { namespace: "systemui" description: "This flag enables the API to allow setting VibrationEffect for NotificationChannels" bug: "241732519" +} + +flag { + name: "evenly_divided_call_style_action_layout" + namespace: "systemui" + description: "Evenly divides horizontal space for action buttons in CallStyle notifications." + bug: "268733030" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/app/wearable/IWearableSensingManager.aidl b/core/java/android/app/wearable/IWearableSensingManager.aidl index ff37bd848d61..9d55ce28c84e 100644 --- a/core/java/android/app/wearable/IWearableSensingManager.aidl +++ b/core/java/android/app/wearable/IWearableSensingManager.aidl @@ -28,6 +28,8 @@ import android.os.SharedMemory; */ interface IWearableSensingManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)") + void provideWearableConnection(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)") void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)") void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback); diff --git a/core/java/android/app/wearable/WearableSensingManager.java b/core/java/android/app/wearable/WearableSensingManager.java index eca0039c20f4..401d0b7b47fd 100644 --- a/core/java/android/app/wearable/WearableSensingManager.java +++ b/core/java/android/app/wearable/WearableSensingManager.java @@ -26,6 +26,7 @@ import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.app.ambientcontext.AmbientContextEvent; +import android.companion.CompanionDeviceManager; import android.content.Context; import android.os.Binder; import android.os.ParcelFileDescriptor; @@ -36,6 +37,8 @@ import android.os.SharedMemory; import android.service.wearable.WearableSensingService; import android.system.OsConstants; +import java.io.InputStream; +import java.io.OutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.Executor; @@ -107,6 +110,14 @@ public class WearableSensingManager { @FlaggedApi(Flags.FLAG_ENABLE_UNSUPPORTED_OPERATION_STATUS_CODE) public static final int STATUS_UNSUPPORTED_OPERATION = 6; + /** + * The value of the status code that indicates an error occurred in the encrypted channel backed + * by the provided connection. See {@link #provideWearableConnection(ParcelFileDescriptor, + * Executor, Consumer)}. + */ + @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API) + public static final int STATUS_CHANNEL_ERROR = 7; + /** @hide */ @IntDef(prefix = { "STATUS_" }, value = { STATUS_UNKNOWN, @@ -115,7 +126,8 @@ public class WearableSensingManager { STATUS_SERVICE_UNAVAILABLE, STATUS_WEARABLE_UNAVAILABLE, STATUS_ACCESS_DENIED, - STATUS_UNSUPPORTED_OPERATION + STATUS_UNSUPPORTED_OPERATION, + STATUS_CHANNEL_ERROR }) @Retention(RetentionPolicy.SOURCE) public @interface StatusCode {} @@ -132,6 +144,60 @@ public class WearableSensingManager { } /** + * Provides a remote wearable device connection to the WearableSensingService and sends the + * resulting status to the {@code statusConsumer} after the call. + * + * <p>This is used by applications that will also provide an implementation of the isolated + * WearableSensingService. + * + * <p>The provided {@code wearableConnection} is expected to be a connection to a remotely + * connected wearable device. This {@code wearableConnection} will be attached to + * CompanionDeviceManager via {@link CompanionDeviceManager#attachSystemDataTransport(int, + * InputStream, OutputStream)}, which will create an encrypted channel using {@code + * wearableConnection} as the raw underlying connection. The wearable device is expected to + * attach its side of the raw connection to its CompanionDeviceManager via the same method so + * that the two CompanionDeviceManagers on the two devices can perform attestation and set up + * the encrypted channel. Attestation requirements are listed in + * com.android.server.security.AttestationVerificationPeerDeviceVerifier + * + * <p>A proxy to the encrypted channel will be provided to the WearableSensingService, which is + * referred to as the secureWearableConnection in WearableSensingService. Any data written to + * secureWearableConnection will be encrypted by CompanionDeviceManager and sent over the raw + * {@code wearableConnection} to the remote wearable device, which is expected to use its + * CompanionDeviceManager to decrypt the data. Encrypted data arriving at the raw {@code + * wearableConnection} will be decrypted by CompanionDeviceManager and be readable as plain text + * from secureWearableConnection. The raw {@code wearableConnection} provided to this method + * will not be directly available to the WearableSensingService. + * + * <p>If an error occurred in the encrypted channel (such as the underlying stream closed), the + * system will send a status code of {@link STATUS_CHANNEL_ERROR} to the {@code statusConsumer} + * and kill the WearableSensingService process. + * + * <p>Before providing the secureWearableConnection, the system will restart the + * WearableSensingService process. Other method calls into WearableSensingService may be dropped + * during the restart. The caller is responsible for ensuring other method calls are queued + * until a success status is returned from the {@code statusConsumer}. + * + * @param wearableConnection The connection to provide + * @param executor Executor on which to run the consumer callback + * @param statusConsumer A consumer that handles the status codes for providing the connection + * and errors in the encrypted channel. + */ + @RequiresPermission(Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) + @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API) + public void provideWearableConnection( + @NonNull ParcelFileDescriptor wearableConnection, + @NonNull @CallbackExecutor Executor executor, + @NonNull @StatusCode Consumer<Integer> statusConsumer) { + try { + RemoteCallback callback = createStatusCallback(executor, statusConsumer); + mService.provideWearableConnection(wearableConnection, callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Provides a data stream to the WearableSensingService that's backed by the * parcelFileDescriptor, and sends the result to the {@link Consumer} right after the call. * This is used by applications that will also provide an implementation of @@ -149,15 +215,7 @@ public class WearableSensingManager { @NonNull @CallbackExecutor Executor executor, @NonNull @StatusCode Consumer<Integer> statusConsumer) { try { - RemoteCallback callback = new RemoteCallback(result -> { - int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY); - final long identity = Binder.clearCallingIdentity(); - try { - executor.execute(() -> statusConsumer.accept(status)); - } finally { - Binder.restoreCallingIdentity(identity); - } - }); + RemoteCallback callback = createStatusCallback(executor, statusConsumer); mService.provideDataStream(parcelFileDescriptor, callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -191,19 +249,24 @@ public class WearableSensingManager { @NonNull @CallbackExecutor Executor executor, @NonNull @StatusCode Consumer<Integer> statusConsumer) { try { - RemoteCallback callback = new RemoteCallback(result -> { - int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY); - final long identity = Binder.clearCallingIdentity(); - try { - executor.execute(() -> statusConsumer.accept(status)); - } finally { - Binder.restoreCallingIdentity(identity); - } - }); + RemoteCallback callback = createStatusCallback(executor, statusConsumer); mService.provideData(data, sharedMemory, callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + private static RemoteCallback createStatusCallback( + Executor executor, Consumer<Integer> statusConsumer) { + return new RemoteCallback( + result -> { + int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> statusConsumer.accept(status)); + } finally { + Binder.restoreCallingIdentity(identity); + } + }); + } } diff --git a/core/java/android/content/pm/IBackgroundInstallControlService.aidl b/core/java/android/content/pm/IBackgroundInstallControlService.aidl index c8e7caebc821..2e7f19ed2e95 100644 --- a/core/java/android/content/pm/IBackgroundInstallControlService.aidl +++ b/core/java/android/content/pm/IBackgroundInstallControlService.aidl @@ -17,10 +17,15 @@ package android.content.pm; import android.content.pm.ParceledListSlice; +import android.os.IRemoteCallback; /** * {@hide} */ interface IBackgroundInstallControlService { ParceledListSlice getBackgroundInstalledPackages(long flags, int userId); + + void registerBackgroundInstallCallback(IRemoteCallback callback); + + void unregisterBackgroundInstallCallback(IRemoteCallback callback); } diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 08f185366c49..bff90f1d2298 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -846,4 +846,6 @@ interface IPackageManager { @EnforcePermission("GET_APP_METADATA") int getAppMetadataSource(String packageName, int userId); + + ComponentName getDomainVerificationAgent(); } diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java index f54b2ac06929..d347a0e8ae63 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -16,9 +16,6 @@ package android.content.pm; -import static android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES; - -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -476,22 +473,26 @@ public final class UserProperties implements Parcelable { ) public @interface ProfileApiVisibility { } - /* - * The api visibility value for this profile user is undefined or unknown. + + /** + * The api visibility value for this profile user is undefined or unknown. + * + * @hide */ - @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) public static final int PROFILE_API_VISIBILITY_UNKNOWN = -1; /** * Indicates that information about this profile user should be shown in API surfaces. + * + * @hide */ - @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) public static final int PROFILE_API_VISIBILITY_VISIBLE = 0; /** * Indicates that information about this profile should be not be visible in API surfaces. + * + * @hide */ - @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) public static final int PROFILE_API_VISIBILITY_HIDDEN = 1; @@ -555,9 +556,7 @@ public final class UserProperties implements Parcelable { setShowInQuietMode(orig.getShowInQuietMode()); setShowInSharingSurfaces(orig.getShowInSharingSurfaces()); setCrossProfileContentSharingStrategy(orig.getCrossProfileContentSharingStrategy()); - if (android.multiuser.Flags.supportHidingProfiles()) { - setProfileApiVisibility(orig.getProfileApiVisibility()); - } + setProfileApiVisibility(orig.getProfileApiVisibility()); } /** @@ -1002,9 +1001,10 @@ public final class UserProperties implements Parcelable { /** * Returns the visibility of the profile user in API surfaces. Any information linked to the * profile (userId, package names) should be hidden API surfaces if a user is marked as hidden. + * + * @hide */ @NonNull - @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) public @ProfileApiVisibility int getProfileApiVisibility() { if (isPresent(INDEX_PROFILE_API_VISIBILITY)) return mProfileApiVisibility; if (mDefaultProperties != null) return mDefaultProperties.mProfileApiVisibility; @@ -1012,7 +1012,6 @@ public final class UserProperties implements Parcelable { } /** @hide */ @NonNull - @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) public void setProfileApiVisibility(@ProfileApiVisibility int profileApiVisibility) { this.mProfileApiVisibility = profileApiVisibility; setPresent(INDEX_PROFILE_API_VISIBILITY); @@ -1053,9 +1052,6 @@ public final class UserProperties implements Parcelable { @Override public String toString() { - String profileApiVisibility = - android.multiuser.Flags.supportHidingProfiles() ? ", mProfileApiVisibility=" - + getProfileApiVisibility() : ""; // Please print in increasing order of PropertyIndex. return "UserProperties{" + "mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent) @@ -1079,7 +1075,7 @@ public final class UserProperties implements Parcelable { + ", mDeleteAppWithParent=" + getDeleteAppWithParent() + ", mAlwaysVisible=" + getAlwaysVisible() + ", mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy() - + ", mProfileApiVisibility=" + profileApiVisibility + + ", mProfileApiVisibility=" + getProfileApiVisibility() + ", mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen() + "}"; } @@ -1114,9 +1110,7 @@ public final class UserProperties implements Parcelable { pw.println(prefix + " mAlwaysVisible=" + getAlwaysVisible()); pw.println(prefix + " mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy()); - if (android.multiuser.Flags.supportHidingProfiles()) { - pw.println(prefix + " mProfileApiVisibility=" + getProfileApiVisibility()); - } + pw.println(prefix + " mProfileApiVisibility=" + getProfileApiVisibility()); pw.println(prefix + " mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen()); } @@ -1203,9 +1197,7 @@ public final class UserProperties implements Parcelable { setCrossProfileContentSharingStrategy(parser.getAttributeInt(i)); break; case ATTR_PROFILE_API_VISIBILITY: - if (android.multiuser.Flags.supportHidingProfiles()) { - setProfileApiVisibility(parser.getAttributeInt(i)); - } + setProfileApiVisibility(parser.getAttributeInt(i)); break; case ITEMS_RESTRICTED_ON_HOME_SCREEN: setItemsRestrictedOnHomeScreen(parser.getAttributeBoolean(i)); @@ -1293,10 +1285,8 @@ public final class UserProperties implements Parcelable { mCrossProfileContentSharingStrategy); } if (isPresent(INDEX_PROFILE_API_VISIBILITY)) { - if (android.multiuser.Flags.supportHidingProfiles()) { - serializer.attributeInt(null, ATTR_PROFILE_API_VISIBILITY, - mProfileApiVisibility); - } + serializer.attributeInt(null, ATTR_PROFILE_API_VISIBILITY, + mProfileApiVisibility); } if (isPresent(INDEX_ITEMS_RESTRICTED_ON_HOME_SCREEN)) { serializer.attributeBoolean(null, ITEMS_RESTRICTED_ON_HOME_SCREEN, @@ -1566,7 +1556,6 @@ public final class UserProperties implements Parcelable { * @hide */ @NonNull - @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES) public Builder setProfileApiVisibility(@ProfileApiVisibility int profileApiVisibility){ mProfileApiVisibility = profileApiVisibility; return this; @@ -1650,9 +1639,7 @@ public final class UserProperties implements Parcelable { setDeleteAppWithParent(deleteAppWithParent); setAlwaysVisible(alwaysVisible); setCrossProfileContentSharingStrategy(crossProfileContentSharingStrategy); - if (android.multiuser.Flags.supportHidingProfiles()) { - setProfileApiVisibility(profileApiVisibility); - } + setProfileApiVisibility(profileApiVisibility); setItemsRestrictedOnHomeScreen(itemsRestrictedOnHomeScreen); } } diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index f31521da354f..5e9d8f0a9b7e 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -206,3 +206,11 @@ flag { description: "Feature flag to enable pre-verified domains" bug: "307327678" } + +flag { + name: "min_target_sdk_24" + namespace: "responsible_apis" + description: "Feature flag to bump min target sdk to 24" + bug: "297603927" + is_fixed_read_only: true +} diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 7ded7472bc1f..4b890faf527e 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -109,18 +109,10 @@ flag { } flag { - name: "allow_private_profile_apis" + name: "enable_private_space_features" namespace: "profile_experiences" - description: "Enable only the API changes to support private space" - bug: "299069460" -} - -flag { - name: "support_hiding_profiles" - namespace: "profile_experiences" - description: "Allow the use of a hide_profile property to hide some profiles behind a permission" - bug: "316362775" - is_fixed_read_only: true + description: "Enable the support for private space and all its sub-features" + bug: "286418785" } flag { diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationState.java b/core/java/android/content/pm/verify/domain/DomainVerificationState.java index 8e28042bf581..6c4fe376209e 100644 --- a/core/java/android/content/pm/verify/domain/DomainVerificationState.java +++ b/core/java/android/content/pm/verify/domain/DomainVerificationState.java @@ -33,7 +33,8 @@ public interface DomainVerificationState { STATE_DENIED, STATE_LEGACY_FAILURE, STATE_SYS_CONFIG, - STATE_FIRST_VERIFIER_DEFINED + STATE_PRE_VERIFIED, + STATE_FIRST_VERIFIER_DEFINED, }) @interface State { } @@ -92,6 +93,13 @@ public interface DomainVerificationState { int STATE_SYS_CONFIG = 7; /** + * The application has temporarily been granted auto verification for a set of domains as + * specified by a trusted installer during the installation. This will treat the domain as + * verified, but it should be updated by the verification agent. + */ + int STATE_PRE_VERIFIED = 8; + + /** * @see DomainVerificationInfo#STATE_FIRST_VERIFIER_DEFINED */ int STATE_FIRST_VERIFIER_DEFINED = 0b10000000000; @@ -115,6 +123,8 @@ public interface DomainVerificationState { return "legacy_failure"; case DomainVerificationState.STATE_SYS_CONFIG: return "system_configured"; + case DomainVerificationState.STATE_PRE_VERIFIED: + return "pre_verified"; default: return String.valueOf(state); } @@ -135,6 +145,7 @@ public interface DomainVerificationState { case STATE_DENIED: case STATE_LEGACY_FAILURE: case STATE_SYS_CONFIG: + case STATE_PRE_VERIFIED: default: return false; } @@ -151,6 +162,7 @@ public interface DomainVerificationState { case DomainVerificationState.STATE_MIGRATED: case DomainVerificationState.STATE_RESTORED: case DomainVerificationState.STATE_SYS_CONFIG: + case DomainVerificationState.STATE_PRE_VERIFIED: return true; case DomainVerificationState.STATE_NO_RESPONSE: case DomainVerificationState.STATE_DENIED: @@ -173,6 +185,7 @@ public interface DomainVerificationState { case DomainVerificationState.STATE_MIGRATED: case DomainVerificationState.STATE_RESTORED: case DomainVerificationState.STATE_LEGACY_FAILURE: + case DomainVerificationState.STATE_PRE_VERIFIED: return true; case DomainVerificationState.STATE_APPROVED: case DomainVerificationState.STATE_DENIED: @@ -194,6 +207,7 @@ public interface DomainVerificationState { case STATE_RESTORED: case STATE_APPROVED: case STATE_DENIED: + case STATE_PRE_VERIFIED: return true; case STATE_NO_RESPONSE: case STATE_LEGACY_FAILURE: diff --git a/core/java/android/credentials/Constants.java b/core/java/android/credentials/Constants.java new file mode 100644 index 000000000000..ea30c44fba1a --- /dev/null +++ b/core/java/android/credentials/Constants.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +/** + * Constants for credential manager service that doesn't fit into other structures + * + * @hide + */ +public class Constants { + /** + * The request is success and user selected an entry + */ + public static final int SUCCESS_CREDMAN_SELECTOR = 0; + /** + * The error code for ui getting cancelled by user + */ + public static final int FAILURE_CREDMAN_SELECTOR = -1; +} diff --git a/core/java/android/credentials/GetCandidateCredentialsResponse.java b/core/java/android/credentials/GetCandidateCredentialsResponse.java index 73361ad25964..6e53fd99873b 100644 --- a/core/java/android/credentials/GetCandidateCredentialsResponse.java +++ b/core/java/android/credentials/GetCandidateCredentialsResponse.java @@ -41,8 +41,6 @@ public final class GetCandidateCredentialsResponse implements Parcelable { private final PendingIntent mPendingIntent; - private final GetCredentialResponse mGetCredentialResponse; - /** * @hide */ @@ -52,7 +50,6 @@ public final class GetCandidateCredentialsResponse implements Parcelable { ) { mCandidateProviderDataList = null; mPendingIntent = null; - mGetCredentialResponse = getCredentialResponse; } /** @@ -68,7 +65,6 @@ public final class GetCandidateCredentialsResponse implements Parcelable { /*valueName=*/ "candidateProviderDataList"); mCandidateProviderDataList = new ArrayList<>(candidateProviderDataList); mPendingIntent = pendingIntent; - mGetCredentialResponse = null; } /** @@ -85,15 +81,6 @@ public final class GetCandidateCredentialsResponse implements Parcelable { * * @hide */ - public GetCredentialResponse getGetCredentialResponse() { - return mGetCredentialResponse; - } - - /** - * Returns candidate provider data list. - * - * @hide - */ public PendingIntent getPendingIntent() { return mPendingIntent; } @@ -106,14 +93,12 @@ public final class GetCandidateCredentialsResponse implements Parcelable { AnnotationValidations.validate(NonNull.class, null, mCandidateProviderDataList); mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); - mGetCredentialResponse = in.readTypedObject(GetCredentialResponse.CREATOR); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeTypedList(mCandidateProviderDataList); dest.writeTypedObject(mPendingIntent, flags); - dest.writeTypedObject(mGetCredentialResponse, flags); } @Override diff --git a/core/java/android/credentials/selection/CancelSelectionRequest.java b/core/java/android/credentials/selection/CancelSelectionRequest.java index 2662d761c1c0..55acfdbddf94 100644 --- a/core/java/android/credentials/selection/CancelSelectionRequest.java +++ b/core/java/android/credentials/selection/CancelSelectionRequest.java @@ -21,7 +21,6 @@ import static android.credentials.flags.Flags.FLAG_CONFIGURABLE_SELECTOR_UI_ENAB import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; -import android.annotation.TestApi; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; @@ -59,7 +58,7 @@ public final class CancelSelectionRequest implements Parcelable { private final boolean mShouldShowCancellationExplanation; @NonNull - private final String mAppPackageName; + private final String mPackageName; /** * Returns the request token matching the user request that should be cancelled. @@ -85,8 +84,8 @@ public final class CancelSelectionRequest implements Parcelable { * metadata (e.g. "Cancelled by `App Name`"). */ @NonNull - public String getAppPackageName() { - return mAppPackageName; + public String getPackageName() { + return mPackageName; } /** @@ -98,33 +97,36 @@ public final class CancelSelectionRequest implements Parcelable { return mShouldShowCancellationExplanation; } + /** * Constructs a {@link CancelSelectionRequest}. * - * @hide + * @param requestToken request token matching the app request that should be cancelled + * @param shouldShowCancellationExplanation whether the UI should display some informational + * cancellation message before closing + * @param packageName package that is invoking this request + * */ - @TestApi - @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED) - public CancelSelectionRequest(@NonNull IBinder token, boolean shouldShowCancellationExplanation, - @NonNull String appPackageName) { - mToken = token; + public CancelSelectionRequest(@NonNull RequestToken requestToken, + boolean shouldShowCancellationExplanation, @NonNull String packageName) { + mToken = requestToken.getToken(); mShouldShowCancellationExplanation = shouldShowCancellationExplanation; - mAppPackageName = appPackageName; + mPackageName = packageName; } private CancelSelectionRequest(@NonNull Parcel in) { mToken = in.readStrongBinder(); AnnotationValidations.validate(NonNull.class, null, mToken); mShouldShowCancellationExplanation = in.readBoolean(); - mAppPackageName = in.readString8(); - AnnotationValidations.validate(NonNull.class, null, mAppPackageName); + mPackageName = in.readString8(); + AnnotationValidations.validate(NonNull.class, null, mPackageName); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeStrongBinder(mToken); dest.writeBoolean(mShouldShowCancellationExplanation); - dest.writeString8(mAppPackageName); + dest.writeString8(mPackageName); } @Override diff --git a/core/java/android/credentials/selection/Constants.java b/core/java/android/credentials/selection/Constants.java index 7e6c7810f1ab..f7fec237dd08 100644 --- a/core/java/android/credentials/selection/Constants.java +++ b/core/java/android/credentials/selection/Constants.java @@ -36,5 +36,11 @@ public class Constants { public static final String EXTRA_REQ_FOR_ALL_OPTIONS = "android.credentials.selection.extra.REQ_FOR_ALL_OPTIONS"; + /** + * The intent extra key for the final result receiver object + */ + public static final String EXTRA_FINAL_RESPONSE_RECEIVER = + "android.credentials.selection.extra.FINAL_RESPONSE_RECEIVER"; + private Constants() {} } diff --git a/core/java/android/credentials/selection/IntentFactory.java b/core/java/android/credentials/selection/IntentFactory.java index 1837976f5d1f..ac2bae495219 100644 --- a/core/java/android/credentials/selection/IntentFactory.java +++ b/core/java/android/credentials/selection/IntentFactory.java @@ -210,7 +210,8 @@ public class IntentFactory { .config_credentialManagerDialogComponent)); intent.setComponent(componentName); intent.putExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST, - new CancelSelectionRequest(requestToken, shouldShowCancellationUi, appPackageName)); + new CancelSelectionRequest(new RequestToken(requestToken), shouldShowCancellationUi, + appPackageName)); return intent; } diff --git a/core/java/android/credentials/selection/RequestInfo.java b/core/java/android/credentials/selection/RequestInfo.java index 60bbae683680..16d0802709f3 100644 --- a/core/java/android/credentials/selection/RequestInfo.java +++ b/core/java/android/credentials/selection/RequestInfo.java @@ -106,7 +106,7 @@ public final class RequestInfo implements Parcelable { private final String mType; @NonNull - private final String mAppPackageName; + private final String mPackageName; private final boolean mHasPermissionToOverrideDefault; @@ -172,8 +172,8 @@ public final class RequestInfo implements Parcelable { /** Returns the display name of the app that made this request. */ @NonNull - public String getAppPackageName() { - return mAppPackageName; + public String getPackageName() { + return mPackageName; } /** @@ -248,7 +248,7 @@ public final class RequestInfo implements Parcelable { boolean isShowAllOptionsRequested) { mToken = token; mType = type; - mAppPackageName = appPackageName; + mPackageName = appPackageName; mCreateCredentialRequest = createCredentialRequest; mGetCredentialRequest = getCredentialRequest; mHasPermissionToOverrideDefault = hasPermissionToOverrideDefault; @@ -270,8 +270,8 @@ public final class RequestInfo implements Parcelable { AnnotationValidations.validate(NonNull.class, null, mToken); mType = type; AnnotationValidations.validate(NonNull.class, null, mType); - mAppPackageName = appPackageName; - AnnotationValidations.validate(NonNull.class, null, mAppPackageName); + mPackageName = appPackageName; + AnnotationValidations.validate(NonNull.class, null, mPackageName); mCreateCredentialRequest = createCredentialRequest; mGetCredentialRequest = getCredentialRequest; mHasPermissionToOverrideDefault = in.readBoolean(); @@ -284,7 +284,7 @@ public final class RequestInfo implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeStrongBinder(mToken); dest.writeString8(mType); - dest.writeString8(mAppPackageName); + dest.writeString8(mPackageName); dest.writeTypedObject(mCreateCredentialRequest, flags); dest.writeTypedObject(mGetCredentialRequest, flags); dest.writeBoolean(mHasPermissionToOverrideDefault); diff --git a/core/java/android/credentials/selection/RequestToken.java b/core/java/android/credentials/selection/RequestToken.java index 27b83f873732..f1953ced6202 100644 --- a/core/java/android/credentials/selection/RequestToken.java +++ b/core/java/android/credentials/selection/RequestToken.java @@ -30,6 +30,11 @@ import android.os.IBinder; * To compare if two requests pertain to the same session, compare their RequestTokens using * the {@link RequestToken#equals(Object)} method. * + * For example, when receiving a {@link android.credentials.selection.CancelSelectionRequest}, + * the developer should use {@link RequestToken#getToken()} to retrieve the token from request and + * compare whether it is equal with the cached token using {@link RequestToken#equals(Object)}. Only + * cancel the request when two tokens are the same. + * * @hide */ @SystemApi @@ -39,6 +44,12 @@ public final class RequestToken { @NonNull private final IBinder mToken; + /** @hide **/ + @NonNull + public IBinder getToken() { + return mToken; + } + /** @hide */ @TestApi @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED) diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index 54e34ecf7006..62473c5c58ce 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -76,6 +76,12 @@ public class InputSettings { */ public static final int MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS = 5000; + /** + * Default value for {@link Settings.Secure#STYLUS_POINTER_ICON_ENABLED}. + * @hide + */ + public static final int DEFAULT_STYLUS_POINTER_ICON_ENABLED = 1; + private InputSettings() { } @@ -383,14 +389,19 @@ public class InputSettings { } /** - * Whether a pointer icon will be shown over the location of a - * stylus pointer. + * Whether a pointer icon will be shown over the location of a stylus pointer. + * * @hide */ public static boolean isStylusPointerIconEnabled(@NonNull Context context) { + if (InputProperties.force_enable_stylus_pointer_icon().orElse(false)) { + return true; + } return context.getResources() - .getBoolean(com.android.internal.R.bool.config_enableStylusPointerIcon) - || InputProperties.force_enable_stylus_pointer_icon().orElse(false); + .getBoolean(com.android.internal.R.bool.config_enableStylusPointerIcon) + && Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.STYLUS_POINTER_ICON_ENABLED, + DEFAULT_STYLUS_POINTER_ICON_ENABLED, UserHandle.USER_CURRENT_OR_SELF) != 0; } /** diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index 08b32bf2b9e0..c9c91fc49aeb 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -33,7 +33,6 @@ import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; -import android.util.Log; import android.util.MathUtils; import com.android.internal.util.Preconditions; @@ -54,7 +53,6 @@ import java.util.StringJoiner; * <p>These effects may be any number of things, from single shot vibrations to complex waveforms. */ public abstract class VibrationEffect implements Parcelable { - private static final String TAG = "VibrationEffect"; // Stevens' coefficient to scale the perceived vibration intensity. private static final float SCALE_GAMMA = 0.65f; // If a vibration is playing for longer than 1s, it's probably not haptic feedback @@ -397,32 +395,26 @@ public abstract class VibrationEffect implements Parcelable { return null; } - try { - final ContentResolver cr = context.getContentResolver(); - Uri uncanonicalUri = cr.uncanonicalize(uri); - if (uncanonicalUri == null) { - // If we already had an uncanonical URI, it's possible we'll get null back here. In - // this case, just use the URI as passed in since it wasn't canonicalized in the - // first place. - uncanonicalUri = uri; - } + final ContentResolver cr = context.getContentResolver(); + Uri uncanonicalUri = cr.uncanonicalize(uri); + if (uncanonicalUri == null) { + // If we already had an uncanonical URI, it's possible we'll get null back here. In + // this case, just use the URI as passed in since it wasn't canonicalized in the first + // place. + uncanonicalUri = uri; + } - for (int i = 0; i < uris.length && i < RINGTONES.length; i++) { - if (uris[i] == null) { - continue; - } - Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i])); - if (mappedUri == null) { - continue; - } - if (mappedUri.equals(uncanonicalUri)) { - return get(RINGTONES[i]); - } + for (int i = 0; i < uris.length && i < RINGTONES.length; i++) { + if (uris[i] == null) { + continue; + } + Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i])); + if (mappedUri == null) { + continue; + } + if (mappedUri.equals(uncanonicalUri)) { + return get(RINGTONES[i]); } - } catch (Exception e) { - // Don't give unexpected exceptions to callers if the Uri's ContentProvider is - // misbehaving - it's very unlikely to be mapped in that case anyway. - Log.e(TAG, "Exception getting default vibration for Uri " + uri, e); } return null; } diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 6c728a4a7288..abfa4e3dd8dc 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -122,3 +122,11 @@ flag { is_fixed_read_only: true bug: "309792384" } + +flag { + namespace: "system_performance" + name: "telemetry_apis_framework_initialization" + description: "Control framework initialization APIs of telemetry APIs feature." + is_fixed_read_only: true + bug: "324241334" +} diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index 437668c9a7de..ea9375ef323c 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -16,13 +16,6 @@ flag { flag { namespace: "haptics" - name: "haptics_customization_ringtone_v2_enabled" - description: "Enables the usage of the new RingtoneV2 class" - bug: "241918098" -} - -flag { - namespace: "haptics" name: "enable_vibration_serialization_apis" description: "Enables the APIs for vibration serialization/deserialization." bug: "245129509" diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ef2d5ebd33fd..ce7a026c368b 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -12312,6 +12312,16 @@ public final class Settings { public static final String PRIVATE_SPACE_AUTO_LOCK = "private_space_auto_lock"; /** + * Toggle for enabling stylus pointer icon. Pointer icons for styluses will only be be shown + * when this is enabled. Enabling this alone won't enable the stylus pointer; + * config_enableStylusPointerIcon needs to be true as well. + * + * @hide + */ + @Readable + public static final String STYLUS_POINTER_ICON_ENABLED = "stylus_pointer_icon_enabled"; + + /** * These entries are considered common between the personal and the managed profile, * since the managed profile doesn't get to change them. */ diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java index 1afe8d95185b..da8817a69449 100644 --- a/core/java/android/service/autofill/Dataset.java +++ b/core/java/android/service/autofill/Dataset.java @@ -26,8 +26,8 @@ import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.ClipData; +import android.content.Intent; import android.content.IntentSender; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; @@ -190,7 +190,7 @@ public final class Dataset implements Parcelable { @Nullable private final InlinePresentation mInlineTooltipPresentation; private final IntentSender mAuthentication; - @Nullable private final Bundle mAuthenticationExtras; + @Nullable private Intent mCredentialFillInIntent; @Nullable String mId; @@ -229,7 +229,7 @@ public final class Dataset implements Parcelable { mInlinePresentation = inlinePresentation; mInlineTooltipPresentation = inlineTooltipPresentation; mAuthentication = authentication; - mAuthenticationExtras = null; + mCredentialFillInIntent = null; mId = id; } @@ -252,7 +252,7 @@ public final class Dataset implements Parcelable { mInlinePresentation = dataset.mInlinePresentation; mInlineTooltipPresentation = dataset.mInlineTooltipPresentation; mAuthentication = dataset.mAuthentication; - mAuthenticationExtras = dataset.mAuthenticationExtras; + mCredentialFillInIntent = dataset.mCredentialFillInIntent; mId = dataset.mId; mAutofillDatatypes = dataset.mAutofillDatatypes; } @@ -271,7 +271,7 @@ public final class Dataset implements Parcelable { mInlinePresentation = builder.mInlinePresentation; mInlineTooltipPresentation = builder.mInlineTooltipPresentation; mAuthentication = builder.mAuthentication; - mAuthenticationExtras = builder.mAuthenticationExtras; + mCredentialFillInIntent = builder.mCredentialFillInIntent; mId = builder.mId; mAutofillDatatypes = builder.mAutofillDatatypes; } @@ -354,8 +354,14 @@ public final class Dataset implements Parcelable { /** @hide */ @Hide - public @Nullable Bundle getAuthenticationExtras() { - return mAuthenticationExtras; + public @Nullable Intent getCredentialFillInIntent() { + return mCredentialFillInIntent; + } + + /** @hide */ + @Hide + public void setCredentialFillInIntent(Intent intent) { + mCredentialFillInIntent = intent; } /** @hide */ @@ -415,7 +421,7 @@ public final class Dataset implements Parcelable { if (mAuthentication != null) { builder.append(", hasAuthentication"); } - if (mAuthenticationExtras != null) { + if (mCredentialFillInIntent != null) { builder.append(", hasAuthenticationExtras"); } if (mAutofillDatatypes != null) { @@ -472,7 +478,7 @@ public final class Dataset implements Parcelable { @Nullable private InlinePresentation mInlineTooltipPresentation; private IntentSender mAuthentication; - private Bundle mAuthenticationExtras; + private Intent mCredentialFillInIntent; private boolean mDestroyed; @Nullable private String mId; @@ -655,9 +661,9 @@ public final class Dataset implements Parcelable { * @hide */ @Hide - public @NonNull Builder setAuthenticationExtras(@Nullable Bundle authenticationExtra) { + public @NonNull Builder setCredentialFillInIntent(@Nullable Intent credentialFillInIntent) { throwIfDestroyed(); - mAuthenticationExtras = authenticationExtra; + mCredentialFillInIntent = credentialFillInIntent; return this; } @@ -1401,7 +1407,7 @@ public final class Dataset implements Parcelable { parcel.writeParcelable(mAuthentication, flags); parcel.writeString(mId); parcel.writeInt(mEligibleReason); - parcel.writeTypedObject(mAuthenticationExtras, flags); + parcel.writeTypedObject(mCredentialFillInIntent, flags); } public static final @NonNull Creator<Dataset> CREATOR = new Creator<Dataset>() { @@ -1437,7 +1443,7 @@ public final class Dataset implements Parcelable { android.content.IntentSender.class); final String datasetId = parcel.readString(); final int eligibleReason = parcel.readInt(); - final Bundle authenticationExtras = parcel.readTypedObject(Bundle.CREATOR); + final Intent credentialFillInIntent = parcel.readTypedObject(Intent.CREATOR); // Always go through the builder to ensure the data ingested by // the system obeys the contract of the builder to avoid attacks @@ -1482,7 +1488,7 @@ public final class Dataset implements Parcelable { fieldDialogPresentation); } builder.setAuthentication(authentication); - builder.setAuthenticationExtras(authenticationExtras); + builder.setCredentialFillInIntent(credentialFillInIntent); builder.setId(datasetId); Dataset dataset = builder.build(); dataset.mEligibleReason = eligibleReason; diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index 09ec933880d4..c43ba6c16201 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -56,6 +56,7 @@ import java.util.Set; * <p>See the main {@link AutofillService} documentation for more details and examples. */ public final class FillResponse implements Parcelable { + // common_typos_disable /** * Flag used to generate {@link FillEventHistory.Event events} of type @@ -82,11 +83,17 @@ public final class FillResponse implements Parcelable { */ public static final int FLAG_DELAY_FILL = 0x4; + /** + * @hide + */ + public static final int FLAG_CREDENTIAL_MANAGER_RESPONSE = 0x8; + /** @hide */ @IntDef(flag = true, prefix = { "FLAG_" }, value = { FLAG_TRACK_CONTEXT_COMMITED, FLAG_DISABLE_ACTIVITY_ONLY, - FLAG_DELAY_FILL + FLAG_DELAY_FILL, + FLAG_CREDENTIAL_MANAGER_RESPONSE }) @Retention(RetentionPolicy.SOURCE) @interface FillResponseFlags {} @@ -834,7 +841,9 @@ public final class FillResponse implements Parcelable { public Builder setFlags(@FillResponseFlags int flags) { throwIfDestroyed(); mFlags = Preconditions.checkFlagsArgument(flags, - FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY | FLAG_DELAY_FILL); + FLAG_TRACK_CONTEXT_COMMITED + | FLAG_DISABLE_ACTIVITY_ONLY | FLAG_DELAY_FILL + | FLAG_CREDENTIAL_MANAGER_RESPONSE); return this; } diff --git a/core/java/android/service/voice/OWNERS b/core/java/android/service/voice/OWNERS index ec4410086edf..763c79e20846 100644 --- a/core/java/android/service/voice/OWNERS +++ b/core/java/android/service/voice/OWNERS @@ -4,4 +4,4 @@ include /core/java/android/app/assist/OWNERS # The owner here should not be assist owner liangyuchen@google.com -tuanng@google.com +adudani@google.com diff --git a/core/java/android/service/wearable/IWearableSensingService.aidl b/core/java/android/service/wearable/IWearableSensingService.aidl index 44a13c4fb9e5..8fdb2c20e719 100644 --- a/core/java/android/service/wearable/IWearableSensingService.aidl +++ b/core/java/android/service/wearable/IWearableSensingService.aidl @@ -28,6 +28,7 @@ import android.os.SharedMemory; * @hide */ oneway interface IWearableSensingService { + void provideSecureWearableConnection(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback); void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback); void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback); void startDetection(in AmbientContextEventRequest request, in String packageName, diff --git a/core/java/android/service/wearable/WearableSensingService.java b/core/java/android/service/wearable/WearableSensingService.java index e7e44a54cbae..d21115b5d622 100644 --- a/core/java/android/service/wearable/WearableSensingService.java +++ b/core/java/android/service/wearable/WearableSensingService.java @@ -17,12 +17,14 @@ package android.service.wearable; import android.annotation.BinderThread; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.Service; import android.app.ambientcontext.AmbientContextEvent; import android.app.ambientcontext.AmbientContextEventRequest; +import android.app.wearable.Flags; import android.app.wearable.WearableSensingManager; import android.content.Intent; import android.os.Bundle; @@ -39,6 +41,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -94,17 +97,20 @@ public abstract class WearableSensingService extends Service { return new IWearableSensingService.Stub() { /** {@inheritDoc} */ @Override + public void provideSecureWearableConnection( + ParcelFileDescriptor secureWearableConnection, RemoteCallback callback) { + Objects.requireNonNull(secureWearableConnection); + Consumer<Integer> consumer = createWearableStatusConsumer(callback); + WearableSensingService.this.onSecureWearableConnectionProvided( + secureWearableConnection, consumer); + } + + /** {@inheritDoc} */ + @Override public void provideDataStream( - ParcelFileDescriptor parcelFileDescriptor, - RemoteCallback callback) { + ParcelFileDescriptor parcelFileDescriptor, RemoteCallback callback) { Objects.requireNonNull(parcelFileDescriptor); - Consumer<Integer> consumer = response -> { - Bundle bundle = new Bundle(); - bundle.putInt( - STATUS_RESPONSE_BUNDLE_KEY, - response); - callback.sendResult(bundle); - }; + Consumer<Integer> consumer = createWearableStatusConsumer(callback); WearableSensingService.this.onDataStreamProvided( parcelFileDescriptor, consumer); } @@ -116,38 +122,38 @@ public abstract class WearableSensingService extends Service { SharedMemory sharedMemory, RemoteCallback callback) { Objects.requireNonNull(data); - Consumer<Integer> consumer = response -> { - Bundle bundle = new Bundle(); - bundle.putInt( - STATUS_RESPONSE_BUNDLE_KEY, - response); - callback.sendResult(bundle); - }; + Consumer<Integer> consumer = createWearableStatusConsumer(callback); WearableSensingService.this.onDataProvided(data, sharedMemory, consumer); } /** {@inheritDoc} */ @Override - public void startDetection(@NonNull AmbientContextEventRequest request, - String packageName, RemoteCallback detectionResultCallback, + public void startDetection( + @NonNull AmbientContextEventRequest request, + String packageName, + RemoteCallback detectionResultCallback, RemoteCallback statusCallback) { Objects.requireNonNull(request); Objects.requireNonNull(packageName); Objects.requireNonNull(detectionResultCallback); Objects.requireNonNull(statusCallback); - Consumer<AmbientContextDetectionResult> detectionResultConsumer = result -> { - Bundle bundle = new Bundle(); - bundle.putParcelable( - AmbientContextDetectionResult.RESULT_RESPONSE_BUNDLE_KEY, result); - detectionResultCallback.sendResult(bundle); - }; - Consumer<AmbientContextDetectionServiceStatus> statusConsumer = status -> { - Bundle bundle = new Bundle(); - bundle.putParcelable( - AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY, - status); - statusCallback.sendResult(bundle); - }; + Consumer<AmbientContextDetectionResult> detectionResultConsumer = + result -> { + Bundle bundle = new Bundle(); + bundle.putParcelable( + AmbientContextDetectionResult.RESULT_RESPONSE_BUNDLE_KEY, + result); + detectionResultCallback.sendResult(bundle); + }; + Consumer<AmbientContextDetectionServiceStatus> statusConsumer = + status -> { + Bundle bundle = new Bundle(); + bundle.putParcelable( + AmbientContextDetectionServiceStatus + .STATUS_RESPONSE_BUNDLE_KEY, + status); + statusCallback.sendResult(bundle); + }; WearableSensingService.this.onStartDetection( request, packageName, statusConsumer, detectionResultConsumer); Slog.d(TAG, "startDetection " + request); @@ -162,23 +168,26 @@ public abstract class WearableSensingService extends Service { /** {@inheritDoc} */ @Override - public void queryServiceStatus(@AmbientContextEvent.EventCode int[] eventTypes, - String packageName, RemoteCallback callback) { + public void queryServiceStatus( + @AmbientContextEvent.EventCode int[] eventTypes, + String packageName, + RemoteCallback callback) { Objects.requireNonNull(eventTypes); Objects.requireNonNull(packageName); Objects.requireNonNull(callback); - Consumer<AmbientContextDetectionServiceStatus> consumer = response -> { - Bundle bundle = new Bundle(); - bundle.putParcelable( - AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY, - response); - callback.sendResult(bundle); - }; + Consumer<AmbientContextDetectionServiceStatus> consumer = + response -> { + Bundle bundle = new Bundle(); + bundle.putParcelable( + AmbientContextDetectionServiceStatus + .STATUS_RESPONSE_BUNDLE_KEY, + response); + callback.sendResult(bundle); + }; Integer[] events = intArrayToIntegerArray(eventTypes); WearableSensingService.this.onQueryServiceStatus( new HashSet<>(Arrays.asList(events)), packageName, consumer); } - }; } Slog.w(TAG, "Incorrect service interface, returning null."); @@ -186,6 +195,30 @@ public abstract class WearableSensingService extends Service { } /** + * Called when a secure connection to the wearable is available. See {@link + * WearableSensingManager#provideWearableConnection(ParcelFileDescriptor, Executor, Consumer)} + * for details about the secure connection. + * + * <p>When the {@code secureWearableConnection} is closed, the system will send a {@link + * WearableSensingManager#STATUS_CHANNEL_ERROR} status code to the status consumer provided by + * the caller of {@link WearableSensingManager#provideWearableConnection(ParcelFileDescriptor, + * Executor, Consumer)}. + * + * <p>The implementing class should override this method. It should return an appropriate status + * code via {@code statusConsumer} after receiving the {@code secureWearableConnection}. + * + * @param secureWearableConnection The secure connection to the wearable. + * @param statusConsumer The consumer for the service status. + */ + @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API) + @BinderThread + public void onSecureWearableConnectionProvided( + @NonNull ParcelFileDescriptor secureWearableConnection, + @NonNull Consumer<Integer> statusConsumer) { + statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION); + } + + /** * Called when a data stream to the wearable is provided. This data stream can be used to obtain * data from a wearable device. It is up to the implementation to maintain the data stream and * close the data stream when it is finished. @@ -275,4 +308,13 @@ public abstract class WearableSensingService extends Service { } return intArray; } + + @NonNull + private static Consumer<Integer> createWearableStatusConsumer(RemoteCallback statusCallback) { + return response -> { + Bundle bundle = new Bundle(); + bundle.putInt(STATUS_RESPONSE_BUNDLE_KEY, response); + statusCallback.sendResult(bundle); + }; + } } diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 7bae7ec6559c..4ba4ee3ffff7 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -1497,13 +1497,21 @@ public abstract class Window { * {@link View#SYSTEM_UI_LAYOUT_FLAGS} as well the * {@link WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE} flag and fits content according * to these flags. - * </p> + * * <p> * If set to {@code false}, the framework will not fit the content view to the insets and will * just pass through the {@link WindowInsets} to the content view. - * </p> + * + * <p> + * If the app targets + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above, + * the behavior will be like setting this to {@code false}, and cannot be changed. + * * @param decorFitsSystemWindows Whether the decor view should fit root-level content views for * insets. + * @deprecated Make space in the container views to prevent the critical elements from getting + * obscured by {@link WindowInsets.Type#systemBars()} or + * {@link WindowInsets.Type#displayCutout()} instead. */ public void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) { } @@ -2597,7 +2605,9 @@ public abstract class Window { /** * @return the color of the status bar. + * @deprecated This is no longer needed since the setter is deprecated. */ + @Deprecated @ColorInt public abstract int getStatusBarColor(); @@ -2614,13 +2624,29 @@ public abstract class Window { * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}. * <p> * The transitionName for the view background will be "android:status:background". - * </p> + * + * <p> + * If the color is transparent and the window enforces the status bar contrast, the system + * will determine whether a scrim is necessary and draw one on behalf of the app to ensure + * that the status bar has enough contrast with the contents of this app, and set an appropriate + * effective bar background accordingly. + * + * <p> + * If the app targets + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above, + * the color will be transparent and cannot be changed. + * + * @see #setNavigationBarContrastEnforced + * @deprecated Draw proper background behind {@link WindowInsets.Type#statusBars()}} instead. */ + @Deprecated public abstract void setStatusBarColor(@ColorInt int color); /** * @return the color of the navigation bar. + * @deprecated This is no longer needed since the setter is deprecated. */ + @Deprecated @ColorInt public abstract int getNavigationBarColor(); @@ -2637,9 +2663,24 @@ public abstract class Window { * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}. * <p> * The transitionName for the view background will be "android:navigation:background". - * </p> + * + * <p> + * If the color is transparent and the window enforces the navigation bar contrast, the system + * will determine whether a scrim is necessary and draw one on behalf of the app to ensure that + * the navigation bar has enough contrast with the contents of this app, and set an appropriate + * effective bar background accordingly. + * + * <p> + * If the app targets + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above, + * the color will be transparent and cannot be changed. + * * @attr ref android.R.styleable#Window_navigationBarColor + * @see #setNavigationBarContrastEnforced + * @deprecated Draw proper background behind {@link WindowInsets.Type#navigationBars()} or + * {@link WindowInsets.Type#tappableElement()} instead. */ + @Deprecated public abstract void setNavigationBarColor(@ColorInt int color); /** @@ -2651,9 +2692,17 @@ public abstract class Window { * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set. * + * <p> + * If the app targets + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above, + * the color will be transparent and cannot be changed. + * * @param dividerColor The color of the thin line. * @attr ref android.R.styleable#Window_navigationBarDividerColor + * @deprecated Draw proper background behind {@link WindowInsets.Type#navigationBars()} or + * {@link WindowInsets.Type#tappableElement()} instead. */ + @Deprecated public void setNavigationBarDividerColor(@ColorInt int dividerColor) { } @@ -2663,7 +2712,9 @@ public abstract class Window { * @return The color of the navigation bar divider color. * @see #setNavigationBarColor(int) * @attr ref android.R.styleable#Window_navigationBarDividerColor + * @deprecated This is no longer needed since the setter is deprecated. */ + @Deprecated public @ColorInt int getNavigationBarDividerColor() { return 0; } @@ -2682,7 +2733,9 @@ public abstract class Window { * @see android.R.attr#enforceStatusBarContrast * @see #isStatusBarContrastEnforced * @see #setStatusBarColor + * @deprecated Draw proper background behind {@link WindowInsets.Type#statusBars()}} instead. */ + @Deprecated public void setStatusBarContrastEnforced(boolean ensureContrast) { } @@ -2697,7 +2750,9 @@ public abstract class Window { * @see android.R.attr#enforceStatusBarContrast * @see #setStatusBarContrastEnforced * @see #setStatusBarColor + * @deprecated This is not needed since the setter is deprecated. */ + @Deprecated public boolean isStatusBarContrastEnforced() { return false; } diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 559ccfea72c2..7ebabee46a74 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -64,6 +64,7 @@ import android.service.autofill.AutofillService; import android.service.autofill.FillEventHistory; import android.service.autofill.Flags; import android.service.autofill.UserData; +import android.service.credentials.CredentialProviderService; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -2382,7 +2383,18 @@ public final class AutofillManager { return; } - final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT); + final Parcelable result; + if (data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT) != null) { + result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT); + } else if (data.getParcelableExtra( + CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE) != null + && Flags.autofillCredmanIntegration()) { + result = data.getParcelableExtra( + CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE); + } else { + result = null; + } + final Bundle responseData = new Bundle(); responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result); final Bundle newClientState = data.getBundleExtra(EXTRA_CLIENT_STATE); diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig index 1dd99baf8d2a..6a4408bdd391 100644 --- a/core/java/android/view/flags/view_flags.aconfig +++ b/core/java/android/view/flags/view_flags.aconfig @@ -1,13 +1,6 @@ package: "android.view.flags" flag { - name: "enable_surface_native_alloc_registration" - namespace: "toolkit" - description: "Feature flag for registering surfaces with the VM for faster cleanup" - bug: "306193257" -} - -flag { name: "enable_surface_native_alloc_registration_ro" namespace: "toolkit" description: "Feature flag for registering surfaces with the VM for faster" diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index b4f9ee3b0b74..523924566dd7 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -120,7 +120,6 @@ import android.window.OnBackInvokedDispatcher; import android.window.ProxyOnBackInvokedDispatcher; import com.android.internal.R; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.view.menu.ContextMenuBuilder; import com.android.internal.view.menu.IconMenuPresenter; import com.android.internal.view.menu.ListMenuPresenter; @@ -369,8 +368,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { boolean mDecorFitsSystemWindows = true; - @VisibleForTesting - public final boolean mEdgeToEdgeEnforced; + private boolean mEdgeToEdgeEnforced; private final ProxyOnBackInvokedDispatcher mProxyOnBackInvokedDispatcher; @@ -390,11 +388,6 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context); mAllowFloatingWindowsFillScreen = context.getResources().getBoolean( com.android.internal.R.bool.config_allowFloatingWindowsFillScreen); - mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(context.getApplicationInfo(), true /* local */); - if (mEdgeToEdgeEnforced) { - getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED; - mDecorFitsSystemWindows = false; - } } /** @@ -436,15 +429,18 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { * * @param info The application to query. * @param local Whether this is called from the process of the given application. + * @param windowStyle The style of the window. * @return {@code true} if edge-to-edge is enforced. Otherwise, {@code false}. */ - public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local) { - return info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION - || (Flags.enforceEdgeToEdge() && (local - // Calling this doesn't require a permission. - ? CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE) - // Calling this requires permissions. - : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE))); + public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local, + TypedArray windowStyle) { + return !windowStyle.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false) + && (info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION + || (Flags.enforceEdgeToEdge() && (local + // Calling this doesn't require a permission. + ? CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE) + // Calling this requires permissions. + : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)))); } @Override @@ -2470,6 +2466,13 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { System.out.println(s); } + mEdgeToEdgeEnforced = isEdgeToEdgeEnforced( + getContext().getApplicationInfo(), true /* local */, a); + if (mEdgeToEdgeEnforced) { + getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED; + mDecorFitsSystemWindows = false; + } + mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false); int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) & (~getForcedWindowFlags()); diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index a8d0d37f78bd..889434f40472 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -168,12 +168,12 @@ public class ConversationLayout extends FrameLayout } public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, - @AttrRes int defStyleAttr) { + @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); } public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, - @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @@ -432,8 +432,14 @@ public class ConversationLayout extends FrameLayout final List<MessagingMessage> newHistoricMessagingMessages = createMessages(newHistoricMessages, /* isHistoric= */true, usePrecomputedText); + // Add our new MessagingMessages to groups + List<List<MessagingMessage>> groups = new ArrayList<>(); + List<Person> senders = new ArrayList<>(); + // Lets first find the groups (populate `groups` and `senders`) + findGroups(newHistoricMessagingMessages, newMessagingMessages, user, groups, senders); + return new MessagingData(user, showSpinner, unreadCount, - newHistoricMessagingMessages, newMessagingMessages); + newHistoricMessagingMessages, newMessagingMessages, groups, senders); } /** @@ -509,21 +515,13 @@ public class ConversationLayout extends FrameLayout setUser(messagingData.getUser()); setUnreadCount(messagingData.getUnreadCount()); - List<MessagingMessage> messages = messagingData.getNewMessagingMessages(); - List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages(); // Copy our groups, before they get clobbered ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); - // Add our new MessagingMessages to groups - List<List<MessagingMessage>> groups = new ArrayList<>(); - List<Person> senders = new ArrayList<>(); - - // Lets first find the groups (populate `groups` and `senders`) - findGroups(historicMessages, messages, groups, senders); - // Let's now create the views and reorder them accordingly // side-effect: updates mGroups, mAddedGroups - createGroupViews(groups, senders, messagingData.getShowSpinner()); + createGroupViews(messagingData.getGroups(), messagingData.getSenders(), + messagingData.getShowSpinner()); // Let's first check which groups were removed altogether and remove them in one animation removeGroups(oldGroups); @@ -536,8 +534,8 @@ public class ConversationLayout extends FrameLayout historicMessage.removeMessage(mToRecycle); } - mMessages = messages; - mHistoricMessages = historicMessages; + mMessages = messagingData.getNewMessagingMessages(); + mHistoricMessages = messagingData.getHistoricMessagingMessages(); updateHistoricMessageVisibility(); updateTitleAndNamesDisplay(); @@ -935,7 +933,7 @@ public class ConversationLayout extends FrameLayout } private void createGroupViews(List<List<MessagingMessage>> groups, - List<Person> senders, boolean showSpinner) { + List<Person> senders, boolean showSpinner) { mGroups.clear(); for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { List<MessagingMessage> group = groups.get(groupIndex); @@ -983,9 +981,12 @@ public class ConversationLayout extends FrameLayout } } + /** + * Finds groups and senders from the given messaging messages and fills outGroups and outSenders + */ private void findGroups(List<MessagingMessage> historicMessages, - List<MessagingMessage> messages, List<List<MessagingMessage>> groups, - List<Person> senders) { + List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> outGroups, + List<Person> outSenders) { CharSequence currentSenderKey = null; List<MessagingMessage> currentGroup = null; int histSize = historicMessages.size(); @@ -1003,14 +1004,14 @@ public class ConversationLayout extends FrameLayout isNewGroup |= !TextUtils.equals(key, currentSenderKey); if (isNewGroup) { currentGroup = new ArrayList<>(); - groups.add(currentGroup); + outGroups.add(currentGroup); if (sender == null) { - sender = mUser; + sender = user; } else { // Remove all formatting from the sender name sender = sender.toBuilder().setName(Objects.toString(sender.getName())).build(); } - senders.add(sender); + outSenders.add(sender); currentSenderKey = key; } currentGroup.add(message); diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java index 5cda3f2b2bc0..3e065bf9f450 100644 --- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java +++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java @@ -16,8 +16,8 @@ package com.android.internal.widget; +import static android.app.Flags.evenlyDividedCallStyleActionLayout; import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT; -import static android.app.Notification.CallStyle.USE_NEW_ACTION_LAYOUT; import static android.text.style.DynamicDrawableSpan.ALIGN_CENTER; import android.annotation.NonNull; @@ -166,7 +166,7 @@ public class EmphasizedNotificationButton extends Button { } private void setIconToGlue(@Nullable Drawable icon) { - if (!USE_NEW_ACTION_LAYOUT) { + if (!evenlyDividedCallStyleActionLayout()) { Log.e(TAG, "glueIcon: new action layout disabled; doing nothing"); return; } @@ -207,7 +207,7 @@ public class EmphasizedNotificationButton extends Button { } private void setLabelToGlue(@Nullable CharSequence label) { - if (!USE_NEW_ACTION_LAYOUT) { + if (!evenlyDividedCallStyleActionLayout()) { Log.e(TAG, "glueLabel: new action layout disabled; doing nothing"); return; } @@ -255,7 +255,7 @@ public class EmphasizedNotificationButton extends Button { return; } - if (!USE_NEW_ACTION_LAYOUT) { + if (!evenlyDividedCallStyleActionLayout()) { Log.e(TAG, "glueIconAndLabelIfNeeded: new action layout disabled; doing nothing"); return; } diff --git a/core/java/com/android/internal/widget/MessagingData.java b/core/java/com/android/internal/widget/MessagingData.java index 85b02018e7c7..42de60e08e18 100644 --- a/core/java/com/android/internal/widget/MessagingData.java +++ b/core/java/com/android/internal/widget/MessagingData.java @@ -28,24 +28,33 @@ final class MessagingData { private final boolean mShowSpinner; private final List<MessagingMessage> mHistoricMessagingMessages; private final List<MessagingMessage> mNewMessagingMessages; + private final List<List<MessagingMessage>> mGroups; + private final List<Person> mSenders; private final int mUnreadCount; MessagingData(Person user, boolean showSpinner, List<MessagingMessage> historicMessagingMessages, - List<MessagingMessage> newMessagingMessages) { + List<MessagingMessage> newMessagingMessages, List<List<MessagingMessage>> groups, + List<Person> senders) { this(user, showSpinner, /* unreadCount= */0, - historicMessagingMessages, newMessagingMessages); + historicMessagingMessages, newMessagingMessages, + groups, + senders); } MessagingData(Person user, boolean showSpinner, int unreadCount, List<MessagingMessage> historicMessagingMessages, - List<MessagingMessage> newMessagingMessages) { + List<MessagingMessage> newMessagingMessages, + List<List<MessagingMessage>> groups, + List<Person> senders) { mUser = user; mShowSpinner = showSpinner; mUnreadCount = unreadCount; mHistoricMessagingMessages = historicMessagingMessages; mNewMessagingMessages = newMessagingMessages; + mGroups = groups; + mSenders = senders; } public Person getUser() { @@ -67,4 +76,12 @@ final class MessagingData { public int getUnreadCount() { return mUnreadCount; } + + public List<Person> getSenders() { + return mSenders; + } + + public List<List<MessagingMessage>> getGroups() { + return mGroups; + } } diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java index b6d7503119fe..d000596390ec 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -189,9 +189,15 @@ public class MessagingLayout extends FrameLayout /* isHistoric= */true, usePrecomputedText); final List<MessagingMessage> newMessagingMessages = createMessages(newMessages, /* isHistoric */false, usePrecomputedText); + // Let's first find our groups! + List<List<MessagingMessage>> groups = new ArrayList<>(); + List<Person> senders = new ArrayList<>(); + + // Lets first find the groups + findGroups(historicMessagingMessages, newMessagingMessages, groups, senders); return new MessagingData(user, showSpinner, - historicMessagingMessages, newMessagingMessages); + historicMessagingMessages, newMessagingMessages, groups, senders); } /** @@ -256,10 +262,10 @@ public class MessagingLayout extends FrameLayout private void bind(MessagingData messagingData) { setUser(messagingData.getUser()); - List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages(); - List<MessagingMessage> messages = messagingData.getNewMessagingMessages(); + // Let's now create the views and reorder them accordingly ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); - addMessagesToGroups(historicMessages, messages, messagingData.getShowSpinner()); + createGroupViews(messagingData.getGroups(), messagingData.getSenders(), + messagingData.getShowSpinner()); // Let's first check which groups were removed altogether and remove them in one animation removeGroups(oldGroups); @@ -272,8 +278,8 @@ public class MessagingLayout extends FrameLayout historicMessage.removeMessage(mToRecycle); } - mMessages = messages; - mHistoricMessages = historicMessages; + mMessages = messagingData.getNewMessagingMessages(); + mHistoricMessages = messagingData.getHistoricMessagingMessages(); updateHistoricMessageVisibility(); updateTitleAndNamesDisplay(); @@ -451,19 +457,6 @@ public class MessagingLayout extends FrameLayout } } - private void addMessagesToGroups(List<MessagingMessage> historicMessages, - List<MessagingMessage> messages, boolean showSpinner) { - // Let's first find our groups! - List<List<MessagingMessage>> groups = new ArrayList<>(); - List<Person> senders = new ArrayList<>(); - - // Lets first find the groups - findGroups(historicMessages, messages, groups, senders); - - // Let's now create the views and reorder them accordingly - createGroupViews(groups, senders, showSpinner); - } - private void createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner) { mGroups.clear(); diff --git a/core/java/com/android/internal/widget/NotificationActionListLayout.java b/core/java/com/android/internal/widget/NotificationActionListLayout.java index 69d254499ef4..301dc392c125 100644 --- a/core/java/com/android/internal/widget/NotificationActionListLayout.java +++ b/core/java/com/android/internal/widget/NotificationActionListLayout.java @@ -17,7 +17,7 @@ package com.android.internal.widget; import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT; -import static android.app.Notification.CallStyle.USE_NEW_ACTION_LAYOUT; +import static android.app.Flags.evenlyDividedCallStyleActionLayout; import android.annotation.DimenRes; import android.app.Notification; @@ -410,7 +410,7 @@ public class NotificationActionListLayout extends LinearLayout { */ @RemotableViewMethod public void setEvenlyDividedMode(boolean evenlyDividedMode) { - if (evenlyDividedMode && !USE_NEW_ACTION_LAYOUT) { + if (evenlyDividedMode && !evenlyDividedCallStyleActionLayout()) { Log.e(TAG, "setEvenlyDividedMode(true) called with new action layout disabled; " + "leaving evenly divided mode disabled"); return; diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 104c023f550d..10f75d08ee84 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -612,6 +612,7 @@ message SecureSettingsProto { } optional Sounds sounds = 72; + optional SettingProto stylus_pointer_icon_enabled = 99 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto swipe_bottom_to_notification_enabled = 82 [ (android.privacy).dest = DEST_AUTOMATIC ]; // Defines whether managed profile ringtones should be synced from its // parent profile. @@ -720,5 +721,5 @@ message SecureSettingsProto { // Please insert fields in alphabetical order and group them into messages // if possible (to avoid reaching the method limit). - // Next tag = 99; + // Next tag = 100; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c71a8420ae88..5c764e2f0e7d 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3216,6 +3216,13 @@ android:description="@string/permdesc_accessHiddenProfile" android:protectionLevel="normal" /> + <!-- @SystemApi @hide Allows privileged applications to get details about hidden profile + users. + @FlaggedApi("android.multiuser.flags.enable_permission_to_access_hidden_profiles") --> + <permission + android:name="android.permission.ACCESS_HIDDEN_PROFILES_FULL" + android:protectionLevel="signature|privileged" /> + <!-- @SystemApi @hide Allows starting activities across profiles in the same profile group. --> <permission android:name="android.permission.START_CROSS_PROFILE_ACTIVITIES" android:protectionLevel="signature|role" /> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index e7b1d09d6ab0..908eeeb9d7fd 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2292,7 +2292,17 @@ {@link android.R.attr#windowDrawsSystemBarBackgrounds} and the status bar must not have been requested to be translucent with {@link android.R.attr#windowTranslucentStatus}. - Corresponds to {@link android.view.Window#setStatusBarColor(int)}. --> + Corresponds to {@link android.view.Window#setStatusBarColor(int)}. + <p>If the color is transparent and the window enforces the status bar contrast, the + system will determine whether a scrim is necessary and draw one on behalf of the app to + ensure that the status bar has enough contrast with the contents of this app, and set + an appropriate effective bar background accordingly. + See: {@link android.R.attr#enforceStatusBarContrast} + <p>If the app targets + {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above, + this attribute is ignored. + @deprecated Draw proper background behind + {@link android.view.WindowInsets.Type#statusBars()}} instead. --> <attr name="statusBarColor" format="color" /> <!-- The color for the navigation bar. If the color is not opaque, consider setting @@ -2302,7 +2312,18 @@ {@link android.R.attr#windowDrawsSystemBarBackgrounds} and the navigation bar must not have been requested to be translucent with {@link android.R.attr#windowTranslucentNavigation}. - Corresponds to {@link android.view.Window#setNavigationBarColor(int)}. --> + Corresponds to {@link android.view.Window#setNavigationBarColor(int)}. + <p>If the color is transparent and the window enforces the navigation bar contrast, the + system will determine whether a scrim is necessary and draw one on behalf of the app to + ensure that the navigation bar has enough contrast with the contents of this app, and + set an appropriate effective bar background accordingly. + See: {@link android.R.attr#enforceNavigationBarContrast} + <p>If the app targets + {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above, + this attribute is ignored. + @deprecated Draw proper background behind + {@link android.view.WindowInsets.Type#navigationBars()} or + {@link android.view.WindowInsets.Type#tappableElement()} instead. --> <attr name="navigationBarColor" format="color" /> <!-- Shows a thin line of the specified color between the navigation bar and the app @@ -2311,7 +2332,13 @@ {@link android.R.attr#windowDrawsSystemBarBackgrounds} and the navigation bar must not have been requested to be translucent with {@link android.R.attr#windowTranslucentNavigation}. - Corresponds to {@link android.view.Window#setNavigationBarDividerColor(int)}. --> + Corresponds to {@link android.view.Window#setNavigationBarDividerColor(int)}. + <p>If the app targets + {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above, + this attribute is ignored. + @deprecated Draw proper background behind + {@link android.view.WindowInsets.Type#navigationBars()} or + {@link android.view.WindowInsets.Type#tappableElement()} instead. --> <attr name="navigationBarDividerColor" format="color" /> <!-- Sets whether the system should ensure that the status bar has enough @@ -2327,7 +2354,9 @@ <p>If the app does not target at least {@link android.os.Build.VERSION_CODES#Q Q}, this attribute is ignored. - @see android.view.Window#setStatusBarContrastEnforced --> + @see android.view.Window#setStatusBarContrastEnforced + @deprecated Draw proper background behind + {@link android.view.WindowInsets.Type#statusBars()}} instead. --> <attr name="enforceStatusBarContrast" format="boolean" /> <!-- Sets whether the system should ensure that the navigation bar has enough @@ -2483,6 +2512,31 @@ <!-- The icon is shown unless the launching app specified SPLASH_SCREEN_STYLE_EMPTY --> <enum name="icon_preferred" value="1" /> </attr> + + <!-- Flag indicating whether this window would opt-out the edge-to-edge enforcement. + + <p>If this is false, the edge-to-edge enforcement will be applied to the window if its + app targets + {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above. + The affected behaviors are: + <ul> + <li>The framework will not fit the content view to the insets and will just pass + through the {@link android.view.WindowInsets} to the content view, as if calling + {@link android.view.Window#setDecorFitsSystemWindows(boolean)} with false. + <li>{@link android.view.WindowManager.LayoutParams#layoutInDisplayCutoutMode} of + the non-floating windows will be set to {@link + android.view.WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS}. + Changing it to other values will cause {@link lang.IllegalArgumentException}. + <li>The framework will set {@link android.R.attr#statusBarColor}, + {@link android.R.attr#navigationBarColor}, and + {@link android.R.attr#navigationBarDividerColor} to transparent. + </ul> + + <p>If this is true, the edge-to-edge enforcement won't be applied. However, this + attribute will be deprecated and disabled in a future SDK level. + + <p>This is false by default. --> + <attr name="windowOptOutEdgeToEdgeEnforcement" format="boolean"/> </declare-styleable> <!-- The set of attributes that describe a AlertDialog's theme. --> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 81a8908b37df..4799c3769b0f 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -147,6 +147,8 @@ <public name="useBoundsForWidth"/> <!-- @FlaggedApi("android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP") --> <public name="autoTransact"/> + <!-- @FlaggedApi("com.android.window.flags.enforce_edge_to_edge") --> + <public name="windowOptOutEdgeToEdgeEnforcement"/> </staging-public-group> <staging-public-group type="id" first-id="0x01bc0000"> diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 9bb249999d99..61e6a36839ff 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -127,7 +127,7 @@ <!-- France: 5 digits, free: 3xxxx, premium [4-8]xxxx, plus EU: http://clients.txtnation.com/entries/161972-france-premium-sms-short-code-requirements, visual voicemail code for Orange: 21101 --> - <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051" /> + <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051|33033" /> <!-- United Kingdom (Great Britain): 4-6 digits, common codes [5-8]xxxx, plus EU: http://www.short-codes.com/media/Co-regulatoryCodeofPracticeforcommonshortcodes170206.pdf, @@ -150,6 +150,9 @@ http://clients.txtnation.com/entries/209633-hungary-premium-sms-short-code-regulations --> <shortcode country="hu" pattern="[01](?:\\d{3}|\\d{9})" premium="0691227910|1784" free="116\\d{3}" /> + <!-- Honduras --> + <shortcode country="hn" pattern="\\d{4,6}" free="466453" /> + <!-- India: 1-5 digits (standard system default, not country specific) --> <shortcode country="in" pattern="\\d{1,5}" free="59336|53969" /> @@ -171,7 +174,7 @@ <shortcode country="jp" pattern="\\d{1,5}" free="8083" /> <!-- Kenya: 5 digits, known premium codes listed --> - <shortcode country="ke" pattern="\\d{5}" free="21725|21562|40520|23342|40023" /> + <shortcode country="ke" pattern="\\d{5}" free="21725|21562|40520|23342|40023|24088|23054" /> <!-- Kyrgyzstan: 4 digits, known premium codes listed --> <shortcode country="kg" pattern="\\d{4}" premium="415[2367]|444[69]" /> @@ -183,7 +186,7 @@ <shortcode country="kz" pattern="\\d{4}" premium="335[02]|4161|444[469]|77[2359]0|8444|919[3-5]|968[2-5]" /> <!-- Kuwait: 1-5 digits (standard system default, not country specific) --> - <shortcode country="kw" pattern="\\d{1,5}" free="1378|50420|94006|55991" /> + <shortcode country="kw" pattern="\\d{1,5}" free="1378|50420|94006|55991|50976" /> <!-- Lithuania: 3-5 digits, known premium codes listed, plus EU --> <shortcode country="lt" pattern="\\d{3,5}" premium="13[89]1|1394|16[34]5" free="116\\d{3}|1399|1324" /> @@ -195,9 +198,18 @@ <!-- Latvia: 4 digits, known premium codes listed, plus EU --> <shortcode country="lv" pattern="\\d{4}" premium="18(?:19|63|7[1-4])" free="116\\d{3}|1399" /> + <!-- Morocco: 1-5 digits (standard system default, not country specific) --> + <shortcode country="ma" pattern="\\d{1,5}" free="53819" /> + <!-- Macedonia: 1-6 digits (not confirmed), known premium codes listed --> <shortcode country="mk" pattern="\\d{1,6}" free="129005|122" /> + <!-- Malawi: 1-5 digits (standard system default, not country specific) --> + <shortcode country="mw" pattern="\\d{1,5}" free="4276" /> + + <!-- Mozambique: 1-5 digits (standard system default, not country specific) --> + <shortcode country="mz" pattern="\\d{1,5}" free="1714" /> + <!-- Mexico: 4-5 digits (not confirmed), known premium codes listed --> <shortcode country="mx" pattern="\\d{4,6}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101|45453|550346" /> @@ -207,6 +219,9 @@ <!-- Namibia: 1-5 digits (standard system default, not country specific) --> <shortcode country="na" pattern="\\d{1,5}" free="40005" /> + <!-- Nicaragua --> + <shortcode country="ni" pattern="\\d{4,6}" free="466453" /> + <!-- The Netherlands, 4 digits, known premium codes listed, plus EU --> <shortcode country="nl" pattern="\\d{4}" premium="4466|5040" free="116\\d{3}|2223|6225|2223|1662" /> @@ -219,8 +234,8 @@ <!-- New Zealand: 3-4 digits, known premium codes listed --> <shortcode country="nz" pattern="\\d{3,4}" premium="3903|8995|4679" free="1737|176|2141|3067|3068|3110|3876|4006|4053|4061|4062|4202|4300|4334|4412|4575|5626|8006|8681" /> - <!-- Peru: 4-5 digits (not confirmed), known premium codes listed --> - <shortcode country="pe" pattern="\\d{4,5}" free="9963|40778" /> + <!-- Peru: 4-6 digits (not confirmed), known premium codes listed --> + <shortcode country="pe" pattern="\\d{4,6}" free="9963|40778|301303" /> <!-- Philippines --> <shortcode country="ph" pattern="\\d{1,5}" free="2147|5495|5496" /> @@ -269,6 +284,12 @@ <!-- Slovakia: 4 digits (premium), plus EU: http://www.cmtelecom.com/premium-sms/slovakia --> <shortcode country="sk" premium="\\d{4}" free="116\\d{3}|8000" /> + <!-- Senegal(SN): 1-5 digits (standard system default, not country specific) --> + <shortcode country="sn" pattern="\\d{1,5}" free="21215" /> + + <!-- El Salvador(SV): 1-5 digits (standard system default, not country specific) --> + <shortcode country="sv" pattern="\\d{4,6}" free="466453" /> + <!-- Taiwan --> <shortcode country="tw" pattern="\\d{4}" free="1922" /> @@ -278,15 +299,21 @@ <!-- Tajikistan: 4 digits, known premium codes listed --> <shortcode country="tj" pattern="\\d{4}" premium="11[3-7]1|4161|4333|444[689]" /> + <!-- Tanzania: 1-5 digits (standard system default, not country specific) --> + <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234" /> + <!-- Turkey --> <shortcode country="tr" pattern="\\d{1,5}" free="7529|5528|6493|3193" /> <!-- Ukraine: 4 digits, known premium codes listed --> <shortcode country="ua" pattern="\\d{4}" premium="444[3-9]|70[579]4|7540" /> + <!-- Uganda(UG): 4 digits (standard system default, not country specific) --> + <shortcode country="ug" pattern="\\d{4}" free="8000" /> + <!-- 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}" 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" /> + <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" /> <!-- Vietnam: 1-5 digits (standard system default, not country specific) --> <shortcode country="vn" pattern="\\d{1,5}" free="5001|9055" /> diff --git a/core/tests/coretests/src/android/app/QueuedWorkTest.java b/core/tests/coretests/src/android/app/QueuedWorkTest.java new file mode 100644 index 000000000000..70211870989f --- /dev/null +++ b/core/tests/coretests/src/android/app/QueuedWorkTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.Semaphore; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@Presubmit +@SmallTest +public class QueuedWorkTest { + + private QueuedWork mQueuedWork; + private AtomicInteger mCounter; + + private class AddToCounter implements Runnable { + private final int mDelta; + + public AddToCounter(int delta) { + mDelta = delta; + } + + @Override + public void run() { + mCounter.addAndGet(mDelta); + } + } + + private class IncrementCounter extends AddToCounter { + public IncrementCounter() { + super(1); + } + } + + @Before + public void setup() { + mQueuedWork = new QueuedWork(); + mCounter = new AtomicInteger(0); + } + + @After + public void teardown() { + mQueuedWork.waitToFinish(); + QueuedWork.resetHandler(); + } + + @Test + public void testQueueThenWait() { + mQueuedWork.queue(new IncrementCounter(), false); + mQueuedWork.waitToFinish(); + assertThat(mCounter.get()).isEqualTo(1); + assertThat(mQueuedWork.hasPendingWork()).isFalse(); + } + + @Test + public void testQueueWithDelayThenWait() { + mQueuedWork.queue(new IncrementCounter(), true); + mQueuedWork.waitToFinish(); + assertThat(mCounter.get()).isEqualTo(1); + assertThat(mQueuedWork.hasPendingWork()).isFalse(); + } + + @Test + public void testWorkHappensNotOnCallerThread() { + AtomicBoolean childThreadStarted = new AtomicBoolean(false); + InheritableThreadLocal<Boolean> setTrueInChild = + new InheritableThreadLocal<Boolean>() { + @Override + protected Boolean initialValue() { + return false; + } + + @Override + protected Boolean childValue(Boolean parentValue) { + childThreadStarted.set(true); + return true; + } + }; + + // Enqueue work to force a worker thread to be created + setTrueInChild.get(); + assertThat(childThreadStarted.get()).isFalse(); + mQueuedWork.queue(() -> setTrueInChild.get(), false); + mQueuedWork.waitToFinish(); + assertThat(childThreadStarted.get()).isTrue(); + } + + @Test + public void testWaitToFinishDoesNotCreateThread() { + InheritableThreadLocal<Boolean> throwInChild = + new InheritableThreadLocal<Boolean>() { + @Override + protected Boolean initialValue() { + return false; + } + + @Override + protected Boolean childValue(Boolean parentValue) { + throw new RuntimeException("New thread should not be started!"); + } + }; + + try { + throwInChild.get(); + // Intentionally don't enqueue work. + mQueuedWork.waitToFinish(); + throwInChild.get(); + // If a worker thread was unnecessarily started, we will have crashed. + } finally { + throwInChild.remove(); + } + } + + @Test + public void testFinisher() { + mQueuedWork.addFinisher(new AddToCounter(3)); + mQueuedWork.addFinisher(new AddToCounter(7)); + mQueuedWork.queue(new IncrementCounter(), false); + mQueuedWork.waitToFinish(); + // The queued task and the two finishers all ran + assertThat(mCounter.get()).isEqualTo(1 + 3 + 7); + } + + @Test + public void testRemoveFinisher() { + Runnable addThree = new AddToCounter(3); + Runnable addSeven = new AddToCounter(7); + mQueuedWork.addFinisher(addThree); + mQueuedWork.addFinisher(addSeven); + mQueuedWork.removeFinisher(addThree); + mQueuedWork.queue(new IncrementCounter(), false); + mQueuedWork.waitToFinish(); + // The queued task and the two finishers all ran + assertThat(mCounter.get()).isEqualTo(1 + 7); + } + + @Test + public void testHasPendingWork() { + Semaphore releaser = new Semaphore(0); + mQueuedWork.queue( + () -> { + try { + releaser.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }, false); + assertThat(mQueuedWork.hasPendingWork()).isTrue(); + releaser.release(); + mQueuedWork.waitToFinish(); + assertThat(mQueuedWork.hasPendingWork()).isFalse(); + } +}
\ No newline at end of file diff --git a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java index de55b0759edd..3df3b9d2c555 100644 --- a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java +++ b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java @@ -20,6 +20,7 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; @@ -63,7 +64,8 @@ public final class PhoneWindowTest { createPhoneWindowWithTheme(R.style.LayoutInDisplayCutoutModeUnset); installDecor(); - if (mPhoneWindow.mEdgeToEdgeEnforced && !mPhoneWindow.isFloating()) { + if ((mPhoneWindow.getAttributes().privateFlags & PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED) != 0 + && !mPhoneWindow.isFloating()) { assertThat(mPhoneWindow.getAttributes().layoutInDisplayCutoutMode, is(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS)); } else { diff --git a/data/etc/preinstalled-packages-platform.xml b/data/etc/preinstalled-packages-platform.xml index bf6094469215..782327713fdc 100644 --- a/data/etc/preinstalled-packages-platform.xml +++ b/data/etc/preinstalled-packages-platform.xml @@ -108,6 +108,7 @@ to pre-existing users, but cannot uninstall pre-existing system packages from pr <install-in user-type="FULL" /> <install-in user-type="PROFILE" /> <do-not-install-in user-type="android.os.usertype.profile.CLONE" /> + <do-not-install-in user-type="android.os.usertype.profile.PRIVATE" /> </install-in-user-type> <!-- Settings (Settings app) --> diff --git a/keystore/java/android/security/AndroidKeyStoreMaintenance.java b/keystore/java/android/security/AndroidKeyStoreMaintenance.java index 2beb434566e5..2430e8d8e662 100644 --- a/keystore/java/android/security/AndroidKeyStoreMaintenance.java +++ b/keystore/java/android/security/AndroidKeyStoreMaintenance.java @@ -18,6 +18,7 @@ package android.security; import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.os.StrictMode; @@ -218,4 +219,28 @@ public class AndroidKeyStoreMaintenance { return SYSTEM_ERROR; } } + + /** + * Returns the list of Application UIDs that have auth-bound keys that are bound to + * the given SID. This enables warning the user when they are about to invalidate + * a SID (for example, removing the LSKF). + * + * @param userId - The ID of the user the SID is associated with. + * @param userSecureId - The SID in question. + * + * @return A list of app UIDs. + */ + public static long[] getAllAppUidsAffectedBySid(int userId, long userSecureId) + throws KeyStoreException { + StrictMode.noteDiskWrite(); + try { + return getService().getAppUidsAffectedBySid(userId, userSecureId); + } catch (RemoteException | NullPointerException e) { + throw new KeyStoreException(SYSTEM_ERROR, + "Failure to connect to Keystore while trying to get apps affected by SID."); + } catch (ServiceSpecificException e) { + throw new KeyStoreException(e.errorCode, + "Keystore error while trying to get apps affected by SID."); + } + } } diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 9854e58dd7cf..a80afe2bf40a 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -45,9 +45,6 @@ <!-- Allow PIP to resize to a slightly bigger state upon touch/showing the menu --> <bool name="config_pipEnableResizeForMenu">true</bool> - <!-- Allow PIP to resize via dragging the corner of PiP. --> - <bool name="config_pipEnableDragCornerResize">false</bool> - <!-- PiP minimum size, which is a % based off the shorter side of display width and height --> <fraction name="config_pipShortestEdgePercent">40%</fraction> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index a2a2914f969f..7a4ad0a56022 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -912,9 +912,8 @@ public class Bubble implements BubbleViewProvider { if (uid != -1) { intent.putExtra(Settings.EXTRA_APP_UID, uid); } - intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); return intent; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index 8b6c7b663f82..68d26da2002f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -140,7 +140,7 @@ public class PipBoundsState { // spec takes the aspect ratio of the bounds into account, so both width and height // scale by the same factor. addPipExclusionBoundsChangeCallback((bounds) -> { - mBoundsScale = Math.min((float) bounds.width() / mMaxSize.x, 1.0f); + updateBoundsScale(); }); } @@ -152,6 +152,11 @@ public class PipBoundsState { mSizeSpecSource.onConfigurationChanged(); } + /** Update the bounds scale percentage value. */ + public void updateBoundsScale() { + mBoundsScale = Math.min((float) mBounds.width() / mMaxSize.x, 1.0f); + } + private void reloadResources() { mStashOffset = mContext.getResources().getDimensionPixelSize(R.dimen.pip_stash_offset); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 896ca9647720..e018ecc0f7e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -286,12 +286,6 @@ public class PipTransition extends PipTransitionController { // For transition that we don't animate, but contains the PIP leash, we need to update the // PIP surface, otherwise it will be reset after the transition. if (currentPipTaskChange != null) { - // Set the "end" bounds of pip. The default setup uses the start bounds. Since this is - // changing the *finish*Transaction, we need to use the end bounds. This will also - // make sure that the fade-in animation (below) uses the end bounds as well. - if (!currentPipTaskChange.getEndAbsBounds().isEmpty()) { - mPipBoundsState.setBounds(currentPipTaskChange.getEndAbsBounds()); - } updatePipForUnhandledTransition(currentPipTaskChange, startTransaction, finishTransaction); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index c5a01025dcdd..05d4f53986b8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -244,6 +244,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb // The same rotation may have been set by auto PiP-able or fixed rotation. So notify // the change with fromRotation=false to apply the rotated destination bounds from // PipTaskOrganizer#onMovementBoundsChanged. + // We need to update the bounds scale in case this was from fixed rotation, as the + // current proportion was computed using the previous orientation max size and is wrong. + mPipBoundsState.updateBoundsScale(); updateMovementBounds(null, false /* fromRotation */, false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t); return; @@ -797,21 +800,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsAlgorithm.getMovementBounds(postChangeBounds), mPipBoundsState.getStashedState()); - // Scale PiP on density dpi change, so it appears to be the same size physically. - final boolean densityDpiChanged = - mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0 - && (mPipDisplayLayoutState.getDisplayLayout().densityDpi() - != layout.densityDpi()); - if (densityDpiChanged) { - final float scale = (float) layout.densityDpi() - / mPipDisplayLayoutState.getDisplayLayout().densityDpi(); - postChangeBounds.set(0, 0, - (int) (postChangeBounds.width() * scale), - (int) (postChangeBounds.height() * scale)); - } - updateDisplayLayout.run(); + // Resize the PiP bounds to be at the same scale relative to the new size spec. For + // example, if PiP was resized to 90% of the maximum size on the previous layout, + // make sure it is 90% of the new maximum size spec. + postChangeBounds.set(0, 0, + (int) (mPipBoundsState.getMaxSize().x * mPipBoundsState.getBoundsScale()), + (int) (mPipBoundsState.getMaxSize().y * mPipBoundsState.getBoundsScale())); + // Calculate the PiP bounds in the new orientation based on same fraction along the // rotated movement bounds. final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds( @@ -827,6 +824,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.setHasUserResizedPip(true); mTouchHandler.setUserResizeBounds(postChangeBounds); + final boolean densityDpiChanged = + mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0 + && (mPipDisplayLayoutState.getDisplayLayout().densityDpi() + != layout.densityDpi()); if (densityDpiChanged) { // Using PipMotionHelper#movePip directly here may cause race condition since // the app content in PiP mode may or may not be updated for the new density dpi. @@ -1146,6 +1147,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb // Update the display layout mPipDisplayLayoutState.rotateTo(toRotation); + mTouchHandler.updateMinMaxSize(mPipBoundsState.getAspectRatio()); + + postChangeStackBounds.set(0, 0, + (int) (mPipBoundsState.getMaxSize().x * mPipBoundsState.getBoundsScale()), + (int) (mPipBoundsState.getMaxSize().y * mPipBoundsState.getBoundsScale())); // Calculate the stack bounds in the new orientation based on same fraction along the // rotated movement bounds. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index f175775ce8b2..5f9195a37c1b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -15,19 +15,13 @@ */ package com.android.wm.shell.pip.phone; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT; import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; -import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; import android.content.Context; import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; -import android.graphics.Region; import android.hardware.input.InputManager; import android.os.Looper; import android.view.BatchedInputEventReceiver; @@ -41,7 +35,6 @@ import android.view.ViewConfiguration; import androidx.annotation.VisibleForTesting; -import com.android.internal.policy.TaskResizingAlgorithm; import com.android.wm.shell.R; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -53,7 +46,6 @@ import com.android.wm.shell.pip.PipTaskOrganizer; import java.io.PrintWriter; import java.util.function.Consumer; -import java.util.function.Function; /** * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to @@ -77,7 +69,6 @@ public class PipResizeGestureHandler { private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; private final int mDisplayId; private final ShellExecutor mMainExecutor; - private final Region mTmpRegion = new Region(); private final PointF mDownPoint = new PointF(); private final PointF mDownSecondPoint = new PointF(); @@ -88,24 +79,15 @@ public class PipResizeGestureHandler { private final Rect mLastResizeBounds = new Rect(); private final Rect mUserResizeBounds = new Rect(); private final Rect mDownBounds = new Rect(); - private final Rect mDragCornerSize = new Rect(); - private final Rect mTmpTopLeftCorner = new Rect(); - private final Rect mTmpTopRightCorner = new Rect(); - private final Rect mTmpBottomLeftCorner = new Rect(); - private final Rect mTmpBottomRightCorner = new Rect(); - private final Rect mDisplayBounds = new Rect(); - private final Function<Rect, Rect> mMovementBoundsSupplier; private final Runnable mUpdateMovementBoundsRunnable; private final Consumer<Rect> mUpdateResizeBoundsCallback; - private int mDelta; private float mTouchSlop; private boolean mAllowGesture; private boolean mIsAttached; private boolean mIsEnabled; private boolean mEnablePinchResize; - private boolean mEnableDragCornerResize; private boolean mIsSysUiStateValid; private boolean mThresholdCrossed; private boolean mOngoingPinchToResize = false; @@ -123,7 +105,7 @@ public class PipResizeGestureHandler { PipBoundsState pipBoundsState, PipMotionHelper motionHelper, PipTouchState pipTouchState, PipTaskOrganizer pipTaskOrganizer, PipDismissTargetHandler pipDismissTargetHandler, - Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, + Runnable updateMovementBoundsRunnable, PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, ShellExecutor mainExecutor) { mContext = context; @@ -135,7 +117,6 @@ public class PipResizeGestureHandler { mPipTouchState = pipTouchState; mPipTaskOrganizer = pipTaskOrganizer; mPipDismissTargetHandler = pipDismissTargetHandler; - mMovementBoundsSupplier = movementBoundsSupplier; mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; @@ -171,20 +152,9 @@ public class PipResizeGestureHandler { } private void reloadResources() { - final Resources res = mContext.getResources(); - mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size); - mEnableDragCornerResize = res.getBoolean(R.bool.config_pipEnableDragCornerResize); mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); } - private void resetDragCorners() { - mDragCornerSize.set(0, 0, mDelta, mDelta); - mTmpTopLeftCorner.set(mDragCornerSize); - mTmpTopRightCorner.set(mDragCornerSize); - mTmpBottomLeftCorner.set(mDragCornerSize); - mTmpBottomRightCorner.set(mDragCornerSize); - } - private void disposeInputChannel() { if (mInputEventReceiver != null) { mInputEventReceiver.dispose(); @@ -232,7 +202,7 @@ public class PipResizeGestureHandler { @VisibleForTesting void onInputEvent(InputEvent ev) { - if (!mEnableDragCornerResize && !mEnablePinchResize) { + if (!mEnablePinchResize) { // No need to handle anything if neither form of resizing is enabled. return; } @@ -260,8 +230,6 @@ public class PipResizeGestureHandler { if (mEnablePinchResize && mOngoingPinchToResize) { onPinchResize(mv); - } else if (mEnableDragCornerResize) { - onDragCornerResize(mv); } } } @@ -273,48 +241,6 @@ public class PipResizeGestureHandler { return mCtrlType != CTRL_NONE || mOngoingPinchToResize; } - /** - * Check whether the current x,y coordinate is within the region in which drag-resize should - * start. - * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which - * overlaps with the PIP window while the rest goes outside of the PIP window. - * _ _ _ _ - * |_|_|_________|_|_| - * |_|_| |_|_| - * | PIP | - * | WINDOW | - * _|_ _|_ - * |_|_|_________|_|_| - * |_|_| |_|_| - */ - public boolean isWithinDragResizeRegion(int x, int y) { - if (!mEnableDragCornerResize) { - return false; - } - - final Rect currentPipBounds = mPipBoundsState.getBounds(); - if (currentPipBounds == null) { - return false; - } - resetDragCorners(); - mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2, - currentPipBounds.top - mDelta / 2); - mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2, - currentPipBounds.top - mDelta / 2); - mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2, - currentPipBounds.bottom - mDelta / 2); - mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2, - currentPipBounds.bottom - mDelta / 2); - - mTmpRegion.setEmpty(); - mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION); - mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION); - mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION); - mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION); - - return mTmpRegion.contains(x, y); - } - public boolean isUsingPinchToZoom() { return mEnablePinchResize; } @@ -325,62 +251,17 @@ public class PipResizeGestureHandler { public boolean willStartResizeGesture(MotionEvent ev) { if (isInValidSysUiState()) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - if (isWithinDragResizeRegion((int) ev.getRawX(), (int) ev.getRawY())) { - return true; - } - break; - - case MotionEvent.ACTION_POINTER_DOWN: - if (mEnablePinchResize && ev.getPointerCount() == 2) { - onPinchResize(ev); - mOngoingPinchToResize = mAllowGesture; - return mAllowGesture; - } - break; - - default: - break; + if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { + if (mEnablePinchResize && ev.getPointerCount() == 2) { + onPinchResize(ev); + mOngoingPinchToResize = mAllowGesture; + return mAllowGesture; + } } } return false; } - private void setCtrlType(int x, int y) { - final Rect currentPipBounds = mPipBoundsState.getBounds(); - - Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); - - mDisplayBounds.set(movementBounds.left, - movementBounds.top, - movementBounds.right + currentPipBounds.width(), - movementBounds.bottom + currentPipBounds.height()); - - if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top - && currentPipBounds.left != mDisplayBounds.left) { - mCtrlType |= CTRL_LEFT; - mCtrlType |= CTRL_TOP; - } - if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top - && currentPipBounds.right != mDisplayBounds.right) { - mCtrlType |= CTRL_RIGHT; - mCtrlType |= CTRL_TOP; - } - if (mTmpBottomRightCorner.contains(x, y) - && currentPipBounds.bottom != mDisplayBounds.bottom - && currentPipBounds.right != mDisplayBounds.right) { - mCtrlType |= CTRL_RIGHT; - mCtrlType |= CTRL_BOTTOM; - } - if (mTmpBottomLeftCorner.contains(x, y) - && currentPipBounds.bottom != mDisplayBounds.bottom - && currentPipBounds.left != mDisplayBounds.left) { - mCtrlType |= CTRL_LEFT; - mCtrlType |= CTRL_BOTTOM; - } - } - private boolean isInValidSysUiState() { return mIsSysUiStateValid; } @@ -457,59 +338,6 @@ public class PipResizeGestureHandler { } } - private void onDragCornerResize(MotionEvent ev) { - int action = ev.getActionMasked(); - float x = ev.getX(); - float y = ev.getY() - mOhmOffset; - if (action == MotionEvent.ACTION_DOWN) { - mLastResizeBounds.setEmpty(); - mAllowGesture = isInValidSysUiState() && isWithinDragResizeRegion((int) x, (int) y); - if (mAllowGesture) { - setCtrlType((int) x, (int) y); - mDownPoint.set(x, y); - mDownBounds.set(mPipBoundsState.getBounds()); - } - } else if (mAllowGesture) { - switch (action) { - case MotionEvent.ACTION_POINTER_DOWN: - // We do not support multi touch for resizing via drag - mAllowGesture = false; - break; - case MotionEvent.ACTION_MOVE: - // Capture inputs - if (!mThresholdCrossed - && Math.hypot(x - mDownPoint.x, y - mDownPoint.y) > mTouchSlop) { - mThresholdCrossed = true; - // Reset the down to begin resizing from this point - mDownPoint.set(x, y); - mInputMonitor.pilferPointers(); - } - if (mThresholdCrossed) { - if (mPhonePipMenuController.isMenuVisible()) { - mPhonePipMenuController.hideMenu(ANIM_TYPE_NONE, - false /* resize */); - } - final Rect currentPipBounds = mPipBoundsState.getBounds(); - mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, - mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x, - mMinSize.y, mMaxSize, true, - mDownBounds.width() > mDownBounds.height())); - mPipBoundsAlgorithm.transformBoundsToAspectRatio(mLastResizeBounds, - mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */, - true /* useCurrentSize */); - mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds, - null); - mPipBoundsState.setHasUserResizedPip(true); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - finishResize(); - break; - } - } - } - private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) { final int leftEdge = bounds.left; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 81705e20a1df..11c356d94ee1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -212,7 +212,7 @@ public class PipTouchHandler { mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, mMotionHelper, mTouchState, pipTaskOrganizer, mPipDismissTargetHandler, - this::getMovementBounds, this::updateMovementBounds, pipUiEventLogger, + this::updateMovementBounds, pipUiEventLogger, menuController, mainExecutor); mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState, mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index a666e208a1b9..bfb60c0f4fc8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -253,7 +253,7 @@ public class SplashscreenContentDrawer { : taskInfo.topActivityInfo; params.layoutInDisplayCutoutMode = a.getInt( R.styleable.Window_windowLayoutInDisplayCutoutMode, - PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */) + PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */, a) ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : params.layoutInDisplayCutoutMode); params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0); diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt index 8207b85c3e0c..07cd68261083 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt @@ -86,6 +86,8 @@ class EnterPipToOtherOrientation(flicker: LegacyFlickerTest) : PipTransition(fli .withNavOrTaskBarVisible() .withStatusBarVisible() .waitForAndVerify() + + pipApp.tapPipToShowMenu(wmHelper) } } @@ -194,6 +196,16 @@ class EnterPipToOtherOrientation(flicker: LegacyFlickerTest) : PipTransition(fli } } + @Postsubmit + @Test + fun menuOverlayMatchesTaskSurface() { + flicker.assertLayersEnd { + val pipAppRegion = visibleRegion(pipApp) + val pipMenuRegion = visibleRegion(ComponentNameMatcher.PIP_MENU_OVERLAY) + pipAppRegion.coversExactly(pipMenuRegion.region) + } + } + /** {@inheritDoc} */ @FlakyTest(bugId = 267424412) @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index 9719ba89b4bb..cc726cb0adcf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -121,7 +121,7 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { mPipResizeGestureHandler = new PipResizeGestureHandler(mContext, pipBoundsAlgorithm, mPipBoundsState, motionHelper, mPipTouchState, mPipTaskOrganizer, mPipDismissTargetHandler, - (Rect bounds) -> new Rect(), () -> {}, mPipUiEventLogger, mPhonePipMenuController, + () -> {}, mPipUiEventLogger, mPhonePipMenuController, mMainExecutor) { @Override public void pilferPointers() { diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING index 4fbe9ee90c4c..6ac969527d89 100644 --- a/media/TEST_MAPPING +++ b/media/TEST_MAPPING @@ -40,17 +40,6 @@ }, { "file_patterns": [ - "[^/]*(Ringtone)[^/]*\\.java" - ], - "name": "MediaRingtoneTests", - "options": [ - {"exclude-annotation": "androidx.test.filters.LargeTest"}, - {"exclude-annotation": "androidx.test.filters.FlakyTest"}, - {"exclude-annotation": "org.junit.Ignore"} - ] - }, - { - "file_patterns": [ "[^/]*(LoudnessCodec)[^/]*\\.java" ], "name": "LoudnessCodecApiTest", diff --git a/media/java/android/media/FadeManagerConfiguration.java b/media/java/android/media/FadeManagerConfiguration.java index 40b0e3e03ef6..64ba6b2a30bf 100644 --- a/media/java/android/media/FadeManagerConfiguration.java +++ b/media/java/android/media/FadeManagerConfiguration.java @@ -18,6 +18,7 @@ package android.media; import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; +import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -112,17 +113,11 @@ public final class FadeManagerConfiguration implements Parcelable { */ public static final int FADE_STATE_ENABLED_DEFAULT = 1; - /** - * Defines the enabled state with Automotive specific configurations - */ - public static final int FADE_STATE_ENABLED_AUTO = 2; - /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = false, prefix = "FADE_STATE", value = { FADE_STATE_DISABLED, FADE_STATE_ENABLED_DEFAULT, - FADE_STATE_ENABLED_AUTO, }) public @interface FadeStateEnum {} @@ -143,7 +138,14 @@ public final class FadeManagerConfiguration implements Parcelable { * @see #getFadeOutDurationForAudioAttributes(AudioAttributes) * @see #getFadeInDurationForAudioAttributes(AudioAttributes) */ - public static final long DURATION_NOT_SET = 0; + public static final @DurationMillisLong long DURATION_NOT_SET = 0; + + /** Defines the default fade out duration */ + private static final @DurationMillisLong long DEFAULT_FADE_OUT_DURATION_MS = 2_000; + + /** Defines the default fade in duration */ + private static final @DurationMillisLong long DEFAULT_FADE_IN_DURATION_MS = 1_000; + /** Map of Usage to Fade volume shaper configs wrapper */ private final SparseArray<FadeVolumeShaperConfigsWrapper> mUsageToFadeWrapperMap; /** Map of AudioAttributes to Fade volume shaper configs wrapper */ @@ -161,14 +163,15 @@ public final class FadeManagerConfiguration implements Parcelable { /** fade state */ private final @FadeStateEnum int mFadeState; /** fade out duration from builder - used for creating default fade out volume shaper */ - private final long mFadeOutDurationMillis; + private final @DurationMillisLong long mFadeOutDurationMillis; /** fade in duration from builder - used for creating default fade in volume shaper */ - private final long mFadeInDurationMillis; + private final @DurationMillisLong long mFadeInDurationMillis; /** delay after which the offending players are faded back in */ - private final long mFadeInDelayForOffendersMillis; + private final @DurationMillisLong long mFadeInDelayForOffendersMillis; - private FadeManagerConfiguration(int fadeState, long fadeOutDurationMillis, - long fadeInDurationMillis, long offendersFadeInDelayMillis, + private FadeManagerConfiguration(int fadeState, @DurationMillisLong long fadeOutDurationMillis, + @DurationMillisLong long fadeInDurationMillis, + @DurationMillisLong long offendersFadeInDelayMillis, @NonNull SparseArray<FadeVolumeShaperConfigsWrapper> usageToFadeWrapperMap, @NonNull ArrayMap<AudioAttributes, FadeVolumeShaperConfigsWrapper> attrToFadeWrapperMap, @NonNull IntArray fadeableUsages, @NonNull IntArray unfadeableContentTypes, @@ -196,8 +199,6 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Get the fade state - * - * @return one of the {@link FadeStateEnum} state */ @FadeStateEnum public int getFadeState() { @@ -207,7 +208,7 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Get the list of usages that can be faded * - * @return list of {@link android.media.AudioAttributes.AttributeUsage} that shall be faded + * @return list of {@link android.media.AudioAttributes usages} that shall be faded * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ @NonNull @@ -217,10 +218,10 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Get the list of {@link android.media.AudioPlaybackConfiguration.PlayerType player types} - * that cannot be faded + * Get the list of {@link android.media.AudioPlaybackConfiguration player types} that can be + * faded * - * @return list of {@link android.media.AudioPlaybackConfiguration.PlayerType} + * @return list of {@link android.media.AudioPlaybackConfiguration player types} * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ @NonNull @@ -230,10 +231,9 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Get the list of {@link android.media.AudioAttributes.AttributeContentType content types} - * that cannot be faded + * Get the list of {@link android.media.AudioAttributes content types} that can be faded * - * @return list of {@link android.media.AudioAttributes.AttributeContentType} + * @return list of {@link android.media.AudioAttributes content types} * @throws IllegalStateExceptionif if the fade state is set to {@link #FADE_STATE_DISABLED} */ @NonNull @@ -267,15 +267,15 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Get the duration used to fade out players with - * {@link android.media.AudioAttributes.AttributeUsage} + * Get the duration used to fade out players with {@link android.media.AudioAttributes usage} * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @param usage the {@link android.media.AudioAttributes usage} * @return duration in milliseconds if set for the usage or {@link #DURATION_NOT_SET} otherwise * @throws IllegalArgumentException if the usage is invalid * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ - public long getFadeOutDurationForUsage(int usage) { + @DurationMillisLong + public long getFadeOutDurationForUsage(@AudioAttributes.AttributeUsage int usage) { ensureFadingIsEnabled(); validateUsage(usage); return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper( @@ -283,15 +283,15 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Get the duration used to fade in players with - * {@link android.media.AudioAttributes.AttributeUsage} + * Get the duration used to fade in players with {@link android.media.AudioAttributes usage} * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @param usage the {@link android.media.AudioAttributes usage} * @return duration in milliseconds if set for the usage or {@link #DURATION_NOT_SET} otherwise * @throws IllegalArgumentException if the usage is invalid * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ - public long getFadeInDurationForUsage(int usage) { + @DurationMillisLong + public long getFadeInDurationForUsage(@AudioAttributes.AttributeUsage int usage) { ensureFadingIsEnabled(); validateUsage(usage); return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper( @@ -300,16 +300,17 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Get the {@link android.media.VolumeShaper.Configuration} used to fade out players with - * {@link android.media.AudioAttributes.AttributeUsage} + * {@link android.media.AudioAttributes usage} * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @param usage the {@link android.media.AudioAttributes usage} * @return {@link android.media.VolumeShaper.Configuration} if set for the usage or - * {@code null} otherwise + * {@code null} otherwise * @throws IllegalArgumentException if the usage is invalid * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ @Nullable - public VolumeShaper.Configuration getFadeOutVolumeShaperConfigForUsage(int usage) { + public VolumeShaper.Configuration getFadeOutVolumeShaperConfigForUsage( + @AudioAttributes.AttributeUsage int usage) { ensureFadingIsEnabled(); validateUsage(usage); return getVolumeShaperConfigFromWrapper(mUsageToFadeWrapperMap.get(usage), @@ -318,16 +319,17 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Get the {@link android.media.VolumeShaper.Configuration} used to fade in players with - * {@link android.media.AudioAttributes.AttributeUsage} + * {@link android.media.AudioAttributes usage} * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of player + * @param usage the {@link android.media.AudioAttributes usage} * @return {@link android.media.VolumeShaper.Configuration} if set for the usage or - * {@code null} otherwise + * {@code null} otherwise * @throws IllegalArgumentException if the usage is invalid * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ @Nullable - public VolumeShaper.Configuration getFadeInVolumeShaperConfigForUsage(int usage) { + public VolumeShaper.Configuration getFadeInVolumeShaperConfigForUsage( + @AudioAttributes.AttributeUsage int usage) { ensureFadingIsEnabled(); validateUsage(usage); return getVolumeShaperConfigFromWrapper(mUsageToFadeWrapperMap.get(usage), @@ -339,10 +341,11 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param audioAttributes {@link android.media.AudioAttributes} * @return duration in milliseconds if set for the audio attributes or - * {@link #DURATION_NOT_SET} otherwise + * {@link #DURATION_NOT_SET} otherwise * @throws NullPointerException if the audio attributes is {@code null} * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ + @DurationMillisLong public long getFadeOutDurationForAudioAttributes(@NonNull AudioAttributes audioAttributes) { ensureFadingIsEnabled(); return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper( @@ -354,10 +357,11 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param audioAttributes {@link android.media.AudioAttributes} * @return duration in milliseconds if set for the audio attributes or - * {@link #DURATION_NOT_SET} otherwise + * {@link #DURATION_NOT_SET} otherwise * @throws NullPointerException if the audio attributes is {@code null} * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ + @DurationMillisLong public long getFadeInDurationForAudioAttributes(@NonNull AudioAttributes audioAttributes) { ensureFadingIsEnabled(); return getDurationForVolumeShaperConfig(getVolumeShaperConfigFromWrapper( @@ -370,7 +374,7 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param audioAttributes {@link android.media.AudioAttributes} * @return {@link android.media.VolumeShaper.Configuration} if set for the audio attribute or - * {@code null} otherwise + * {@code null} otherwise * @throws NullPointerException if the audio attributes is {@code null} * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ @@ -389,7 +393,7 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param audioAttributes {@link android.media.AudioAttributes} * @return {@link android.media.VolumeShaper.Configuration} used for fading in if set for the - * audio attribute or {@code null} otherwise + * audio attribute or {@code null} otherwise * @throws NullPointerException if the audio attributes is {@code null} * @throws IllegalStateException if the fade state is set to {@link #FADE_STATE_DISABLED} */ @@ -407,7 +411,7 @@ public final class FadeManagerConfiguration implements Parcelable { * configurations are defined * * @return list of {@link android.media.AudioAttributes} with valid volume shaper configs or - * empty list if none set. + * empty list if none set. */ @NonNull public List<AudioAttributes> getAudioAttributesWithVolumeShaperConfigs() { @@ -417,8 +421,14 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Get the delay after which the offending players are faded back in * + * Players are categorized as offending if they do not honor audio focus state changes. For + * example - when an app loses audio focus, it is expected that the app stops any active + * player in favor of the app(s) that gained audio focus. However, if the app do not stop the + * audio playback, such players are termed as offenders. + * * @return delay in milliseconds */ + @DurationMillisLong public long getFadeInDelayForOffenders() { return mFadeInDelayForOffendersMillis; } @@ -435,8 +445,9 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Query if the usage is fadeable * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} - * @return {@code true} if usage is fadeable, {@code false} otherwise + * @param usage the {@link android.media.AudioAttributes usage} + * @return {@code true} if usage is fadeable, {@code false} when the fade state is set to + * {@link #FADE_STATE_DISABLED} or if the usage is not fadeable. */ public boolean isUsageFadeable(@AudioAttributes.AttributeUsage int usage) { if (!isFadeEnabled()) { @@ -448,9 +459,9 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Query if the content type is unfadeable * - * @param contentType the {@link android.media.AudioAttributes.AttributeContentType} + * @param contentType the {@link android.media.AudioAttributes content type} * @return {@code true} if content type is unfadeable or if fade state is set to - * {@link #FADE_STATE_DISABLED}, {@code false} otherwise + * {@link #FADE_STATE_DISABLED}, {@code false} otherwise */ public boolean isContentTypeUnfadeable(@AudioAttributes.AttributeContentType int contentType) { if (!isFadeEnabled()) { @@ -462,11 +473,11 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Query if the player type is unfadeable * - * @param playerType the {@link android.media.AudioPlaybackConfiguration} player type + * @param playerType the {@link android.media.AudioPlaybackConfiguration player type} * @return {@code true} if player type is unfadeable or if fade state is set to - * {@link #FADE_STATE_DISABLED}, {@code false} otherwise + * {@link #FADE_STATE_DISABLED}, {@code false} otherwise */ - public boolean isPlayerTypeUnfadeable(int playerType) { + public boolean isPlayerTypeUnfadeable(@AudioPlaybackConfiguration.PlayerType int playerType) { if (!isFadeEnabled()) { return true; } @@ -478,7 +489,7 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param audioAttributes the {@link android.media.AudioAttributes} * @return {@code true} if audio attributes is unfadeable or if fade state is set to - * {@link #FADE_STATE_DISABLED}, {@code false} otherwise + * {@link #FADE_STATE_DISABLED}, {@code false} otherwise * @throws NullPointerException if the audio attributes is {@code null} */ public boolean isAudioAttributesUnfadeable(@NonNull AudioAttributes audioAttributes) { @@ -494,7 +505,7 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param uid the uid of application * @return {@code true} if uid is unfadeable or if fade state is set to - * {@link #FADE_STATE_DISABLED}, {@code false} otherwise + * {@link #FADE_STATE_DISABLED}, {@code false} otherwise */ public boolean isUidUnfadeable(int uid) { if (!isFadeEnabled()) { @@ -503,6 +514,20 @@ public final class FadeManagerConfiguration implements Parcelable { return mUnfadeableUids.contains(uid); } + /** + * Returns the default fade out duration (in milliseconds) + */ + public static @DurationMillisLong long getDefaultFadeOutDurationMillis() { + return DEFAULT_FADE_OUT_DURATION_MS; + } + + /** + * Returns the default fade in duration (in milliseconds) + */ + public static @DurationMillisLong long getDefaultFadeInDurationMillis() { + return DEFAULT_FADE_IN_DURATION_MS; + } + @Override public String toString() { return "FadeManagerConfiguration { fade state = " + fadeStateToString(mFadeState) @@ -520,7 +545,7 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Convert fade state into a human-readable string * - * @param fadeState one of the fade state in {@link FadeStateEnum} + * @param fadeState one of {@link #FADE_STATE_DISABLED} or {@link #FADE_STATE_ENABLED_DEFAULT} * @return human-readable string * @hide */ @@ -531,8 +556,6 @@ public final class FadeManagerConfiguration implements Parcelable { return "FADE_STATE_DISABLED"; case FADE_STATE_ENABLED_DEFAULT: return "FADE_STATE_ENABLED_DEFAULT"; - case FADE_STATE_ENABLED_AUTO: - return "FADE_STATE_ENABLED_AUTO"; default: return "unknown fade state: " + fadeState; } @@ -712,9 +735,9 @@ public final class FadeManagerConfiguration implements Parcelable { * * <p><b>Notes:</b> * <ul> - * <li>When fade state is set to {@link #FADE_STATE_ENABLED_DEFAULT} or - * {@link #FADE_STATE_ENABLED_AUTO}, the builder expects at least one valid usage to be - * set/added. Failure to do so will result in an exception during {@link #build()}</li> + * <li>When fade state is set to {@link #FADE_STATE_ENABLED_DEFAULT}, the builder expects at + * least one valid usage to be set/added. Failure to do so will result in an exception + * during {@link #build()}</li> * <li>Every usage added to the fadeable list should have corresponding volume shaper * configs defined. This can be achieved by setting either the duration or volume shaper * config through {@link #setFadeOutDurationForUsage(int, long)} or @@ -741,11 +764,6 @@ public final class FadeManagerConfiguration implements Parcelable { private static final long IS_FADEABLE_USAGES_FIELD_SET = 1 << 1; private static final long IS_UNFADEABLE_CONTENT_TYPE_FIELD_SET = 1 << 2; - /** duration of the fade out curve */ - private static final long DEFAULT_FADE_OUT_DURATION_MS = 2_000; - /** duration of the fade in curve */ - private static final long DEFAULT_FADE_IN_DURATION_MS = 1_000; - /** * delay after which a faded out player will be faded back in. This will be heard by the * user only in the case of unmuting players that didn't respect audio focus and didn't @@ -771,9 +789,10 @@ public final class FadeManagerConfiguration implements Parcelable { }); private int mFadeState = FADE_STATE_ENABLED_DEFAULT; - private long mFadeInDelayForOffendersMillis = DEFAULT_DELAY_FADE_IN_OFFENDERS_MS; - private long mFadeOutDurationMillis; - private long mFadeInDurationMillis; + private @DurationMillisLong long mFadeInDelayForOffendersMillis = + DEFAULT_DELAY_FADE_IN_OFFENDERS_MS; + private @DurationMillisLong long mFadeOutDurationMillis; + private @DurationMillisLong long mFadeInDurationMillis; private long mBuilderFieldsSet; private SparseArray<FadeVolumeShaperConfigsWrapper> mUsageToFadeWrapperMap = new SparseArray<>(); @@ -787,7 +806,8 @@ public final class FadeManagerConfiguration implements Parcelable { private List<AudioAttributes> mUnfadeableAudioAttributes = new ArrayList<>(); /** - * Constructs a new Builder with default fade out and fade in durations + * Constructs a new Builder with {@link #DEFAULT_FADE_OUT_DURATION_MS} and + * {@link #DEFAULT_FADE_IN_DURATION_MS} durations. */ public Builder() { mFadeOutDurationMillis = DEFAULT_FADE_OUT_DURATION_MS; @@ -800,7 +820,8 @@ public final class FadeManagerConfiguration implements Parcelable { * @param fadeOutDurationMillis duration in milliseconds used for fading out * @param fadeInDurationMills duration in milliseconds used for fading in */ - public Builder(long fadeOutDurationMillis, long fadeInDurationMills) { + public Builder(@DurationMillisLong long fadeOutDurationMillis, + @DurationMillisLong long fadeInDurationMills) { mFadeOutDurationMillis = fadeOutDurationMillis; mFadeInDurationMillis = fadeInDurationMills; } @@ -830,7 +851,8 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Set the overall fade state * - * @param state one of the {@link FadeStateEnum} states + * @param state one of the {@link #FADE_STATE_DISABLED} or + * {@link #FADE_STATE_ENABLED_DEFAULT} states * @return the same Builder instance * @throws IllegalArgumentException if the fade state is invalid * @see #getFadeState() @@ -844,21 +866,22 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Set the {@link android.media.VolumeShaper.Configuration} used to fade out players with - * {@link android.media.AudioAttributes.AttributeUsage} + * {@link android.media.AudioAttributes usage} * <p> * This method accepts {@code null} for volume shaper config to clear a previously set * configuration (example, if set through * {@link #Builder(android.media.FadeManagerConfiguration)}) * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of target player + * @param usage the {@link android.media.AudioAttributes usage} of target player * @param fadeOutVShaperConfig the {@link android.media.VolumeShaper.Configuration} used - * to fade out players with usage + * to fade out players with usage * @return the same Builder instance * @throws IllegalArgumentException if the usage is invalid * @see #getFadeOutVolumeShaperConfigForUsage(int) */ @NonNull - public Builder setFadeOutVolumeShaperConfigForUsage(int usage, + public Builder setFadeOutVolumeShaperConfigForUsage( + @AudioAttributes.AttributeUsage int usage, @Nullable VolumeShaper.Configuration fadeOutVShaperConfig) { validateUsage(usage); getFadeVolShaperConfigWrapperForUsage(usage) @@ -869,21 +892,22 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Set the {@link android.media.VolumeShaper.Configuration} used to fade in players with - * {@link android.media.AudioAttributes.AttributeUsage} + * {@link android.media.AudioAttributes usage} * <p> * This method accepts {@code null} for volume shaper config to clear a previously set * configuration (example, if set through * {@link #Builder(android.media.FadeManagerConfiguration)}) * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @param usage the {@link android.media.AudioAttributes usage} * @param fadeInVShaperConfig the {@link android.media.VolumeShaper.Configuration} used - * to fade in players with usage + * to fade in players with usage * @return the same Builder instance * @throws IllegalArgumentException if the usage is invalid * @see #getFadeInVolumeShaperConfigForUsage(int) */ @NonNull - public Builder setFadeInVolumeShaperConfigForUsage(int usage, + public Builder setFadeInVolumeShaperConfigForUsage( + @AudioAttributes.AttributeUsage int usage, @Nullable VolumeShaper.Configuration fadeInVShaperConfig) { validateUsage(usage); getFadeVolShaperConfigWrapperForUsage(usage) @@ -894,7 +918,7 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Set the duration used for fading out players with - * {@link android.media.AudioAttributes.AttributeUsage} + * {@link android.media.AudioAttributes usage} * <p> * A Volume shaper configuration is generated with the provided duration and default * volume curve definitions. This config is then used to fade out players with given usage. @@ -904,17 +928,18 @@ public final class FadeManagerConfiguration implements Parcelable { * {@link #DURATION_NOT_SET} and sets the corresponding fade out volume shaper config to * {@code null} * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of target player + * @param usage the {@link android.media.AudioAttributes usage} of target player * @param fadeOutDurationMillis positive duration in milliseconds or - * {@link #DURATION_NOT_SET} + * {@link #DURATION_NOT_SET} * @return the same Builder instance * @throws IllegalArgumentException if the fade out duration is non-positive with the - * exception of {@link #DURATION_NOT_SET} + * exception of {@link #DURATION_NOT_SET} * @see #setFadeOutVolumeShaperConfigForUsage(int, VolumeShaper.Configuration) * @see #getFadeOutDurationForUsage(int) */ @NonNull - public Builder setFadeOutDurationForUsage(int usage, long fadeOutDurationMillis) { + public Builder setFadeOutDurationForUsage(@AudioAttributes.AttributeUsage int usage, + @DurationMillisLong long fadeOutDurationMillis) { validateUsage(usage); VolumeShaper.Configuration fadeOutVShaperConfig = createVolShaperConfigForDuration(fadeOutDurationMillis, /* isFadeIn= */ false); @@ -924,7 +949,7 @@ public final class FadeManagerConfiguration implements Parcelable { /** * Set the duration used for fading in players with - * {@link android.media.AudioAttributes.AttributeUsage} + * {@link android.media.AudioAttributes usage} * <p> * A Volume shaper configuration is generated with the provided duration and default * volume curve definitions. This config is then used to fade in players with given usage. @@ -934,17 +959,18 @@ public final class FadeManagerConfiguration implements Parcelable { * {@link #DURATION_NOT_SET} and sets the corresponding fade in volume shaper config to * {@code null} * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} of target player + * @param usage the {@link android.media.AudioAttributes usage} of target player * @param fadeInDurationMillis positive duration in milliseconds or - * {@link #DURATION_NOT_SET} + * {@link #DURATION_NOT_SET} * @return the same Builder instance * @throws IllegalArgumentException if the fade in duration is non-positive with the - * exception of {@link #DURATION_NOT_SET} + * exception of {@link #DURATION_NOT_SET} * @see #setFadeInVolumeShaperConfigForUsage(int, VolumeShaper.Configuration) * @see #getFadeInDurationForUsage(int) */ @NonNull - public Builder setFadeInDurationForUsage(int usage, long fadeInDurationMillis) { + public Builder setFadeInDurationForUsage(@AudioAttributes.AttributeUsage int usage, + @DurationMillisLong long fadeInDurationMillis) { validateUsage(usage); VolumeShaper.Configuration fadeInVShaperConfig = createVolShaperConfigForDuration(fadeInDurationMillis, /* isFadeIn= */ true); @@ -962,9 +988,8 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param audioAttributes the {@link android.media.AudioAttributes} * @param fadeOutVShaperConfig the {@link android.media.VolumeShaper.Configuration} used to - * fade out players with audio attribute + * fade out players with audio attribute * @return the same Builder instance - * @throws NullPointerException if the audio attributes is {@code null} * @see #getFadeOutVolumeShaperConfigForAudioAttributes(AudioAttributes) */ @NonNull @@ -988,7 +1013,7 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param audioAttributes the {@link android.media.AudioAttributes} * @param fadeInVShaperConfig the {@link android.media.VolumeShaper.Configuration} used to - * fade in players with audio attribute + * fade in players with audio attribute * @return the same Builder instance * @throws NullPointerException if the audio attributes is {@code null} * @see #getFadeInVolumeShaperConfigForAudioAttributes(AudioAttributes) @@ -1017,12 +1042,12 @@ public final class FadeManagerConfiguration implements Parcelable { * {@code null} * * @param audioAttributes the {@link android.media.AudioAttributes} for which the fade out - * duration will be set/updated/reset + * duration will be set/updated/reset * @param fadeOutDurationMillis positive duration in milliseconds or - * {@link #DURATION_NOT_SET} + * {@link #DURATION_NOT_SET} * @return the same Builder instance * @throws IllegalArgumentException if the fade out duration is non-positive with the - * exception of {@link #DURATION_NOT_SET} + * exception of {@link #DURATION_NOT_SET} * @see #getFadeOutDurationForAudioAttributes(AudioAttributes) * @see #setFadeOutVolumeShaperConfigForAudioAttributes(AudioAttributes, * VolumeShaper.Configuration) @@ -1030,7 +1055,7 @@ public final class FadeManagerConfiguration implements Parcelable { @NonNull public Builder setFadeOutDurationForAudioAttributes( @NonNull AudioAttributes audioAttributes, - long fadeOutDurationMillis) { + @DurationMillisLong long fadeOutDurationMillis) { Objects.requireNonNull(audioAttributes, "Audio attribute cannot be null"); VolumeShaper.Configuration fadeOutVShaperConfig = createVolShaperConfigForDuration(fadeOutDurationMillis, /* isFadeIn= */ false); @@ -1039,8 +1064,7 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Set the duration used for fading in players of type - * {@link android.media.AudioAttributes}. + * Set the duration used for fading in players of type {@link android.media.AudioAttributes} * <p> * A Volume shaper configuration is generated with the provided duration and default * volume curve definitions. This config is then used to fade in players with given usage. @@ -1051,19 +1075,19 @@ public final class FadeManagerConfiguration implements Parcelable { * {@code null} * * @param audioAttributes the {@link android.media.AudioAttributes} for which the fade in - * duration will be set/updated/reset + * duration will be set/updated/reset * @param fadeInDurationMillis positive duration in milliseconds or - * {@link #DURATION_NOT_SET} + * {@link #DURATION_NOT_SET} * @return the same Builder instance * @throws IllegalArgumentException if the fade in duration is non-positive with the - * exception of {@link #DURATION_NOT_SET} + * exception of {@link #DURATION_NOT_SET} * @see #getFadeInDurationForAudioAttributes(AudioAttributes) * @see #setFadeInVolumeShaperConfigForAudioAttributes(AudioAttributes, * VolumeShaper.Configuration) */ @NonNull public Builder setFadeInDurationForAudioAttributes(@NonNull AudioAttributes audioAttributes, - long fadeInDurationMillis) { + @DurationMillisLong long fadeInDurationMillis) { Objects.requireNonNull(audioAttributes, "Audio attribute cannot be null"); VolumeShaper.Configuration fadeInVShaperConfig = createVolShaperConfigForDuration(fadeInDurationMillis, /* isFadeIn= */ true); @@ -1072,7 +1096,7 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Set the list of {@link android.media.AudioAttributes.AttributeUsage} that can be faded + * Set the list of {@link android.media.AudioAttributes usage} that can be faded * * <p>This is a positive list. Players with matching usage will be considered for fading. * Usages that are not part of this list will not be faded @@ -1084,10 +1108,9 @@ public final class FadeManagerConfiguration implements Parcelable { * usage to be set/added. Failure to do so will result in an exception during * {@link #build()} * - * @param usages List of the {@link android.media.AudioAttributes.AttributeUsage} + * @param usages List of the {@link android.media.AudioAttributes usages} * @return the same Builder instance * @throws IllegalArgumentException if the usages are invalid - * @throws NullPointerException if the usage list is {@code null} * @see #getFadeableUsages() */ @NonNull @@ -1101,9 +1124,9 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Add the {@link android.media.AudioAttributes.AttributeUsage} to the fadeable list + * Add the {@link android.media.AudioAttributes usage} to the fadeable list * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @param usage the {@link android.media.AudioAttributes usage} * @return the same Builder instance * @throws IllegalArgumentException if the usage is invalid * @see #getFadeableUsages() @@ -1120,11 +1143,11 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Remove the {@link android.media.AudioAttributes.AttributeUsage} from the fadeable list + * Remove the {@link android.media.AudioAttributes usage} from the fadeable list * <p> * Players of this usage type will not be faded. * - * @param usage the {@link android.media.AudioAttributes.AttributeUsage} + * @param usage the {@link android.media.AudioAttributes usage} * @return the same Builder instance * @throws IllegalArgumentException if the usage is invalid * @see #getFadeableUsages() @@ -1142,8 +1165,7 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Set the list of {@link android.media.AudioAttributes.AttributeContentType} that can not - * be faded + * Set the list of {@link android.media.AudioAttributes content type} that can not be faded * * <p>This is a negative list. Players with matching content type of this list will not be * faded. Content types that are not part of this list will be considered for fading. @@ -1151,10 +1173,9 @@ public final class FadeManagerConfiguration implements Parcelable { * <p>Passing an empty list as input clears the existing list. This can be used to * reset the list when using a copy constructor * - * @param contentTypes list of {@link android.media.AudioAttributes.AttributeContentType} + * @param contentTypes list of {@link android.media.AudioAttributes content types} * @return the same Builder instance * @throws IllegalArgumentException if the content types are invalid - * @throws NullPointerException if the content type list is {@code null} * @see #getUnfadeableContentTypes() */ @NonNull @@ -1168,9 +1189,9 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Add the {@link android.media.AudioAttributes.AttributeContentType} to unfadeable list + * Add the {@link android.media.AudioAttributes content type} to unfadeable list * - * @param contentType the {@link android.media.AudioAttributes.AttributeContentType} + * @param contentType the {@link android.media.AudioAttributes content type} * @return the same Builder instance * @throws IllegalArgumentException if the content type is invalid * @see #setUnfadeableContentTypes(List) @@ -1188,10 +1209,9 @@ public final class FadeManagerConfiguration implements Parcelable { } /** - * Remove the {@link android.media.AudioAttributes.AttributeContentType} from the - * unfadeable list + * Remove the {@link android.media.AudioAttributes content type} from the unfadeable list * - * @param contentType the {@link android.media.AudioAttributes.AttributeContentType} + * @param contentType the {@link android.media.AudioAttributes content type} * @return the same Builder instance * @throws IllegalArgumentException if the content type is invalid * @see #setUnfadeableContentTypes(List) @@ -1220,7 +1240,6 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param uids list of uids * @return the same Builder instance - * @throws NullPointerException if the uid list is {@code null} * @see #getUnfadeableUids() */ @NonNull @@ -1274,20 +1293,18 @@ public final class FadeManagerConfiguration implements Parcelable { * reset the list when using a copy constructor * * <p><b>Note:</b> Be cautious when adding generic audio attributes into this list as it can - * negatively impact fadeability decision if such an audio attribute and corresponding - * usage fall into opposing lists. + * negatively impact fadeability decision (if such an audio attribute and corresponding + * usage fall into opposing lists). * For example: * <pre class=prettyprint> * AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build() </pre> * is a generic audio attribute for {@link android.media.AudioAttributes.USAGE_MEDIA}. - * It is an undefined behavior to have an - * {@link android.media.AudioAttributes.AttributeUsage} in the fadeable usage list and the - * corresponding generic {@link android.media.AudioAttributes} in the unfadeable list. Such - * cases will result in an exception during {@link #build()} + * It is an undefined behavior to have an {@link android.media.AudioAttributes usage} in the + * fadeable usage list and the corresponding generic {@link android.media.AudioAttributes} + * in the unfadeable list. Such cases will result in an exception during {@link #build()}. * * @param attrs list of {@link android.media.AudioAttributes} * @return the same Builder instance - * @throws NullPointerException if the audio attributes list is {@code null} * @see #getUnfadeableAudioAttributes() */ @NonNull @@ -1303,7 +1320,6 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param audioAttributes the {@link android.media.AudioAttributes} * @return the same Builder instance - * @throws NullPointerException if the audio attributes is {@code null} * @see #setUnfadeableAudioAttributes(List) * @see #getUnfadeableAudioAttributes() */ @@ -1321,7 +1337,6 @@ public final class FadeManagerConfiguration implements Parcelable { * * @param audioAttributes the {@link android.media.AudioAttributes} * @return the same Builder instance - * @throws NullPointerException if the audio attributes is {@code null} * @see #getUnfadeableAudioAttributes() */ @NonNull @@ -1345,7 +1360,7 @@ public final class FadeManagerConfiguration implements Parcelable { * @see #getFadeInDelayForOffenders() */ @NonNull - public Builder setFadeInDelayForOffenders(long delayMillis) { + public Builder setFadeInDelayForOffenders(@DurationMillisLong long delayMillis) { Preconditions.checkArgument(delayMillis >= 0, "Delay cannot be negative"); mFadeInDelayForOffendersMillis = delayMillis; return this; @@ -1469,7 +1484,6 @@ public final class FadeManagerConfiguration implements Parcelable { switch(state) { case FADE_STATE_DISABLED: case FADE_STATE_ENABLED_DEFAULT: - case FADE_STATE_ENABLED_AUTO: break; default: throw new IllegalArgumentException("Unknown fade state: " + state); diff --git a/media/java/android/media/IRingtonePlayer.aidl b/media/java/android/media/IRingtonePlayer.aidl index 1e57be2c1e22..c96a400407dc 100644 --- a/media/java/android/media/IRingtonePlayer.aidl +++ b/media/java/android/media/IRingtonePlayer.aidl @@ -21,7 +21,6 @@ import android.media.VolumeShaper; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.os.UserHandle; -import android.os.VibrationEffect; /** * @hide @@ -30,23 +29,12 @@ interface IRingtonePlayer { /** Used for Ringtone.java playback */ @UnsupportedAppUsage oneway void play(IBinder token, in Uri uri, in AudioAttributes aa, float volume, boolean looping); + oneway void playWithVolumeShaping(IBinder token, in Uri uri, in AudioAttributes aa, + float volume, boolean looping, in @nullable VolumeShaper.Configuration volumeShaperConfig); oneway void stop(IBinder token); boolean isPlaying(IBinder token); - - // RingtoneV1 - oneway void playWithVolumeShaping(IBinder token, in Uri uri, in AudioAttributes aa, - float volume, boolean looping, in @nullable VolumeShaper.Configuration volumeShaperConfig); oneway void setPlaybackProperties(IBinder token, float volume, boolean looping, - boolean hapticGeneratorEnabled); - - // RingtoneV2 - oneway void playRemoteRingtone(IBinder token, in Uri uri, in AudioAttributes aa, - boolean useExactAudioAttributes, int enabledMedia, in @nullable VibrationEffect ve, - float volume, boolean looping, boolean hapticGeneratorEnabled, - in @nullable VolumeShaper.Configuration volumeShaperConfig); - oneway void setLooping(IBinder token, boolean looping); - oneway void setVolume(IBinder token, float volume); - oneway void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled); + boolean hapticGeneratorEnabled); /** Used for Notification sound playback. */ oneway void playAsync(in Uri uri, in UserHandle user, boolean looping, in AudioAttributes aa, float volume); diff --git a/media/java/android/media/LocalRingtonePlayer.java b/media/java/android/media/LocalRingtonePlayer.java deleted file mode 100644 index fe7cc3ec2af3..000000000000 --- a/media/java/android/media/LocalRingtonePlayer.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.media.audiofx.HapticGenerator; -import android.net.Uri; -import android.os.Trace; -import android.os.VibrationAttributes; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.util.Log; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Objects; - -/** - * Plays a ringtone on the local process. - * @hide - */ -public class LocalRingtonePlayer - implements RingtoneV2.RingtonePlayer, MediaPlayer.OnCompletionListener { - private static final String TAG = "LocalRingtonePlayer"; - - // keep references on active Ringtones until stopped or completion listener called. - private static final ArrayList<LocalRingtonePlayer> sActiveMediaPlayers = new ArrayList<>(); - - private final MediaPlayer mMediaPlayer; - private final AudioAttributes mAudioAttributes; - private final RingtoneV2.RingtonePlayer mVibrationPlayer; - private final Ringtone.Injectables mInjectables; - private final AudioManager mAudioManager; - private final VolumeShaper mVolumeShaper; - private HapticGenerator mHapticGenerator; - - private LocalRingtonePlayer(@NonNull MediaPlayer mediaPlayer, - @NonNull AudioAttributes audioAttributes, @NonNull Ringtone.Injectables injectables, - @NonNull AudioManager audioManager, @Nullable HapticGenerator hapticGenerator, - @Nullable VolumeShaper volumeShaper, - @Nullable RingtoneV2.RingtonePlayer vibrationPlayer) { - Objects.requireNonNull(mediaPlayer); - Objects.requireNonNull(audioAttributes); - Objects.requireNonNull(injectables); - Objects.requireNonNull(audioManager); - mMediaPlayer = mediaPlayer; - mAudioAttributes = audioAttributes; - mInjectables = injectables; - mAudioManager = audioManager; - mVolumeShaper = volumeShaper; - mVibrationPlayer = vibrationPlayer; - mHapticGenerator = hapticGenerator; - } - - /** - * Creates a {@link LocalRingtonePlayer} for a Uri, returning null if the Uri can't be - * loaded in the local player. - */ - @Nullable - static RingtoneV2.RingtonePlayer create(@NonNull Context context, - @NonNull AudioManager audioManager, @NonNull Vibrator vibrator, - @NonNull Uri soundUri, - @NonNull AudioAttributes audioAttributes, - boolean isVibrationOnly, - @Nullable VibrationEffect vibrationEffect, - @NonNull Ringtone.Injectables injectables, - @Nullable VolumeShaper.Configuration volumeShaperConfig, - @Nullable AudioDeviceInfo preferredDevice, boolean initialHapticGeneratorEnabled, - boolean initialLooping, float initialVolume) { - Objects.requireNonNull(context); - Objects.requireNonNull(soundUri); - Objects.requireNonNull(audioAttributes); - Trace.beginSection("createLocalMediaPlayer"); - MediaPlayer mediaPlayer = injectables.newMediaPlayer(); - HapticGenerator hapticGenerator = null; - try { - mediaPlayer.setDataSource(context, soundUri); - mediaPlayer.setAudioAttributes(audioAttributes); - mediaPlayer.setPreferredDevice(preferredDevice); - mediaPlayer.setLooping(initialLooping); - mediaPlayer.setVolume(isVibrationOnly ? 0 : initialVolume); - if (initialHapticGeneratorEnabled) { - hapticGenerator = injectables.createHapticGenerator(mediaPlayer); - if (hapticGenerator != null) { - // In practise, this should always be non-null because the initial value is - // not true unless it's available. - hapticGenerator.setEnabled(true); - vibrationEffect = null; // Don't play the VibrationEffect. - } - } - VolumeShaper volumeShaper = null; - if (volumeShaperConfig != null) { - volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig); - } - mediaPlayer.prepare(); - if (vibrationEffect != null && !audioAttributes.areHapticChannelsMuted()) { - if (injectables.hasHapticChannels(mediaPlayer)) { - // Don't play the Vibration effect if the URI has haptic channels. - vibrationEffect = null; - } - } - VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null : - new VibrationEffectPlayer( - vibrationEffect, audioAttributes, vibrator, initialLooping); - if (isVibrationOnly && vibrationEffectPlayer != null) { - // Abandon the media player now that it's confirmed to not have haptic channels. - mediaPlayer.release(); - return vibrationEffectPlayer; - } - return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager, - hapticGenerator, volumeShaper, vibrationEffectPlayer); - } catch (SecurityException | IOException e) { - if (hapticGenerator != null) { - hapticGenerator.release(); - } - // volume shaper closes with media player - mediaPlayer.release(); - return null; - } finally { - Trace.endSection(); - } - } - - /** - * Creates a {@link LocalRingtonePlayer} for an externally referenced file descriptor. This is - * intended for loading a fallback from an internal resource, rather than via a Uri. - */ - @Nullable - static LocalRingtonePlayer createForFallback( - @NonNull AudioManager audioManager, @NonNull Vibrator vibrator, - @NonNull AssetFileDescriptor afd, - @NonNull AudioAttributes audioAttributes, - @Nullable VibrationEffect vibrationEffect, - @NonNull Ringtone.Injectables injectables, - @Nullable VolumeShaper.Configuration volumeShaperConfig, - @Nullable AudioDeviceInfo preferredDevice, - boolean initialLooping, float initialVolume) { - // Haptic generator not supported for fallback. - Objects.requireNonNull(audioManager); - Objects.requireNonNull(afd); - Objects.requireNonNull(audioAttributes); - Trace.beginSection("createFallbackLocalMediaPlayer"); - - MediaPlayer mediaPlayer = injectables.newMediaPlayer(); - try { - if (afd.getDeclaredLength() < 0) { - mediaPlayer.setDataSource(afd.getFileDescriptor()); - } else { - mediaPlayer.setDataSource(afd.getFileDescriptor(), - afd.getStartOffset(), - afd.getDeclaredLength()); - } - mediaPlayer.setAudioAttributes(audioAttributes); - mediaPlayer.setPreferredDevice(preferredDevice); - mediaPlayer.setLooping(initialLooping); - mediaPlayer.setVolume(initialVolume); - VolumeShaper volumeShaper = null; - if (volumeShaperConfig != null) { - volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig); - } - mediaPlayer.prepare(); - if (vibrationEffect != null && !audioAttributes.areHapticChannelsMuted()) { - if (injectables.hasHapticChannels(mediaPlayer)) { - // Don't play the Vibration effect if the URI has haptic channels. - vibrationEffect = null; - } - } - VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null : - new VibrationEffectPlayer( - vibrationEffect, audioAttributes, vibrator, initialLooping); - return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager, - /* hapticGenerator= */ null, volumeShaper, vibrationEffectPlayer); - } catch (SecurityException | IOException e) { - Log.e(TAG, "Failed to open fallback ringtone"); - // TODO: vibration-effect-only / no-sound LocalRingtonePlayer. - mediaPlayer.release(); - return null; - } finally { - Trace.endSection(); - } - } - - @Override - public boolean play() { - // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone - // (typically because ringer mode is vibrate). - if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes)) - == 0 && (mAudioAttributes.areHapticChannelsMuted() || !hasHapticChannels())) { - maybeStartVibration(); - return true; // Successfully played while muted. - } - synchronized (sActiveMediaPlayers) { - // We keep-alive when a mediaplayer is active, since its finalizer would stop the - // ringtone. This isn't necessary for vibrations in the vibrator service - // (i.e. maybeStartVibration in the muted case, above). - sActiveMediaPlayers.add(this); - } - - mMediaPlayer.setOnCompletionListener(this); - mMediaPlayer.start(); - if (mVolumeShaper != null) { - mVolumeShaper.apply(VolumeShaper.Operation.PLAY); - } - maybeStartVibration(); - return true; - } - - private void maybeStartVibration() { - if (mVibrationPlayer != null) { - mVibrationPlayer.play(); - } - } - - @Override - public boolean isPlaying() { - return mMediaPlayer.isPlaying(); - } - - @Override - public void stopAndRelease() { - synchronized (sActiveMediaPlayers) { - sActiveMediaPlayers.remove(this); - } - try { - mMediaPlayer.stop(); - } finally { - if (mVibrationPlayer != null) { - try { - mVibrationPlayer.stopAndRelease(); - } catch (Exception e) { - Log.e(TAG, "Exception stopping ringtone vibration", e); - } - } - if (mHapticGenerator != null) { - mHapticGenerator.release(); - } - mMediaPlayer.setOnCompletionListener(null); - mMediaPlayer.reset(); - mMediaPlayer.release(); - } - } - - @Override - public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) { - mMediaPlayer.setPreferredDevice(audioDeviceInfo); - } - - @Override - public void setLooping(boolean looping) { - boolean wasLooping = mMediaPlayer.isLooping(); - if (wasLooping == looping) { - return; - } - mMediaPlayer.setLooping(looping); - if (mVibrationPlayer != null) { - mVibrationPlayer.setLooping(looping); - } - } - - @Override - public void setHapticGeneratorEnabled(boolean enabled) { - if (mVibrationPlayer != null) { - // Ignore haptic generator changes if a vibration player is present. The decision to - // use one or the other happens before this object is constructed. - return; - } - if (enabled && mHapticGenerator == null && !hasHapticChannels()) { - mHapticGenerator = mInjectables.createHapticGenerator(mMediaPlayer); - } - if (mHapticGenerator != null) { - mHapticGenerator.setEnabled(enabled); - } - } - - @Override - public void setVolume(float volume) { - mMediaPlayer.setVolume(volume); - // no effect on vibration player - } - - @Override - public boolean hasHapticChannels() { - return mInjectables.hasHapticChannels(mMediaPlayer); - } - - @Override - public void onCompletion(MediaPlayer mp) { - synchronized (sActiveMediaPlayers) { - sActiveMediaPlayers.remove(this); - } - mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle. - // No effect on vibration: either it's looping and this callback only happens when stopped, - // or it's not looping, in which case the vibration should play to its own completion. - } - - /** A RingtonePlayer that only plays a VibrationEffect. */ - static class VibrationEffectPlayer implements RingtoneV2.RingtonePlayer { - private static final int VIBRATION_LOOP_DELAY_MS = 200; - private final VibrationEffect mVibrationEffect; - private final VibrationAttributes mVibrationAttributes; - private final Vibrator mVibrator; - private boolean mIsLooping; - private boolean mStartedVibration; - - VibrationEffectPlayer(@NonNull VibrationEffect vibrationEffect, - @NonNull AudioAttributes audioAttributes, - @NonNull Vibrator vibrator, boolean initialLooping) { - mVibrationEffect = vibrationEffect; - mVibrationAttributes = new VibrationAttributes.Builder(audioAttributes).build(); - mVibrator = vibrator; - mIsLooping = initialLooping; - } - - @Override - public boolean play() { - if (!mStartedVibration) { - try { - // Adjust the vibration effect to loop. - VibrationEffect loopAdjustedEffect = - mVibrationEffect.applyRepeatingIndefinitely( - mIsLooping, VIBRATION_LOOP_DELAY_MS); - mVibrator.vibrate(loopAdjustedEffect, mVibrationAttributes); - mStartedVibration = true; - } catch (Exception e) { - // Catch exceptions widely, because we don't want to "leak" looping sounds or - // vibrations if something goes wrong. - Log.e(TAG, "Problem starting " + (mIsLooping ? "looping " : "") + "vibration " - + "for ringtone: " + mVibrationEffect, e); - return false; - } - } - return true; - } - - @Override - public boolean isPlaying() { - return mStartedVibration; - } - - @Override - public void stopAndRelease() { - if (mStartedVibration) { - try { - mVibrator.cancel(mVibrationAttributes.getUsage()); - mStartedVibration = false; - } catch (Exception e) { - // Catch exceptions widely, because we don't want to "leak" looping sounds or - // vibrations if something goes wrong. - Log.e(TAG, "Problem stopping vibration for ringtone", e); - } - } - } - - @Override - public void setPreferredDevice(AudioDeviceInfo audioDeviceInfo) { - // no-op - } - - @Override - public void setLooping(boolean looping) { - if (looping == mIsLooping) { - return; - } - mIsLooping = looping; - if (mStartedVibration) { - if (!mIsLooping) { - // Was looping, stop looping - stopAndRelease(); - } - // Else was not looping, but can't interfere with a running vibration without - // restarting it, and don't know if it was finished. So do nothing: apps shouldn't - // toggle looping after calling play anyway. - } - } - - @Override - public void setHapticGeneratorEnabled(boolean enabled) { - // n/a - } - - @Override - public void setVolume(float volume) { - // n/a - } - - @Override - public boolean hasHapticChannels() { - return false; - } - } -} diff --git a/media/java/android/media/OWNERS b/media/java/android/media/OWNERS index 058c5be6af6c..49890c150971 100644 --- a/media/java/android/media/OWNERS +++ b/media/java/android/media/OWNERS @@ -11,6 +11,3 @@ per-file *Image* = file:/graphics/java/android/graphics/OWNERS per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=set noparent per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=file:platform/frameworks/av:/media/janitors/media_solutions_OWNERS - -# Haptics team also works on Ringtone -per-file *Ringtone* = file:/services/core/java/com/android/server/vibrator/OWNERS diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java index 8800dc865a55..e78dc31646ca 100644 --- a/media/java/android/media/Ringtone.java +++ b/media/java/android/media/Ringtone.java @@ -16,31 +16,27 @@ package android.media; -import android.Manifest; -import android.annotation.IntDef; -import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.Context; -import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; +import android.content.res.Resources.NotFoundException; import android.database.Cursor; import android.media.audiofx.HapticGenerator; import android.net.Uri; +import android.os.Binder; +import android.os.Build; import android.os.RemoteException; import android.os.Trace; -import android.os.VibrationEffect; -import android.os.Vibrator; import android.provider.MediaStore; import android.provider.MediaStore.MediaColumns; import android.provider.Settings; import android.util.Log; - import com.android.internal.annotations.VisibleForTesting; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import java.io.IOException; +import java.util.ArrayList; /** * Ringtone provides a quick method for playing a ringtone, notification, or @@ -53,39 +49,7 @@ import java.lang.annotation.RetentionPolicy; */ public class Ringtone { private static final String TAG = "Ringtone"; - - /** - * The ringtone should only play sound. Any vibration is managed externally. - * @hide - */ - public static final int MEDIA_SOUND = 1; - /** - * The ringtone should only play vibration. Any sound is managed externally. - * Requires the {@link android.Manifest.permission#VIBRATE} permission. - * @hide - */ - public static final int MEDIA_VIBRATION = 1 << 1; - /** - * The ringtone should play sound and vibration. - * @hide - */ - public static final int MEDIA_SOUND_AND_VIBRATION = MEDIA_SOUND | MEDIA_VIBRATION; - - // This is not a public value, because apps shouldn't enable "all" media - that wouldn't be - // safe if new media types were added. - static final int MEDIA_ALL = MEDIA_SOUND | MEDIA_VIBRATION; - - /** - * Declares the types of media that this Ringtone is allowed to play. - * @hide - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = "MEDIA_", value = { - MEDIA_SOUND, - MEDIA_VIBRATION, - MEDIA_SOUND_AND_VIBRATION, - }) - public @interface RingtoneMedia {} + private static final boolean LOGD = true; private static final String[] MEDIA_COLUMNS = new String[] { MediaStore.Audio.Media._ID, @@ -95,70 +59,51 @@ public class Ringtone { private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR " + MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')"; - // Flag-selected ringtone implementation to use. - private final ApiInterface mApiImpl; + // keep references on active Ringtones until stopped or completion listener called. + private static final ArrayList<Ringtone> sActiveRingtones = new ArrayList<Ringtone>(); - /** {@hide} */ - @UnsupportedAppUsage - public Ringtone(Context context, boolean allowRemote) { - mApiImpl = new RingtoneV1(context, allowRemote); - } + private final Context mContext; + private final AudioManager mAudioManager; + private VolumeShaper.Configuration mVolumeShaperConfig; + private VolumeShaper mVolumeShaper; /** - * Constructor for legacy V1 initialization paths using non-public APIs on RingtoneV1. + * Flag indicating if we're allowed to fall back to remote playback using + * {@link #mRemotePlayer}. Typically this is false when we're the remote + * player and there is nobody else to delegate to. */ - private Ringtone(RingtoneV1 ringtoneV1) { - mApiImpl = ringtoneV1; - } + private final boolean mAllowRemote; + private final IRingtonePlayer mRemotePlayer; + private final Binder mRemoteToken; - private Ringtone(Builder builder, @Ringtone.RingtoneMedia int effectiveEnabledMedia, - @NonNull AudioAttributes effectiveAudioAttributes, - @Nullable VibrationEffect effectiveVibrationEffect, - boolean effectiveHapticGeneratorEnabled) { - mApiImpl = new RingtoneV2(builder.mContext, builder.mInjectables, builder.mAllowRemote, - effectiveEnabledMedia, builder.mUri, effectiveAudioAttributes, - builder.mUseExactAudioAttributes, builder.mVolumeShaperConfig, - builder.mPreferBuiltinDevice, builder.mInitialSoundVolume, builder.mLooping, - effectiveHapticGeneratorEnabled, effectiveVibrationEffect); - } + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private MediaPlayer mLocalPlayer; + private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener(); + private HapticGenerator mHapticGenerator; - /** - * Temporary V1 constructor for legacy V1 paths with audio attributes. - * @hide - */ - public static Ringtone createV1WithCustomAudioAttributes( - Context context, AudioAttributes audioAttributes, Uri uri, - VolumeShaper.Configuration volumeShaperConfig, boolean allowRemote) { - RingtoneV1 ringtoneV1 = new RingtoneV1(context, allowRemote); - ringtoneV1.setAudioAttributesField(audioAttributes); - ringtoneV1.setUri(uri, volumeShaperConfig); - ringtoneV1.reinitializeActivePlayer(); - return new Ringtone(ringtoneV1); - } - - /** - * Temporary V1 constructor for legacy V1 paths with stream type. - * @hide - */ - public static Ringtone createV1WithCustomStreamType( - Context context, int streamType, Uri uri, - VolumeShaper.Configuration volumeShaperConfig) { - RingtoneV1 ringtoneV1 = new RingtoneV1(context, /* allowRemote= */ true); - if (streamType >= 0) { - ringtoneV1.setStreamType(streamType); - } - ringtoneV1.setUri(uri, volumeShaperConfig); - if (!ringtoneV1.reinitializeActivePlayer()) { - Log.e(TAG, "Failed to open ringtone " + uri); - return null; - } - return new Ringtone(ringtoneV1); - } + @UnsupportedAppUsage + private Uri mUri; + private String mTitle; + + private AudioAttributes mAudioAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(); + private boolean mPreferBuiltinDevice; + // playback properties, use synchronized with mPlaybackSettingsLock + private boolean mIsLooping = false; + private float mVolume = 1.0f; + private boolean mHapticGeneratorEnabled = false; + private final Object mPlaybackSettingsLock = new Object(); - /** @hide */ - @RingtoneMedia - public int getEnabledMedia() { - return mApiImpl.getEnabledMedia(); + /** {@hide} */ + @UnsupportedAppUsage + public Ringtone(Context context, boolean allowRemote) { + mContext = context; + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + mAllowRemote = allowRemote; + mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null; + mRemoteToken = allowRemote ? new Binder() : null; } /** @@ -169,7 +114,10 @@ public class Ringtone { */ @Deprecated public void setStreamType(int streamType) { - mApiImpl.setStreamType(streamType); + PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", "setStreamType()"); + setAudioAttributes(new AudioAttributes.Builder() + .setInternalLegacyStreamType(streamType) + .build()); } /** @@ -181,7 +129,7 @@ public class Ringtone { */ @Deprecated public int getStreamType() { - return mApiImpl.getStreamType(); + return AudioAttributes.toLegacyStreamType(mAudioAttributes); } /** @@ -190,45 +138,54 @@ public class Ringtone { */ public void setAudioAttributes(AudioAttributes attributes) throws IllegalArgumentException { - mApiImpl.setAudioAttributes(attributes); + setAudioAttributesField(attributes); + // The audio attributes have to be set before the media player is prepared. + // Re-initialize it. + setUri(mUri, mVolumeShaperConfig); + createLocalMediaPlayer(); } /** - * Returns the vibration effect that this ringtone was created with, if vibration is enabled. - * Otherwise, returns null. + * Same as {@link #setAudioAttributes(AudioAttributes)} except this one does not create + * the media player. * @hide */ - @Nullable - public VibrationEffect getVibrationEffect() { - return mApiImpl.getVibrationEffect(); - } - - /** @hide */ - @VisibleForTesting - public boolean getPreferBuiltinDevice() { - return mApiImpl.getPreferBuiltinDevice(); + public void setAudioAttributesField(@Nullable AudioAttributes attributes) { + if (attributes == null) { + throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone"); + } + mAudioAttributes = attributes; } - /** @hide */ - @VisibleForTesting - public VolumeShaper.Configuration getVolumeShaperConfig() { - return mApiImpl.getVolumeShaperConfig(); + /** + * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is + * the one on which outgoing audio for SIM calls is played. + * + * @param audioManager the audio manage. + * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if + * none can be found. + */ + private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) { + AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); + for (AudioDeviceInfo device : deviceList) { + if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { + return device; + } + } + return null; } /** - * Returns whether this player is local only, or can defer to the remote player. The - * result may differ from the builder if there is no remote player available at all. + * Sets the preferred device of the ringtong playback to the built-in device. + * * @hide */ - @VisibleForTesting - public boolean isLocalOnly() { - return mApiImpl.isLocalOnly(); - } - - /** @hide */ - @VisibleForTesting - public boolean isUsingRemotePlayer() { - return mApiImpl.isUsingRemotePlayer(); + public boolean preferBuiltinDevice(boolean enable) { + mPreferBuiltinDevice = enable; + if (mLocalPlayer == null) { + return true; + } + return mLocalPlayer.setPreferredDevice(getBuiltinDevice(mAudioManager)); } /** @@ -237,16 +194,76 @@ public class Ringtone { * false if it did not succeed and can't be tried remotely. * @hide */ - public boolean reinitializeActivePlayer() { - return mApiImpl.reinitializeActivePlayer(); + public boolean createLocalMediaPlayer() { + Trace.beginSection("createLocalMediaPlayer"); + if (mUri == null) { + Log.e(TAG, "Could not create media player as no URI was provided."); + return mAllowRemote && mRemotePlayer != null; + } + destroyLocalPlayer(); + // try opening uri locally before delegating to remote player + mLocalPlayer = new MediaPlayer(); + try { + mLocalPlayer.setDataSource(mContext, mUri); + mLocalPlayer.setAudioAttributes(mAudioAttributes); + mLocalPlayer.setPreferredDevice( + mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null); + synchronized (mPlaybackSettingsLock) { + applyPlaybackProperties_sync(); + } + if (mVolumeShaperConfig != null) { + mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig); + } + mLocalPlayer.prepare(); + + } catch (SecurityException | IOException e) { + destroyLocalPlayer(); + if (!mAllowRemote) { + Log.w(TAG, "Remote playback not allowed: " + e); + } + } + + if (LOGD) { + if (mLocalPlayer != null) { + Log.d(TAG, "Successfully created local player"); + } else { + Log.d(TAG, "Problem opening; delegating to remote player"); + } + } + Trace.endSection(); + return mLocalPlayer != null || (mAllowRemote && mRemotePlayer != null); } /** * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone. + * If the ringtone has not been created, it will load based on URI provided at {@link #setUri} + * and if not URI has been set, it will assume no haptic channels are present. * @hide */ public boolean hasHapticChannels() { - return mApiImpl.hasHapticChannels(); + // FIXME: support remote player, or internalize haptic channels support and remove entirely. + try { + android.os.Trace.beginSection("Ringtone.hasHapticChannels"); + if (mLocalPlayer != null) { + for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) { + if (trackInfo.hasHapticChannels()) { + return true; + } + } + } + } finally { + android.os.Trace.endSection(); + } + return false; + } + + /** + * Returns whether a local player has been created for this ringtone. + * @hide + */ + @VisibleForTesting + public boolean hasLocalPlayer() { + return mLocalPlayer != null; } /** @@ -255,7 +272,7 @@ public class Ringtone { * {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set. */ public AudioAttributes getAudioAttributes() { - return mApiImpl.getAudioAttributes(); + return mAudioAttributes; } /** @@ -263,7 +280,10 @@ public class Ringtone { * @param looping whether to loop or not. */ public void setLooping(boolean looping) { - mApiImpl.setLooping(looping); + synchronized (mPlaybackSettingsLock) { + mIsLooping = looping; + applyPlaybackProperties_sync(); + } } /** @@ -271,7 +291,9 @@ public class Ringtone { * @return true if this player loops when playing. */ public boolean isLooping() { - return mApiImpl.isLooping(); + synchronized (mPlaybackSettingsLock) { + return mIsLooping; + } } /** @@ -280,7 +302,12 @@ public class Ringtone { * corresponds to no attenuation being applied. */ public void setVolume(float volume) { - mApiImpl.setVolume(volume); + synchronized (mPlaybackSettingsLock) { + if (volume < 0.0f) { volume = 0.0f; } + if (volume > 1.0f) { volume = 1.0f; } + mVolume = volume; + applyPlaybackProperties_sync(); + } } /** @@ -288,7 +315,9 @@ public class Ringtone { * @return a value between 0.0f and 1.0f. */ public float getVolume() { - return mApiImpl.getVolume(); + synchronized (mPlaybackSettingsLock) { + return mVolume; + } } /** @@ -299,7 +328,14 @@ public class Ringtone { * @see android.media.audiofx.HapticGenerator#isAvailable() */ public boolean setHapticGeneratorEnabled(boolean enabled) { - return mApiImpl.setHapticGeneratorEnabled(enabled); + if (!HapticGenerator.isAvailable()) { + return false; + } + synchronized (mPlaybackSettingsLock) { + mHapticGeneratorEnabled = enabled; + applyPlaybackProperties_sync(); + } + return true; } /** @@ -307,7 +343,35 @@ public class Ringtone { * @return true if the HapticGenerator is enabled. */ public boolean isHapticGeneratorEnabled() { - return mApiImpl.isHapticGeneratorEnabled(); + synchronized (mPlaybackSettingsLock) { + return mHapticGeneratorEnabled; + } + } + + /** + * Must be called synchronized on mPlaybackSettingsLock + */ + private void applyPlaybackProperties_sync() { + if (mLocalPlayer != null) { + mLocalPlayer.setVolume(mVolume); + mLocalPlayer.setLooping(mIsLooping); + if (mHapticGenerator == null && mHapticGeneratorEnabled) { + mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId()); + } + if (mHapticGenerator != null) { + mHapticGenerator.setEnabled(mHapticGeneratorEnabled); + } + } else if (mAllowRemote && (mRemotePlayer != null)) { + try { + mRemotePlayer.setPlaybackProperties( + mRemoteToken, mVolume, mIsLooping, mHapticGeneratorEnabled); + } catch (RemoteException e) { + Log.w(TAG, "Problem setting playback properties: ", e); + } + } else { + Log.w(TAG, + "Neither local nor remote player available when applying playback properties"); + } } /** @@ -317,7 +381,8 @@ public class Ringtone { * @param context A context used for querying. */ public String getTitle(Context context) { - return mApiImpl.getTitle(context); + if (mTitle != null) return mTitle; + return mTitle = getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote); } /** @@ -391,379 +456,215 @@ public class Ringtone { return title; } - /** {@hide} */ - @UnsupportedAppUsage - public Uri getUri() { - return mApiImpl.getUri(); - } - - /** - * Plays the ringtone. - */ - public void play() { - mApiImpl.play(); - } - /** - * Stops a playing ringtone. + * Set {@link Uri} to be used for ringtone playback. + * {@link IRingtonePlayer}. + * + * @hide */ - public void stop() { - mApiImpl.stop(); + @UnsupportedAppUsage + public void setUri(Uri uri) { + setUri(uri, null); } /** - * Whether this ringtone is currently playing. - * - * @return True if playing, false otherwise. + * @hide */ - public boolean isPlaying() { - return mApiImpl.isPlaying(); + public void setVolumeShaperConfig(@Nullable VolumeShaper.Configuration volumeShaperConfig) { + mVolumeShaperConfig = volumeShaperConfig; } /** - * Build a {@link Ringtone} to easily play sounds for ringtones, alarms and notifications. + * Set {@link Uri} to be used for ringtone playback. Attempts to open + * locally, otherwise will delegate playback to remote + * {@link IRingtonePlayer}. Add {@link VolumeShaper} if required. * - * TODO: when un-hide, deprecate Ringtone: setAudioAttributes, setLooping, - * setHapticGeneratorEnabled (no-effect if MEDIA_VIBRATION), - * static RingtoneManager.getRingtone. * @hide */ - public static final class Builder { - private final Context mContext; - private final int mEnabledMedia; - private Uri mUri; - private final AudioAttributes mAudioAttributes; - private boolean mUseExactAudioAttributes = false; - // Not a static default since it doesn't really need to be in memory forever. - private Injectables mInjectables = new Injectables(); - private VolumeShaper.Configuration mVolumeShaperConfig; - private boolean mPreferBuiltinDevice = false; - private boolean mAllowRemote = true; - private boolean mHapticGeneratorEnabled = false; - private float mInitialSoundVolume = 1.0f; - private boolean mLooping = false; - private VibrationEffect mVibrationEffect; - - /** - * Constructs a builder to play the given media types from the mediaUri. If the mediaUri - * is null (for example, an unset-setting), then fallback logic will dictate what plays. - * - * <p>When built, if the ringtone is already known to be a no-op, such as explicitly - * silent, then the {@link #build} may return null. - * - * @param context The context for playing the ringtone. - * @param enabledMedia Which media to play. Media not included is implicitly muted. Device - * settings such as volume and vibrate-only may also affect which - * media is played. - * @param audioAttributes The attributes to use for playback, which affects the volumes and - * settings that are applied. - */ - public Builder(@NonNull Context context, @RingtoneMedia int enabledMedia, - @NonNull AudioAttributes audioAttributes) { - mContext = context; - mEnabledMedia = enabledMedia; - mAudioAttributes = audioAttributes; + public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) { + mVolumeShaperConfig = volumeShaperConfig; + mUri = uri; + if (mUri == null) { + destroyLocalPlayer(); } + } - /** - * Inject test intercepters for static methods. - * @hide - */ - @NonNull - public Builder setInjectables(Injectables injectables) { - mInjectables = injectables; - return this; - } + /** {@hide} */ + @UnsupportedAppUsage + public Uri getUri() { + return mUri; + } - /** - * The uri for the ringtone media to play. This is typically a user's preference for the - * sound. If null, then it is treated as though the user's preference is unset and - * fallback behavior, such as using the default ringtone setting, are used instead. - * - * When sound media is enabled, this is assumed to be a sound URI. - */ - @NonNull - public Builder setUri(@Nullable Uri uri) { - mUri = uri; - return this; + /** + * Plays the ringtone. + */ + public void play() { + if (mLocalPlayer != null) { + // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone + // (typically because ringer mode is vibrate). + if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes)) + != 0) { + startLocalPlayer(); + } else if (!mAudioAttributes.areHapticChannelsMuted() && hasHapticChannels()) { + // is haptic only ringtone + startLocalPlayer(); + } + } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != null)) { + final Uri canonicalUri = mUri.getCanonicalUri(); + final boolean looping; + final float volume; + synchronized (mPlaybackSettingsLock) { + looping = mIsLooping; + volume = mVolume; + } + try { + mRemotePlayer.playWithVolumeShaping(mRemoteToken, canonicalUri, mAudioAttributes, + volume, looping, mVolumeShaperConfig); + } catch (RemoteException e) { + if (!playFallbackRingtone()) { + Log.w(TAG, "Problem playing ringtone: " + e); + } + } + } else { + if (!playFallbackRingtone()) { + Log.w(TAG, "Neither local nor remote playback available"); + } } + } - /** - * Sets the VibrationEffect to use if vibration is enabled on this ringtone. The caller - * should use {@link android.os.Vibrator#areVibrationFeaturesSupported} to ensure - * that the effect is usable on this device, otherwise system defaults will be used. - * - * <p>Vibration will only happen if the Builder was created with media type - * {@link Ringtone#MEDIA_VIBRATION} or {@link Ringtone#MEDIA_SOUND_AND_VIBRATION}, and - * the application has the {@link android.Manifest.permission#VIBRATE} permission. - * - * <p>If the Ringtone is looping when it is played, then the VibrationEffect will be - * modified to loop. Similarly, if the ringtone is not looping, a repeating - * VibrationEffect will be modified to be non-repeating when the ringtone is played. Calls - * to {@link Ringtone#setLooping} after the ringtone has started playing will stop a looping - * vibration, but has no effect otherwise: specifically it will not restart vibration. - */ - @NonNull - public Builder setVibrationEffect(@NonNull VibrationEffect effect) { - mVibrationEffect = effect; - return this; + /** + * Stops a playing ringtone. + */ + public void stop() { + if (mLocalPlayer != null) { + destroyLocalPlayer(); + } else if (mAllowRemote && (mRemotePlayer != null)) { + try { + mRemotePlayer.stop(mRemoteToken); + } catch (RemoteException e) { + Log.w(TAG, "Problem stopping ringtone: " + e); + } } + } - /** - * Sets whether the resulting ringtone should loop until {@link Ringtone#stop()} is called, - * or just play once. - */ - @NonNull - public Builder setLooping(boolean looping) { - mLooping = looping; - return this; + private void destroyLocalPlayer() { + if (mLocalPlayer != null) { + if (mHapticGenerator != null) { + mHapticGenerator.release(); + mHapticGenerator = null; + } + mLocalPlayer.setOnCompletionListener(null); + mLocalPlayer.reset(); + mLocalPlayer.release(); + mLocalPlayer = null; + mVolumeShaper = null; + synchronized (sActiveRingtones) { + sActiveRingtones.remove(this); + } } + } - /** - * Sets the VolumeShaper.Configuration to apply to the ringtone. - * @hide - */ - @NonNull - public Builder setVolumeShaperConfig( - @Nullable VolumeShaper.Configuration volumeShaperConfig) { - mVolumeShaperConfig = volumeShaperConfig; - return this; + private void startLocalPlayer() { + if (mLocalPlayer == null) { + return; } - - /** - * Whether to enable or disable the haptic generator. - * @hide - */ - @NonNull - public Builder setEnableHapticGenerator(boolean enabled) { - // Note that this property is mutable (but deprecated) on the Ringtone class itself. - mHapticGeneratorEnabled = enabled; - return this; + synchronized (sActiveRingtones) { + sActiveRingtones.add(this); } - - /** - * Sets the initial sound volume for the ringtone. - */ - @NonNull - public Builder setInitialSoundVolume(float initialSoundVolume) { - mInitialSoundVolume = initialSoundVolume; - return this; + mLocalPlayer.setOnCompletionListener(mCompletionListener); + mLocalPlayer.start(); + if (mVolumeShaper != null) { + mVolumeShaper.apply(VolumeShaper.Operation.PLAY); } + } - /** - * Sets the preferred device of the ringtone playback to the built-in device. This is - * only for use by the system server with known-good Uris. - * @hide - */ - @NonNull - public Builder setPreferBuiltinDevice() { - mPreferBuiltinDevice = true; - mAllowRemote = false; // Already in system. - return this; + /** + * Whether this ringtone is currently playing. + * + * @return True if playing, false otherwise. + */ + public boolean isPlaying() { + if (mLocalPlayer != null) { + return mLocalPlayer.isPlaying(); + } else if (mAllowRemote && (mRemotePlayer != null)) { + try { + return mRemotePlayer.isPlaying(mRemoteToken); + } catch (RemoteException e) { + Log.w(TAG, "Problem checking ringtone: " + e); + return false; + } + } else { + Log.w(TAG, "Neither local nor remote playback available"); + return false; } + } - /** - * Indicates that {@link AudioAttributes#areHapticChannelsMuted()} on the builder's - * AudioAttributes should not be overridden. This is used to enable legacy behavior of - * calling {@link Ringtone#setAudioAttributes} on an already-created ringtone, and can in - * turn cause vibration during a "sound-only" session or can suppress audio-coupled - * haptics that would usually take priority (therefore potentially falling back to - * the VibrationEffect or system defaults). - * - * <p>Without this setting, the haptic channels will be automatically muted or not by the - * Ringtone according to whether vibration is enabled or not. - * - * <p>This is for internal-use only. New applications should configure the vibration - * behavior explicitly with the (TODO: future RingtoneSetting.setVibrationSource). - * Handling haptic channels outside Ringtone leads to extra loads of the sound uri. - * @hide - */ - @NonNull - public Builder setUseExactAudioAttributes(boolean useExactAttrs) { - mUseExactAudioAttributes = useExactAttrs; - return this; + private boolean playFallbackRingtone() { + int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes); + if (mAudioManager.getStreamVolume(streamType) == 0) { + return false; } - - /** - * Prevent fallback to the remote service. This is primarily intended for use within the - * remote IRingtonePlayer service itself, to avoid loops. - * @hide - */ - @NonNull - public Builder setLocalOnly() { - mAllowRemote = false; - return this; + int ringtoneType = RingtoneManager.getDefaultType(mUri); + if (ringtoneType != -1 && + RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) { + Log.w(TAG, "not playing fallback for " + mUri); + return false; } - - private boolean isVibrationEnabledAndAvailable() { - if ((mEnabledMedia & MEDIA_VIBRATION) == 0) { - return false; - } - Vibrator vibrator = mContext.getSystemService(Vibrator.class); - if (!vibrator.hasVibrator()) { - return false; - } - if (mContext.checkSelfPermission(Manifest.permission.VIBRATE) - != PackageManager.PERMISSION_GRANTED) { - Log.w(TAG, "Ringtone requests vibration enabled, but no VIBRATE permission"); + // Default ringtone, try fallback ringtone. + try { + AssetFileDescriptor afd = mContext.getResources().openRawResourceFd( + com.android.internal.R.raw.fallbackring); + if (afd == null) { + Log.e(TAG, "Could not load fallback ringtone"); return false; } - return true; - } - - /** - * Returns the built Ringtone, or null if there was a problem loading the Uri and there - * are no fallback options available. - */ - @Nullable - public Ringtone build() { - @Ringtone.RingtoneMedia int effectiveEnabledMedia = mEnabledMedia; - VibrationEffect effectiveVibrationEffect = mVibrationEffect; - - // Normalize media to that supported on this SDK level. - if (effectiveEnabledMedia != (effectiveEnabledMedia & MEDIA_ALL)) { - Log.e(TAG, "Unsupported media type: " + effectiveEnabledMedia); - effectiveEnabledMedia = effectiveEnabledMedia & MEDIA_ALL; - } - final boolean effectiveHapticGenerator; - final boolean hapticChannelsSupported; - AudioAttributes effectiveAudioAttributes = mAudioAttributes; - final boolean hapticChannelsMuted = mAudioAttributes.areHapticChannelsMuted(); - if (!isVibrationEnabledAndAvailable()) { - // Vibration isn't active: turn off everything that might cause extra work. - effectiveEnabledMedia &= ~MEDIA_VIBRATION; - effectiveHapticGenerator = false; - effectiveVibrationEffect = null; - if (!mUseExactAudioAttributes && !hapticChannelsMuted) { - effectiveAudioAttributes = new AudioAttributes.Builder(effectiveAudioAttributes) - .setHapticChannelsMuted(true) - .build(); - } + mLocalPlayer = new MediaPlayer(); + if (afd.getDeclaredLength() < 0) { + mLocalPlayer.setDataSource(afd.getFileDescriptor()); } else { - // Vibration is active. - effectiveHapticGenerator = - mHapticGeneratorEnabled && mInjectables.isHapticGeneratorAvailable(); - hapticChannelsSupported = mInjectables.isHapticPlaybackSupported(); - // Haptic channels are preferred if they are available, and not explicitly muted. - // We won't know if haptic channels are available until loading the media player, - // and since the media player needs to be reset to change audio attributes, then - // we proactively enable the channels - it won't matter if they aren't present. - if (!mUseExactAudioAttributes) { - boolean shouldBeMuted = effectiveHapticGenerator || !hapticChannelsSupported; - if (shouldBeMuted != hapticChannelsMuted) { - effectiveAudioAttributes = - new AudioAttributes.Builder(effectiveAudioAttributes) - .setHapticChannelsMuted(shouldBeMuted) - .build(); - } - } - // If no contextual vibration, then try loading the default one for the URI. - if (mVibrationEffect == null && mUri != null) { - effectiveVibrationEffect = VibrationEffect.get(mUri, mContext); - } + mLocalPlayer.setDataSource(afd.getFileDescriptor(), + afd.getStartOffset(), + afd.getDeclaredLength()); } - try { - Ringtone ringtone = new Ringtone(this, effectiveEnabledMedia, - effectiveAudioAttributes, effectiveVibrationEffect, - effectiveHapticGenerator); - if (ringtone.reinitializeActivePlayer()) { - return ringtone; - } else { - Log.e(TAG, "Failed to open ringtone " + mUri); - return null; - } - } catch (Exception ex) { - // Catching Exception isn't great, but was done in the old - // RingtoneManager.getRingtone and hides errors like DocumentsProvider throwing - // IllegalArgumentException instead of FileNotFoundException, and also robolectric - // failures when ShadowMediaPlayer wasn't pre-informed of the ringtone. - Log.e(TAG, "Failed while opening ringtone " + mUri, ex); - return null; + mLocalPlayer.setAudioAttributes(mAudioAttributes); + synchronized (mPlaybackSettingsLock) { + applyPlaybackProperties_sync(); } + if (mVolumeShaperConfig != null) { + mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig); + } + mLocalPlayer.prepare(); + startLocalPlayer(); + afd.close(); + } catch (IOException ioe) { + destroyLocalPlayer(); + Log.e(TAG, "Failed to open fallback ringtone"); + return false; + } catch (NotFoundException nfe) { + Log.e(TAG, "Fallback ringtone does not exist"); + return false; } + return true; } - /** - * Interface for intercepting static methods and constructors, for unit testing only. - * @hide - */ - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) - public static class Injectables { - /** Intercept {@code new MediaPlayer()}. */ - @NonNull - public MediaPlayer newMediaPlayer() { - return new MediaPlayer(); - } - - /** Intercept {@link HapticGenerator#isAvailable}. */ - public boolean isHapticGeneratorAvailable() { - return HapticGenerator.isAvailable(); - } - - /** - * Intercept {@link HapticGenerator#create} using - * {@link MediaPlayer#getAudioSessionId()} from the given media player. - */ - @Nullable - public HapticGenerator createHapticGenerator(@NonNull MediaPlayer mediaPlayer) { - return HapticGenerator.create(mediaPlayer.getAudioSessionId()); - } + void setTitle(String title) { + mTitle = title; + } - /** Returns the result of {@link AudioManager#isHapticPlaybackSupported()}. */ - public boolean isHapticPlaybackSupported() { - return AudioManager.isHapticPlaybackSupported(); + @Override + protected void finalize() { + if (mLocalPlayer != null) { + mLocalPlayer.release(); } + } - /** - * Returns whether the MediaPlayer tracks have haptic channels. This is the same as - * AudioManager.hasHapticChannels, except it uses an already prepared MediaPlayer to avoid - * loading the metadata a second time. - */ - public boolean hasHapticChannels(MediaPlayer mp) { - try { - Trace.beginSection("Ringtone.hasHapticChannels"); - for (MediaPlayer.TrackInfo trackInfo : mp.getTrackInfo()) { - if (trackInfo.hasHapticChannels()) { - return true; - } - } - } finally { - Trace.endSection(); + class MyOnCompletionListener implements MediaPlayer.OnCompletionListener { + @Override + public void onCompletion(MediaPlayer mp) { + synchronized (sActiveRingtones) { + sActiveRingtones.remove(Ringtone.this); } - return false; + mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle. } - - } - - /** - * Interface for alternative Ringtone implementations. See the public Ringtone methods that - * delegate to these for documentation. - * @hide - */ - interface ApiInterface { - void setStreamType(int streamType); - int getStreamType(); - void setAudioAttributes(AudioAttributes attributes); - boolean getPreferBuiltinDevice(); - VolumeShaper.Configuration getVolumeShaperConfig(); - boolean isLocalOnly(); - boolean isUsingRemotePlayer(); - boolean reinitializeActivePlayer(); - boolean hasHapticChannels(); - AudioAttributes getAudioAttributes(); - void setLooping(boolean looping); - boolean isLooping(); - void setVolume(float volume); - float getVolume(); - boolean setHapticGeneratorEnabled(boolean enabled); - boolean isHapticGeneratorEnabled(); - String getTitle(Context context); - Uri getUri(); - void play(); - void stop(); - boolean isPlaying(); - // V2 future-public methods - @RingtoneMedia int getEnabledMedia(); - VibrationEffect getVibrationEffect(); } } diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index b5a9ae27263f..3432b3f5995a 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -16,7 +16,7 @@ package android.media; -import android.annotation.IntDef; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -30,27 +30,24 @@ import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.UserInfo; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.StaleDataException; import android.net.Uri; +import android.os.Build; import android.os.Environment; import android.os.FileUtils; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; -import android.os.vibrator.Flags; -import android.os.vibrator.persistence.VibrationXmlParser; import android.provider.BaseColumns; import android.provider.MediaStore; import android.provider.MediaStore.Audio.AudioColumns; import android.provider.MediaStore.MediaColumns; import android.provider.Settings; import android.provider.Settings.System; -import android.text.TextUtils; import android.util.Log; import com.android.internal.database.SortCursor; @@ -61,8 +58,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -122,53 +117,6 @@ public class RingtoneManager { public static final String ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER"; /** - * Given to the ringtone picker as a string that represents the category of ringtone picker that - * should be used. This value should also be returned once a ringtone is selected. - * <p> - * The categories are: - * <li>{@link #CATEGORY_RINGTONE_PICKER_SOUND} - * <li>{@link #CATEGORY_RINGTONE_PICKER_VIBRATION} - * <li>{@link #CATEGORY_RINGTONE_PICKER_RINGTONE} - * <li>{@link Intent#CATEGORY_DEFAULT} - * - * <p> If the category is {@link Intent#CATEGORY_DEFAULT} or absent, then the picker will - * default to a sound-only ringtone picker. - * - * <p> If the selected category was not supported, then the returned category will be null. - * - * @hide - */ - public static final String EXTRA_RINGTONE_PICKER_CATEGORY = - "android.intent.extra.ringtone.RINGTONE_PICKER_CATEGORY"; - - /** - * A sound-only ringtone picker. - * - * @hide - * @see #EXTRA_RINGTONE_PICKER_CATEGORY - */ - public static final String CATEGORY_RINGTONE_PICKER_SOUND = - "android.net.category.RINGTONE_PICKER_SOUND"; - - /** - * A vibration-only ringtone picker. - * - * @hide - * @see #EXTRA_RINGTONE_PICKER_CATEGORY - */ - public static final String CATEGORY_RINGTONE_PICKER_VIBRATION = - "android.net.category.RINGTONE_PICKER_VIBRATION"; - - /** - * A combined sound and vibration ringtone picker. - * - * @hide - * @see #EXTRA_RINGTONE_PICKER_CATEGORY - */ - public static final String CATEGORY_RINGTONE_PICKER_RINGTONE = - "android.net.category.RINGTONE_PICKER_RINGTONE"; - - /** * Given to the ringtone picker as a boolean. Whether to show an item for * "Default". * @@ -209,18 +157,6 @@ public class RingtoneManager { */ public static final String EXTRA_RINGTONE_EXISTING_URI = "android.intent.extra.ringtone.EXISTING_URI"; - - /** - * Similar to #EXTRA_RINGTONE_EXISTING_URI but the {@link Uri} can include both sound and - * vibration. - * <p>This can include silent sound/vibration explicitly by setting that part of the URI to - * null. - * - * @hide - * @see #ACTION_RINGTONE_PICKER - */ - public static final String EXTRA_RINGTONE_EXISTING_RINGTONE_URI = - "android.intent.extra.ringtone.RINGTONE_EXISTING_RINGTONE_URI"; /** * Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the @@ -273,30 +209,21 @@ public class RingtoneManager { */ public static final String EXTRA_RINGTONE_PICKED_URI = "android.intent.extra.ringtone.PICKED_URI"; - - /** - * Declares the allowed types of media for this RingtoneManager. - * @hide - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = "MEDIA_", value = { - Ringtone.MEDIA_SOUND, - Ringtone.MEDIA_VIBRATION, - }) - public @interface MediaType {} - + // Make sure the column ordering and then ..._COLUMN_INDEX are in sync - private static final String[] MEDIA_AUDIO_COLUMNS = new String[] { + private static final String[] INTERNAL_COLUMNS = new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.TITLE_KEY, }; - private static final String[] MEDIA_VIBRATION_COLUMNS = new String[]{ - MediaStore.Files.FileColumns._ID, - MediaStore.Files.FileColumns.TITLE, + private static final String[] MEDIA_COLUMNS = new String[] { + MediaStore.Audio.Media._ID, + MediaStore.Audio.Media.TITLE, + MediaStore.Audio.Media.TITLE, + MediaStore.Audio.Media.TITLE_KEY, }; /** @@ -324,9 +251,7 @@ public class RingtoneManager { private Cursor mCursor; private int mType = TYPE_RINGTONE; - @MediaType - private int mMediaType = Ringtone.MEDIA_SOUND; - + /** * If a column (item from this list) exists in the Cursor, its value must * be true (value of 1) for the row to be returned. @@ -393,41 +318,6 @@ public class RingtoneManager { } /** - * Sets the media type that will be listed by the RingtoneManager. - * - * <p>This method should be called before calling {@link RingtoneManager#getCursor()}. - * - * @hide - */ - public void setMediaType(@MediaType int mediaType) { - if (mCursor != null) { - throw new IllegalStateException( - "Setting media should be done before calling getCursor()."); - } - - switch (mediaType) { - case Ringtone.MEDIA_SOUND: - case Ringtone.MEDIA_VIBRATION: - mMediaType = mediaType; - break; - default: - throw new IllegalArgumentException("Unsupported media type " + mediaType); - } - } - - /** - * Returns the RingtoneManagers media type. - * - * @return the media type. - * @see #setMediaType - * @hide - */ - @MediaType - public int getMediaType() { - return mMediaType; - } - - /** * Sets which type(s) of ringtones will be listed by this. * * @param type The type(s), one or more of {@link #TYPE_RINGTONE}, @@ -465,25 +355,6 @@ public class RingtoneManager { } } - /** @hide */ - @NonNull - public static AudioAttributes getDefaultAudioAttributes(int ringtoneType) { - AudioAttributes.Builder builder = new AudioAttributes.Builder(); - switch (ringtoneType) { - case TYPE_ALARM: - builder.setUsage(AudioAttributes.USAGE_ALARM); - break; - case TYPE_NOTIFICATION: - builder.setUsage(AudioAttributes.USAGE_NOTIFICATION); - break; - default: // ringtone or all - builder.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); - break; - } - builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION); - return builder.build(); - } - /** * Whether retrieving another {@link Ringtone} will stop playing the * previously retrieved {@link Ringtone}. @@ -564,19 +435,19 @@ public class RingtoneManager { return mCursor; } - ArrayList<Cursor> cursors = new ArrayList<>(); - - cursors.add(queryMediaStore(/* internal= */ true)); - cursors.add(queryMediaStore(/* internal= */ false)); + ArrayList<Cursor> ringtoneCursors = new ArrayList<Cursor>(); + ringtoneCursors.add(getInternalRingtones()); + ringtoneCursors.add(getMediaRingtones()); if (mIncludeParentRingtones) { Cursor parentRingtonesCursor = getParentProfileRingtones(); if (parentRingtonesCursor != null) { - cursors.add(parentRingtonesCursor); + ringtoneCursors.add(parentRingtonesCursor); } } - return mCursor = new SortCursor(cursors.toArray(new Cursor[cursors.size()]), - getSortOrderForMedia(mMediaType)); + + return mCursor = new SortCursor(ringtoneCursors.toArray(new Cursor[ringtoneCursors.size()]), + MediaStore.Audio.Media.DEFAULT_SORT_ORDER); } private Cursor getParentProfileRingtones() { @@ -588,7 +459,9 @@ public class RingtoneManager { // We don't need to re-add the internal ringtones for the work profile since // they are the same as the personal profile. We just need the external // ringtones. - return queryMediaStore(parentContext, /* internal= */ false); + final Cursor res = getMediaRingtones(parentContext); + return new ExternalRingtonesCursorWrapper(res, ContentProvider.maybeAddUserId( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, parentInfo.id)); } } return null; @@ -606,32 +479,11 @@ public class RingtoneManager { mPreviousRingtone.stop(); } - Ringtone ringtone; - Uri positionUri = getRingtoneUri(position); - if (Flags.hapticsCustomizationRingtoneV2Enabled()) { - mPreviousRingtone = new Ringtone.Builder( - mContext, mMediaType, getDefaultAudioAttributes(mType)) - .setUri(positionUri) - .build(); - } else { - mPreviousRingtone = createRingtoneV1WithStreamType(mContext, positionUri, - inferStreamType(), /* volumeShaperConfig= */ null); - } + mPreviousRingtone = + getRingtone(mContext, getRingtoneUri(position), inferStreamType(), true); return mPreviousRingtone; } - private static Ringtone createRingtoneV1WithStreamType( - final Context context, Uri ringtoneUri, int streamType, - @Nullable VolumeShaper.Configuration volumeShaperConfig) { - try { - return Ringtone.createV1WithCustomStreamType(context, streamType, ringtoneUri, - volumeShaperConfig); - } catch (Exception ex) { - Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex); - } - return null; - } - /** * Gets a {@link Uri} for the ringtone at the given position in the {@link Cursor}. * @@ -783,13 +635,11 @@ public class RingtoneManager { */ public static Uri getValidRingtoneUri(Context context) { final RingtoneManager rm = new RingtoneManager(context); - - Uri uri = getValidRingtoneUriFromCursorAndClose(context, - rm.queryMediaStore(/* internal= */ true)); + + Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones()); if (uri == null) { - uri = getValidRingtoneUriFromCursorAndClose(context, - rm.queryMediaStore(/* internal= */ false)); + uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones()); } return uri; @@ -810,26 +660,28 @@ public class RingtoneManager { } } - private Cursor queryMediaStore(boolean internal) { - return queryMediaStore(mContext, internal); + @UnsupportedAppUsage + private Cursor getInternalRingtones() { + final Cursor res = query( + MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS, + constructBooleanTrueWhereClause(mFilterColumns), + null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); + return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.INTERNAL_CONTENT_URI); } - private Cursor queryMediaStore(Context context, boolean internal) { - Uri contentUri = getContentUriForMedia(mMediaType, internal); - String[] columns = - mMediaType == Ringtone.MEDIA_VIBRATION ? MEDIA_VIBRATION_COLUMNS - : MEDIA_AUDIO_COLUMNS; - String whereClause = getWhereClauseForMedia(mMediaType, mFilterColumns); - String sortOrder = getSortOrderForMedia(mMediaType); - - Cursor cursor = query(contentUri, columns, whereClause, /* selectionArgs= */ null, - sortOrder, context); - - if (context.getUserId() != mContext.getUserId()) { - contentUri = ContentProvider.maybeAddUserId(contentUri, context.getUserId()); - } + private Cursor getMediaRingtones() { + final Cursor res = getMediaRingtones(mContext); + return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); + } - return new ExternalRingtonesCursorWrapper(cursor, contentUri); + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private Cursor getMediaRingtones(Context context) { + // MediaStore now returns ringtones on other storage devices, even when + // we don't have storage or audio permissions + return query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS, + constructBooleanTrueWhereClause(mFilterColumns), null, + MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context); } private void setFilterColumnsList(int type) { @@ -848,56 +700,6 @@ public class RingtoneManager { columns.add(MediaStore.Audio.AudioColumns.IS_ALARM); } } - - /** - * Returns the sort order for the specified media. - * - * @param media The RingtoneManager media type. - * @return The sort order column. - */ - private static String getSortOrderForMedia(@MediaType int media) { - return media == Ringtone.MEDIA_VIBRATION ? MediaStore.Files.FileColumns.TITLE - : MediaStore.Audio.Media.DEFAULT_SORT_ORDER; - } - - /** - * Returns the content URI based on the specified media and whether it's internal or external - * storage. - * - * @param media The RingtoneManager media type. - * @param internal Whether it's for internal or external storage. - * @return The media content URI. - */ - private static Uri getContentUriForMedia(@MediaType int media, boolean internal) { - switch (media) { - case Ringtone.MEDIA_VIBRATION: - return MediaStore.Files.getContentUri( - internal ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL); - case Ringtone.MEDIA_SOUND: - return internal ? MediaStore.Audio.Media.INTERNAL_CONTENT_URI - : MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - default: - throw new IllegalArgumentException("Unsupported media type " + media); - } - } - - /** - * Constructs a where clause based on the media type. This will be used to find all matching - * sound or vibration files. - * - * @param media The RingtoneManager media type. - * @param columns The columns that must be true, when media type is {@link Ringtone#MEDIA_SOUND} - * @return The where clause. - */ - private static String getWhereClauseForMedia(@MediaType int media, List<String> columns) { - // TODO(b/296213309): Filtering by ringtone-type isn't supported yet for vibrations. - if (media == Ringtone.MEDIA_VIBRATION) { - return TextUtils.formatSimple("(%s='%s')", MediaStore.Files.FileColumns.MIME_TYPE, - VibrationXmlParser.APPLICATION_VIBRATION_XML_MIME_TYPE); - } - - return constructBooleanTrueWhereClause(columns); - } /** * Constructs a where clause that consists of at least one column being 1 @@ -927,6 +729,14 @@ public class RingtoneManager { return sb.toString(); } + + private Cursor query(Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + return query(uri, projection, selection, selectionArgs, sortOrder, mContext); + } private Cursor query(Uri uri, String[] projection, @@ -954,14 +764,40 @@ public class RingtoneManager { * @return A {@link Ringtone} for the given URI, or null. */ public static Ringtone getRingtone(final Context context, Uri ringtoneUri) { - if (Flags.hapticsCustomizationRingtoneV2Enabled()) { - return new Ringtone.Builder( - context, Ringtone.MEDIA_SOUND, getDefaultAudioAttributes(-1)) - .setUri(ringtoneUri) - .build(); - } else { - return createRingtoneV1WithStreamType(context, ringtoneUri, -1, null); - } + // Don't set the stream type + return getRingtone(context, ringtoneUri, -1, true); + } + + /** + * Returns a {@link Ringtone} with {@link VolumeShaper} if required for a given sound URI. + * <p> + * If the given URI cannot be opened for any reason, this method will + * attempt to fallback on another sound. If it cannot find any, it will + * return null. + * + * @param context A context used to query. + * @param ringtoneUri The {@link Uri} of a sound or ringtone. + * @param volumeShaperConfig config for volume shaper of the ringtone if applied. + * @return A {@link Ringtone} for the given URI, or null. + * + * @hide + */ + public static Ringtone getRingtone( + final Context context, Uri ringtoneUri, + @Nullable VolumeShaper.Configuration volumeShaperConfig) { + // Don't set the stream type + return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig, true); + } + + /** + * @hide + */ + public static Ringtone getRingtone(final Context context, Uri ringtoneUri, + @Nullable VolumeShaper.Configuration volumeShaperConfig, + boolean createLocalMediaPlayer) { + // Don't set the stream type + return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig, + createLocalMediaPlayer); } /** @@ -970,23 +806,64 @@ public class RingtoneManager { public static Ringtone getRingtone(final Context context, Uri ringtoneUri, @Nullable VolumeShaper.Configuration volumeShaperConfig, AudioAttributes audioAttributes) { - // TODO: move caller(s) away from this method: inline the builder call. - if (Flags.hapticsCustomizationRingtoneV2Enabled()) { - return new Ringtone.Builder(context, Ringtone.MEDIA_SOUND, audioAttributes) - .setUri(ringtoneUri) - .setVolumeShaperConfig(volumeShaperConfig) - .setUseExactAudioAttributes(true) // May be using audio-coupled via attrs - .build(); - } else { - try { - return Ringtone.createV1WithCustomAudioAttributes(context, audioAttributes, - ringtoneUri, volumeShaperConfig, /* allowRemote= */ true); - } catch (Exception ex) { - // Match broad catching of createRingtoneV1. - Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex); + // Don't set the stream type + Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */, + volumeShaperConfig, false); + if (ringtone != null) { + ringtone.setAudioAttributesField(audioAttributes); + if (!ringtone.createLocalMediaPlayer()) { + Log.e(TAG, "Failed to open ringtone " + ringtoneUri); return null; } } + return ringtone; + } + + //FIXME bypass the notion of stream types within the class + /** + * Returns a {@link Ringtone} for a given sound URI on the given stream + * type. Normally, if you change the stream type on the returned + * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just + * an optimized route to avoid that. + * + * @param streamType The stream type for the ringtone, or -1 if it should + * not be set (and the default used instead). + * @param createLocalMediaPlayer when true, the ringtone returned will be fully + * created otherwise, it will require the caller to create the media player manually + * {@link Ringtone#createLocalMediaPlayer()} in order to play the Ringtone. + * @see #getRingtone(Context, Uri) + */ + @UnsupportedAppUsage + private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType, + boolean createLocalMediaPlayer) { + return getRingtone(context, ringtoneUri, streamType, null /* volumeShaperConfig */, + createLocalMediaPlayer); + } + + private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType, + @Nullable VolumeShaper.Configuration volumeShaperConfig, + boolean createLocalMediaPlayer) { + try { + final Ringtone r = new Ringtone(context, true); + if (streamType >= 0) { + //FIXME deprecated call + r.setStreamType(streamType); + } + + r.setVolumeShaperConfig(volumeShaperConfig); + r.setUri(ringtoneUri, volumeShaperConfig); + if (createLocalMediaPlayer) { + if (!r.createLocalMediaPlayer()) { + Log.e(TAG, "Failed to open ringtone " + ringtoneUri); + return null; + } + } + return r; + } catch (Exception ex) { + Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex); + } + + return null; } /** diff --git a/media/java/android/media/RingtoneV1.java b/media/java/android/media/RingtoneV1.java deleted file mode 100644 index 3c54d4a0d166..000000000000 --- a/media/java/android/media/RingtoneV1.java +++ /dev/null @@ -1,614 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media; - -import android.annotation.Nullable; -import android.compat.annotation.UnsupportedAppUsage; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.content.res.Resources.NotFoundException; -import android.media.audiofx.HapticGenerator; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.RemoteException; -import android.os.Trace; -import android.os.VibrationEffect; -import android.provider.MediaStore; -import android.provider.MediaStore.MediaColumns; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; - -import java.io.IOException; -import java.util.ArrayList; - -/** - * Hosts original Ringtone implementation, retained for flagging large builder+vibration features - * in RingtoneV2.java. This does not support new features in the V2 builder. - * - * Only modified methods are moved here. - * - * @hide - */ -class RingtoneV1 implements Ringtone.ApiInterface { - private static final String TAG = "RingtoneV1"; - private static final boolean LOGD = true; - - private static final String[] MEDIA_COLUMNS = new String[] { - MediaStore.Audio.Media._ID, - MediaStore.Audio.Media.TITLE - }; - /** Selection that limits query results to just audio files */ - private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR " - + MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')"; - - // keep references on active Ringtones until stopped or completion listener called. - private static final ArrayList<RingtoneV1> sActiveRingtones = new ArrayList<>(); - - private final Context mContext; - private final AudioManager mAudioManager; - private VolumeShaper.Configuration mVolumeShaperConfig; - private VolumeShaper mVolumeShaper; - - /** - * Flag indicating if we're allowed to fall back to remote playback using - * {@link #mRemotePlayer}. Typically this is false when we're the remote - * player and there is nobody else to delegate to. - */ - private final boolean mAllowRemote; - private final IRingtonePlayer mRemotePlayer; - private final Binder mRemoteToken; - - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private MediaPlayer mLocalPlayer; - private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener(); - private HapticGenerator mHapticGenerator; - - @UnsupportedAppUsage - private Uri mUri; - private String mTitle; - - private AudioAttributes mAudioAttributes = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build(); - private boolean mPreferBuiltinDevice; - // playback properties, use synchronized with mPlaybackSettingsLock - private boolean mIsLooping = false; - private float mVolume = 1.0f; - private boolean mHapticGeneratorEnabled = false; - private final Object mPlaybackSettingsLock = new Object(); - - /** {@hide} */ - @UnsupportedAppUsage - public RingtoneV1(Context context, boolean allowRemote) { - mContext = context; - mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - mAllowRemote = allowRemote; - mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null; - mRemoteToken = allowRemote ? new Binder() : null; - } - - /** - * Sets the stream type where this ringtone will be played. - * - * @param streamType The stream, see {@link AudioManager}. - * @deprecated use {@link #setAudioAttributes(AudioAttributes)} - */ - @Deprecated - public void setStreamType(int streamType) { - PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", "setStreamType()"); - setAudioAttributes(new AudioAttributes.Builder() - .setInternalLegacyStreamType(streamType) - .build()); - } - - /** - * Gets the stream type where this ringtone will be played. - * - * @return The stream type, see {@link AudioManager}. - * @deprecated use of stream types is deprecated, see - * {@link #setAudioAttributes(AudioAttributes)} - */ - @Deprecated - public int getStreamType() { - return AudioAttributes.toLegacyStreamType(mAudioAttributes); - } - - /** - * Sets the {@link AudioAttributes} for this ringtone. - * @param attributes the non-null attributes characterizing this ringtone. - */ - public void setAudioAttributes(AudioAttributes attributes) - throws IllegalArgumentException { - setAudioAttributesField(attributes); - // The audio attributes have to be set before the media player is prepared. - // Re-initialize it. - setUri(mUri, mVolumeShaperConfig); - reinitializeActivePlayer(); - } - - /** - * Same as {@link #setAudioAttributes(AudioAttributes)} except this one does not create - * the media player. - * @hide - */ - public void setAudioAttributesField(@Nullable AudioAttributes attributes) { - if (attributes == null) { - throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone"); - } - mAudioAttributes = attributes; - } - - /** - * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is - * the one on which outgoing audio for SIM calls is played. - * - * @param audioManager the audio manage. - * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if - * none can be found. - */ - private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) { - AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); - for (AudioDeviceInfo device : deviceList) { - if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { - return device; - } - } - return null; - } - - /** - * Sets the preferred device of the ringtong playback to the built-in device. - * - * @hide - */ - public boolean preferBuiltinDevice(boolean enable) { - mPreferBuiltinDevice = enable; - if (mLocalPlayer == null) { - return true; - } - return mLocalPlayer.setPreferredDevice(getBuiltinDevice(mAudioManager)); - } - - /** - * Creates a local media player for the ringtone using currently set attributes. - * @return true if media player creation succeeded or is deferred, - * false if it did not succeed and can't be tried remotely. - * @hide - */ - public boolean reinitializeActivePlayer() { - Trace.beginSection("reinitializeActivePlayer"); - if (mUri == null) { - Log.e(TAG, "Could not create media player as no URI was provided."); - return mAllowRemote && mRemotePlayer != null; - } - destroyLocalPlayer(); - // try opening uri locally before delegating to remote player - mLocalPlayer = new MediaPlayer(); - try { - mLocalPlayer.setDataSource(mContext, mUri); - mLocalPlayer.setAudioAttributes(mAudioAttributes); - mLocalPlayer.setPreferredDevice( - mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null); - synchronized (mPlaybackSettingsLock) { - applyPlaybackProperties_sync(); - } - if (mVolumeShaperConfig != null) { - mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig); - } - mLocalPlayer.prepare(); - - } catch (SecurityException | IOException e) { - destroyLocalPlayer(); - if (!mAllowRemote) { - Log.w(TAG, "Remote playback not allowed: " + e); - } - } - - if (LOGD) { - if (mLocalPlayer != null) { - Log.d(TAG, "Successfully created local player"); - } else { - Log.d(TAG, "Problem opening; delegating to remote player"); - } - } - Trace.endSection(); - return mLocalPlayer != null || (mAllowRemote && mRemotePlayer != null); - } - - /** - * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone. - * If the ringtone has not been created, it will load based on URI provided at {@link #setUri} - * and if not URI has been set, it will assume no haptic channels are present. - * @hide - */ - public boolean hasHapticChannels() { - // FIXME: support remote player, or internalize haptic channels support and remove entirely. - try { - android.os.Trace.beginSection("Ringtone.hasHapticChannels"); - if (mLocalPlayer != null) { - for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) { - if (trackInfo.hasHapticChannels()) { - return true; - } - } - } - } finally { - android.os.Trace.endSection(); - } - return false; - } - - /** - * Returns whether a local player has been created for this ringtone. - * @hide - */ - @VisibleForTesting - public boolean hasLocalPlayer() { - return mLocalPlayer != null; - } - - public @Ringtone.RingtoneMedia int getEnabledMedia() { - return Ringtone.MEDIA_SOUND; // RingtoneV2 only - } - - public VibrationEffect getVibrationEffect() { - return null; // RingtoneV2 only - } - - /** - * Returns the {@link AudioAttributes} used by this object. - * @return the {@link AudioAttributes} that were set with - * {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set. - */ - public AudioAttributes getAudioAttributes() { - return mAudioAttributes; - } - - /** - * Sets the player to be looping or non-looping. - * @param looping whether to loop or not. - */ - public void setLooping(boolean looping) { - synchronized (mPlaybackSettingsLock) { - mIsLooping = looping; - applyPlaybackProperties_sync(); - } - } - - /** - * Returns whether the looping mode was enabled on this player. - * @return true if this player loops when playing. - */ - public boolean isLooping() { - synchronized (mPlaybackSettingsLock) { - return mIsLooping; - } - } - - /** - * Sets the volume on this player. - * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0 - * corresponds to no attenuation being applied. - */ - public void setVolume(float volume) { - synchronized (mPlaybackSettingsLock) { - if (volume < 0.0f) { volume = 0.0f; } - if (volume > 1.0f) { volume = 1.0f; } - mVolume = volume; - applyPlaybackProperties_sync(); - } - } - - /** - * Returns the volume scalar set on this player. - * @return a value between 0.0f and 1.0f. - */ - public float getVolume() { - synchronized (mPlaybackSettingsLock) { - return mVolume; - } - } - - /** - * Enable or disable the {@link android.media.audiofx.HapticGenerator} effect. The effect can - * only be enabled on devices that support the effect. - * - * @return true if the HapticGenerator effect is successfully enabled. Otherwise, return false. - * @see android.media.audiofx.HapticGenerator#isAvailable() - */ - public boolean setHapticGeneratorEnabled(boolean enabled) { - if (!HapticGenerator.isAvailable()) { - return false; - } - synchronized (mPlaybackSettingsLock) { - mHapticGeneratorEnabled = enabled; - applyPlaybackProperties_sync(); - } - return true; - } - - /** - * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not. - * @return true if the HapticGenerator is enabled. - */ - public boolean isHapticGeneratorEnabled() { - synchronized (mPlaybackSettingsLock) { - return mHapticGeneratorEnabled; - } - } - - /** - * Must be called synchronized on mPlaybackSettingsLock - */ - private void applyPlaybackProperties_sync() { - if (mLocalPlayer != null) { - mLocalPlayer.setVolume(mVolume); - mLocalPlayer.setLooping(mIsLooping); - if (mHapticGenerator == null && mHapticGeneratorEnabled) { - mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId()); - } - if (mHapticGenerator != null) { - mHapticGenerator.setEnabled(mHapticGeneratorEnabled); - } - } else if (mAllowRemote && (mRemotePlayer != null)) { - try { - mRemotePlayer.setPlaybackProperties( - mRemoteToken, mVolume, mIsLooping, mHapticGeneratorEnabled); - } catch (RemoteException e) { - Log.w(TAG, "Problem setting playback properties: ", e); - } - } else { - Log.w(TAG, - "Neither local nor remote player available when applying playback properties"); - } - } - - /** - * Returns a human-presentable title for ringtone. Looks in media - * content provider. If not in either, uses the filename - * - * @param context A context used for querying. - */ - public String getTitle(Context context) { - if (mTitle != null) return mTitle; - return mTitle = Ringtone.getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote); - } - - /** - * Set {@link Uri} to be used for ringtone playback. - * {@link IRingtonePlayer}. - * - * @hide - */ - @UnsupportedAppUsage - public void setUri(Uri uri) { - setUri(uri, null); - } - - /** - * @hide - */ - public void setVolumeShaperConfig(@Nullable VolumeShaper.Configuration volumeShaperConfig) { - mVolumeShaperConfig = volumeShaperConfig; - } - - /** - * Set {@link Uri} to be used for ringtone playback. Attempts to open - * locally, otherwise will delegate playback to remote - * {@link IRingtonePlayer}. Add {@link VolumeShaper} if required. - * - * @hide - */ - public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) { - mVolumeShaperConfig = volumeShaperConfig; - mUri = uri; - if (mUri == null) { - destroyLocalPlayer(); - } - } - - /** {@hide} */ - @UnsupportedAppUsage - public Uri getUri() { - return mUri; - } - - /** - * Plays the ringtone. - */ - public void play() { - if (mLocalPlayer != null) { - // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone - // (typically because ringer mode is vibrate). - if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes)) - != 0) { - startLocalPlayer(); - } else if (!mAudioAttributes.areHapticChannelsMuted() && hasHapticChannels()) { - // is haptic only ringtone - startLocalPlayer(); - } - } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != null)) { - final Uri canonicalUri = mUri.getCanonicalUri(); - final boolean looping; - final float volume; - synchronized (mPlaybackSettingsLock) { - looping = mIsLooping; - volume = mVolume; - } - try { - mRemotePlayer.playWithVolumeShaping(mRemoteToken, canonicalUri, mAudioAttributes, - volume, looping, mVolumeShaperConfig); - } catch (RemoteException e) { - if (!playFallbackRingtone()) { - Log.w(TAG, "Problem playing ringtone: " + e); - } - } - } else { - if (!playFallbackRingtone()) { - Log.w(TAG, "Neither local nor remote playback available"); - } - } - } - - /** - * Stops a playing ringtone. - */ - public void stop() { - if (mLocalPlayer != null) { - destroyLocalPlayer(); - } else if (mAllowRemote && (mRemotePlayer != null)) { - try { - mRemotePlayer.stop(mRemoteToken); - } catch (RemoteException e) { - Log.w(TAG, "Problem stopping ringtone: " + e); - } - } - } - - private void destroyLocalPlayer() { - if (mLocalPlayer != null) { - if (mHapticGenerator != null) { - mHapticGenerator.release(); - mHapticGenerator = null; - } - mLocalPlayer.setOnCompletionListener(null); - mLocalPlayer.reset(); - mLocalPlayer.release(); - mLocalPlayer = null; - mVolumeShaper = null; - synchronized (sActiveRingtones) { - sActiveRingtones.remove(this); - } - } - } - - private void startLocalPlayer() { - if (mLocalPlayer == null) { - return; - } - synchronized (sActiveRingtones) { - sActiveRingtones.add(this); - } - if (LOGD) { - Log.d(TAG, "Starting ringtone playback"); - } - mLocalPlayer.setOnCompletionListener(mCompletionListener); - mLocalPlayer.start(); - if (mVolumeShaper != null) { - mVolumeShaper.apply(VolumeShaper.Operation.PLAY); - } - } - - /** - * Whether this ringtone is currently playing. - * - * @return True if playing, false otherwise. - */ - public boolean isPlaying() { - if (mLocalPlayer != null) { - return mLocalPlayer.isPlaying(); - } else if (mAllowRemote && (mRemotePlayer != null)) { - try { - return mRemotePlayer.isPlaying(mRemoteToken); - } catch (RemoteException e) { - Log.w(TAG, "Problem checking ringtone: " + e); - return false; - } - } else { - Log.w(TAG, "Neither local nor remote playback available"); - return false; - } - } - - private boolean playFallbackRingtone() { - int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes); - if (mAudioManager.getStreamVolume(streamType) == 0) { - return false; - } - int ringtoneType = RingtoneManager.getDefaultType(mUri); - if (ringtoneType != -1 && - RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) { - Log.w(TAG, "not playing fallback for " + mUri); - return false; - } - // Default ringtone, try fallback ringtone. - try { - AssetFileDescriptor afd = mContext.getResources().openRawResourceFd( - com.android.internal.R.raw.fallbackring); - if (afd == null) { - Log.e(TAG, "Could not load fallback ringtone"); - return false; - } - mLocalPlayer = new MediaPlayer(); - if (afd.getDeclaredLength() < 0) { - mLocalPlayer.setDataSource(afd.getFileDescriptor()); - } else { - mLocalPlayer.setDataSource(afd.getFileDescriptor(), - afd.getStartOffset(), - afd.getDeclaredLength()); - } - mLocalPlayer.setAudioAttributes(mAudioAttributes); - synchronized (mPlaybackSettingsLock) { - applyPlaybackProperties_sync(); - } - if (mVolumeShaperConfig != null) { - mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig); - } - mLocalPlayer.prepare(); - startLocalPlayer(); - afd.close(); - } catch (IOException ioe) { - destroyLocalPlayer(); - Log.e(TAG, "Failed to open fallback ringtone"); - return false; - } catch (NotFoundException nfe) { - Log.e(TAG, "Fallback ringtone does not exist"); - return false; - } - return true; - } - - public boolean getPreferBuiltinDevice() { - return mPreferBuiltinDevice; - } - - public VolumeShaper.Configuration getVolumeShaperConfig() { - return mVolumeShaperConfig; - } - - public boolean isLocalOnly() { - return mAllowRemote; - } - - public boolean isUsingRemotePlayer() { - // V2 testing api, but this is the v1 approximation. - return (mLocalPlayer == null) && mAllowRemote && (mRemotePlayer != null); - } - - class MyOnCompletionListener implements MediaPlayer.OnCompletionListener { - @Override - public void onCompletion(MediaPlayer mp) { - synchronized (sActiveRingtones) { - sActiveRingtones.remove(RingtoneV1.this); - } - mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle. - } - } -} diff --git a/media/java/android/media/RingtoneV2.java b/media/java/android/media/RingtoneV2.java deleted file mode 100644 index f1a81553bdfc..000000000000 --- a/media/java/android/media/RingtoneV2.java +++ /dev/null @@ -1,690 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.compat.annotation.UnsupportedAppUsage; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.content.res.Resources.NotFoundException; -import android.media.Ringtone.Injectables; -import android.net.Uri; -import android.os.Binder; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.Trace; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.provider.MediaStore; -import android.provider.MediaStore.MediaColumns; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; - -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * New Ringtone implementation, supporting vibration as well as sound, and configuration via a - * builder. During flagged transition, the original implementation is in RingtoneV1.java. - * - * Only modified methods are moved here. - * - * @hide - */ -class RingtoneV2 implements Ringtone.ApiInterface { - private static final String TAG = "RingtoneV2"; - - /** - * The ringtone should only play sound. Any vibration is managed externally. - * @hide - */ - public static final int MEDIA_SOUND = 1; - /** - * The ringtone should only play vibration. Any sound is managed externally. - * Requires the {@link android.Manifest.permission#VIBRATE} permission. - * @hide - */ - public static final int MEDIA_VIBRATION = 1 << 1; - /** - * The ringtone should play sound and vibration. - * @hide - */ - public static final int MEDIA_SOUND_AND_VIBRATION = MEDIA_SOUND | MEDIA_VIBRATION; - - // This is not a public value, because apps shouldn't enable "all" media - that wouldn't be - // safe if new media types were added. - static final int MEDIA_ALL = MEDIA_SOUND | MEDIA_VIBRATION; - - /** - * Declares the types of media that this Ringtone is allowed to play. - * @hide - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = "MEDIA_", value = { - MEDIA_SOUND, - MEDIA_VIBRATION, - MEDIA_SOUND_AND_VIBRATION, - }) - public @interface RingtoneMedia {} - - private static final String[] MEDIA_COLUMNS = new String[] { - MediaStore.Audio.Media._ID, - MediaStore.Audio.Media.TITLE - }; - /** Selection that limits query results to just audio files */ - private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR " - + MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')"; - - private final Context mContext; - private final Vibrator mVibrator; - private final AudioManager mAudioManager; - private VolumeShaper.Configuration mVolumeShaperConfig; - - /** - * Flag indicating if we're allowed to fall back to remote playback using - * {@link #mRemoteRingtoneService}. Typically this is false when we're the remote - * player and there is nobody else to delegate to. - */ - private final boolean mAllowRemote; - private final IRingtonePlayer mRemoteRingtoneService; - private final Injectables mInjectables; - - private final int mEnabledMedia; - - private final Uri mUri; - private String mTitle; - - private AudioAttributes mAudioAttributes; - private boolean mUseExactAudioAttributes; - private boolean mPreferBuiltinDevice; - private RingtonePlayer mActivePlayer; - // playback properties, use synchronized with mPlaybackSettingsLock - private boolean mIsLooping; - private float mVolume; - private boolean mHapticGeneratorEnabled; - private final Object mPlaybackSettingsLock = new Object(); - private final VibrationEffect mVibrationEffect; - - /** Only for use by Ringtone constructor */ - RingtoneV2(@NonNull Context context, @NonNull Injectables injectables, - boolean allowRemote, @Ringtone.RingtoneMedia int enabledMedia, - @Nullable Uri uri, @NonNull AudioAttributes audioAttributes, - boolean useExactAudioAttributes, - @Nullable VolumeShaper.Configuration volumeShaperConfig, - boolean preferBuiltinDevice, float soundVolume, boolean looping, - boolean hapticGeneratorEnabled, @Nullable VibrationEffect vibrationEffect) { - // Context - mContext = context; - mInjectables = injectables; - mVibrator = mContext.getSystemService(Vibrator.class); - mAudioManager = mContext.getSystemService(AudioManager.class); - mRemoteRingtoneService = allowRemote ? mAudioManager.getRingtonePlayer() : null; - mAllowRemote = (mRemoteRingtoneService != null); // Only set if allowed, and present. - - // Properties potentially propagated to remote player. - mEnabledMedia = enabledMedia; - mUri = uri; - mAudioAttributes = audioAttributes; - mUseExactAudioAttributes = useExactAudioAttributes; - mVolumeShaperConfig = volumeShaperConfig; - mPreferBuiltinDevice = preferBuiltinDevice; // system-only, not supported for remote play. - mVolume = soundVolume; - mIsLooping = looping; - mHapticGeneratorEnabled = hapticGeneratorEnabled; - mVibrationEffect = vibrationEffect; - } - - /** @hide */ - @RingtoneMedia - public int getEnabledMedia() { - return mEnabledMedia; - } - - /** - * Sets the stream type where this ringtone will be played. - * - * @param streamType The stream, see {@link AudioManager}. - * @deprecated use {@link #setAudioAttributes(AudioAttributes)} - */ - @Deprecated - public void setStreamType(int streamType) { - setAudioAttributes( - getAudioAttributesForLegacyStreamType(streamType, "setStreamType()")); - } - - private AudioAttributes getAudioAttributesForLegacyStreamType(int streamType, String originOp) { - PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", originOp); - return new AudioAttributes.Builder() - .setInternalLegacyStreamType(streamType) - .build(); - } - - /** - * Gets the stream type where this ringtone will be played. - * - * @return The stream type, see {@link AudioManager}. - * @deprecated use of stream types is deprecated, see - * {@link #setAudioAttributes(AudioAttributes)} - */ - @Deprecated - public int getStreamType() { - return AudioAttributes.toLegacyStreamType(mAudioAttributes); - } - - /** - * Sets the {@link AudioAttributes} for this ringtone. - * @param attributes the non-null attributes characterizing this ringtone. - */ - public void setAudioAttributes(AudioAttributes attributes) - throws IllegalArgumentException { - // TODO: deprecate this method - it will be done with a builder. - if (attributes == null) { - throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone"); - } - mAudioAttributes = attributes; - // Setting the audio attributes requires re-initializing the player. - if (mActivePlayer != null) { - // The audio attributes have to be set before the media player is prepared. - // Re-initialize it. - reinitializeActivePlayer(); - } - } - - /** - * Returns the vibration effect that this ringtone was created with, if vibration is enabled. - * Otherwise, returns null. - * @hide - */ - @Nullable - public VibrationEffect getVibrationEffect() { - return mVibrationEffect; - } - - /** @hide */ - @VisibleForTesting - public boolean getPreferBuiltinDevice() { - return mPreferBuiltinDevice; - } - - /** @hide */ - @VisibleForTesting - public VolumeShaper.Configuration getVolumeShaperConfig() { - return mVolumeShaperConfig; - } - - /** - * Returns whether this player is local only, or can defer to the remote player. The - * result may differ from the builder if there is no remote player available at all. - * @hide - */ - @VisibleForTesting - public boolean isLocalOnly() { - return !mAllowRemote; - } - - /** @hide */ - @VisibleForTesting - public boolean isUsingRemotePlayer() { - return mActivePlayer instanceof RemoteRingtonePlayer; - } - - /** - * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is - * the one on which outgoing audio for SIM calls is played. - * - * @param audioManager the audio manage. - * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if - * none can be found. - */ - private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) { - AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); - for (AudioDeviceInfo device : deviceList) { - if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { - return device; - } - } - return null; - } - - /** - * Creates a local media player for the ringtone using currently set attributes. - * @return true if media player creation succeeded or is deferred, - * false if it did not succeed and can't be tried remotely. - * @hide - */ - public boolean reinitializeActivePlayer() { - // Try creating a local media player, or fallback to creating a remote one. - Trace.beginSection("reinitializeActivePlayer"); - try { - if (mActivePlayer != null) { - // This would only happen if calling the deprecated setAudioAttributes after - // building the Ringtone. - stopAndReleaseActivePlayer(); - } - - boolean vibrationOnly = (mEnabledMedia & MEDIA_ALL) == MEDIA_VIBRATION; - // Vibration can come from the audio file if using haptic generator or if haptic - // channels are a possibility. - boolean maybeAudioVibration = mUri != null && mInjectables.isHapticPlaybackSupported() - && (mHapticGeneratorEnabled || !mAudioAttributes.areHapticChannelsMuted()); - - // VibrationEffect only, use the simplified player without checking for haptic channels. - if (vibrationOnly && !maybeAudioVibration && mVibrationEffect != null) { - mActivePlayer = new LocalRingtonePlayer.VibrationEffectPlayer( - mVibrationEffect, mAudioAttributes, mVibrator, mIsLooping); - return true; - } - - AudioDeviceInfo preferredDevice = - mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null; - if (mUri != null) { - mActivePlayer = LocalRingtonePlayer.create(mContext, mAudioManager, mVibrator, mUri, - mAudioAttributes, vibrationOnly, mVibrationEffect, mInjectables, - mVolumeShaperConfig, preferredDevice, mHapticGeneratorEnabled, mIsLooping, - mVolume); - } else { - // Using the remote player won't help play a null Uri. Revert straight to fallback. - // The vibration-only case was already covered above. - mActivePlayer = createFallbackRingtonePlayer(); - // Fall through to attempting remote fallback play if null. - } - - if (mActivePlayer == null && mAllowRemote) { - mActivePlayer = new RemoteRingtonePlayer(mRemoteRingtoneService, mUri, - mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect, - mVolumeShaperConfig, mHapticGeneratorEnabled, mIsLooping, mVolume); - } - - return mActivePlayer != null; - } finally { - if (mActivePlayer != null) { - Log.d(TAG, "Initialized ringtone player with " + mActivePlayer.getClass()); - } else { - Log.d(TAG, "Failed to initialize ringtone player"); - } - Trace.endSection(); - } - } - - @Nullable - private LocalRingtonePlayer createFallbackRingtonePlayer() { - int ringtoneType = RingtoneManager.getDefaultType(mUri); - if (ringtoneType != -1 - && RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) { - Log.w(TAG, "not playing fallback for " + mUri); - return null; - } - // Default ringtone, try fallback ringtone. - try (AssetFileDescriptor afd = mContext.getResources().openRawResourceFd( - com.android.internal.R.raw.fallbackring)) { - if (afd == null) { - Log.e(TAG, "Could not load fallback ringtone"); - return null; - } - - AudioDeviceInfo preferredDevice = - mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null; - return LocalRingtonePlayer.createForFallback(mAudioManager, mVibrator, afd, - mAudioAttributes, mVibrationEffect, mInjectables, mVolumeShaperConfig, - preferredDevice, mIsLooping, mVolume); - } catch (NotFoundException nfe) { - Log.e(TAG, "Fallback ringtone does not exist"); - return null; - } catch (IOException e) { - // As with the above messages, not including much information about the - // failure so as not to expose details of the fallback ringtone resource. - Log.e(TAG, "Exception reading fallback ringtone"); - return null; - } - } - - /** - * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone. - * @hide - */ - public boolean hasHapticChannels() { - return (mActivePlayer == null) ? false : mActivePlayer.hasHapticChannels(); - } - - /** - * Returns the {@link AudioAttributes} used by this object. - * @return the {@link AudioAttributes} that were set with - * {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set. - */ - public AudioAttributes getAudioAttributes() { - return mAudioAttributes; - } - - /** - * Sets the player to be looping or non-looping. - * @param looping whether to loop or not. - */ - public void setLooping(boolean looping) { - synchronized (mPlaybackSettingsLock) { - mIsLooping = looping; - if (mActivePlayer != null) { - mActivePlayer.setLooping(looping); - } - } - } - - /** - * Returns whether the looping mode was enabled on this player. - * @return true if this player loops when playing. - */ - public boolean isLooping() { - synchronized (mPlaybackSettingsLock) { - return mIsLooping; - } - } - - /** - * Sets the volume on this player. - * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0 - * corresponds to no attenuation being applied. - */ - public void setVolume(float volume) { - // Ignore if sound not enabled. - if ((mEnabledMedia & MEDIA_SOUND) == 0) { - return; - } - if (volume < 0.0f) { - volume = 0.0f; - } else if (volume > 1.0f) { - volume = 1.0f; - } - - synchronized (mPlaybackSettingsLock) { - mVolume = volume; - if (mActivePlayer != null) { - mActivePlayer.setVolume(volume); - } - } - } - - /** - * Returns the volume scalar set on this player. - * @return a value between 0.0f and 1.0f. - */ - public float getVolume() { - synchronized (mPlaybackSettingsLock) { - return mVolume; - } - } - - /** - * Enable or disable the {@link android.media.audiofx.HapticGenerator} effect. The effect can - * only be enabled on devices that support the effect. - * - * @return true if the HapticGenerator effect is successfully enabled. Otherwise, return false. - * @see android.media.audiofx.HapticGenerator#isAvailable() - */ - public boolean setHapticGeneratorEnabled(boolean enabled) { - if (!mInjectables.isHapticGeneratorAvailable()) { - return false; - } - synchronized (mPlaybackSettingsLock) { - mHapticGeneratorEnabled = enabled; - if (mActivePlayer != null) { - mActivePlayer.setHapticGeneratorEnabled(enabled); - } - } - return true; - } - - /** - * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not. - * @return true if the HapticGenerator is enabled. - */ - public boolean isHapticGeneratorEnabled() { - synchronized (mPlaybackSettingsLock) { - return mHapticGeneratorEnabled; - } - } - - /** - * Returns a human-presentable title for ringtone. Looks in media - * content provider. If not in either, uses the filename - * - * @param context A context used for querying. - */ - public String getTitle(Context context) { - if (mTitle != null) return mTitle; - return mTitle = Ringtone.getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote); - } - - - /** {@hide} */ - @UnsupportedAppUsage - public Uri getUri() { - return mUri; - } - - /** - * Plays the ringtone. - */ - public void play() { - if (mActivePlayer != null) { - Log.d(TAG, "Starting ringtone playback"); - if (mActivePlayer.play()) { - return; - } else { - // Discard active player: play() is only meant to be called once. - stopAndReleaseActivePlayer(); - } - } - if (!playFallbackRingtone()) { - Log.w(TAG, "Neither local nor remote playback available"); - } - } - - /** - * Stops a playing ringtone. - */ - public void stop() { - stopAndReleaseActivePlayer(); - } - - private void stopAndReleaseActivePlayer() { - if (mActivePlayer != null) { - mActivePlayer.stopAndRelease(); - mActivePlayer = null; - } - } - - /** - * Whether this ringtone is currently playing. - * - * @return True if playing, false otherwise. - */ - public boolean isPlaying() { - if (mActivePlayer != null) { - return mActivePlayer.isPlaying(); - } else { - Log.w(TAG, "No active ringtone player"); - return false; - } - } - - /** - * Fallback during the play stage rather than initialization, typically due to an issue - * communicating with the remote player. - */ - private boolean playFallbackRingtone() { - if (mActivePlayer != null) { - Log.wtf(TAG, "Playing fallback ringtone with another active player"); - stopAndReleaseActivePlayer(); - } - int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes); - if (mAudioManager.getStreamVolume(streamType) == 0) { - // TODO: Return true? If volume is off, this is a successful play. - return false; - } - mActivePlayer = createFallbackRingtonePlayer(); - if (mActivePlayer == null) { - return false; // the create method logs if it returns null. - } else if (mActivePlayer.play()) { - return true; - } else { - stopAndReleaseActivePlayer(); - return false; - } - } - - void setTitle(String title) { - mTitle = title; - } - - /** - * Play a specific ringtone. This interface is implemented by either local (this process) or - * proxied-remote playback via AudioManager.getRingtonePlayer, so that the caller - * (Ringtone class) can just use a single player after the initial creation. - * @hide - */ - interface RingtonePlayer { - /** - * Start playing the ringtone, returning false if there was a problem that - * requires falling back to the fallback ringtone resource. - */ - boolean play(); - boolean isPlaying(); - void stopAndRelease(); - - // Mutating playback methods. - void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo); - void setLooping(boolean looping); - void setHapticGeneratorEnabled(boolean enabled); - void setVolume(float volume); - - boolean hasHapticChannels(); - } - - /** - * Remote RingtonePlayer. All operations are delegated via the IRingtonePlayer interface, which - * should ultimately be backed by a RingtoneLocalPlayer within the system services. - */ - static class RemoteRingtonePlayer implements RingtonePlayer { - private final IBinder mRemoteToken = new Binder(); - private final IRingtonePlayer mRemoteRingtoneService; - private final Uri mCanonicalUri; - private final int mEnabledMedia; - private final VibrationEffect mVibrationEffect; - private final VolumeShaper.Configuration mVolumeShaperConfig; - private final AudioAttributes mAudioAttributes; - private final boolean mUseExactAudioAttributes; - private boolean mIsLooping; - private float mVolume; - private boolean mHapticGeneratorEnabled; - - RemoteRingtonePlayer(@NonNull IRingtonePlayer remoteRingtoneService, - @NonNull Uri uri, @NonNull AudioAttributes audioAttributes, - boolean useExactAudioAttributes, - @RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect, - @Nullable VolumeShaper.Configuration volumeShaperConfig, - boolean hapticGeneratorEnabled, boolean initialIsLooping, float initialVolume) { - mRemoteRingtoneService = remoteRingtoneService; - mCanonicalUri = (uri == null) ? null : uri.getCanonicalUri(); - mAudioAttributes = audioAttributes; - mUseExactAudioAttributes = useExactAudioAttributes; - mEnabledMedia = enabledMedia; - mVibrationEffect = vibrationEffect; - mVolumeShaperConfig = volumeShaperConfig; - mHapticGeneratorEnabled = hapticGeneratorEnabled; - mIsLooping = initialIsLooping; - mVolume = initialVolume; - } - - @Override - public boolean play() { - try { - mRemoteRingtoneService.playRemoteRingtone(mRemoteToken, mCanonicalUri, - mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect, - mVolume, mIsLooping, mHapticGeneratorEnabled, mVolumeShaperConfig); - return true; - } catch (RemoteException e) { - Log.w(TAG, "Problem playing ringtone: " + e); - return false; - } - } - - @Override - public boolean isPlaying() { - try { - return mRemoteRingtoneService.isPlaying(mRemoteToken); - } catch (RemoteException e) { - Log.w(TAG, "Problem checking ringtone isPlaying: " + e); - return false; - } - } - - @Override - public void stopAndRelease() { - try { - mRemoteRingtoneService.stop(mRemoteToken); - } catch (RemoteException e) { - Log.w(TAG, "Problem stopping ringtone: " + e); - } - } - - @Override - public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) { - // un-implemented for remote (but not used outside system). - } - - @Override - public void setLooping(boolean looping) { - mIsLooping = looping; - try { - mRemoteRingtoneService.setLooping(mRemoteToken, looping); - } catch (RemoteException e) { - Log.w(TAG, "Problem setting looping: " + e); - } - } - - @Override - public void setHapticGeneratorEnabled(boolean enabled) { - mHapticGeneratorEnabled = enabled; - try { - mRemoteRingtoneService.setHapticGeneratorEnabled(mRemoteToken, enabled); - } catch (RemoteException e) { - Log.w(TAG, "Problem setting hapticGeneratorEnabled: " + e); - } - } - - @Override - public void setVolume(float volume) { - mVolume = volume; - try { - mRemoteRingtoneService.setVolume(mRemoteToken, volume); - } catch (RemoteException e) { - Log.w(TAG, "Problem setting volume: " + e); - } - } - - @Override - public boolean hasHapticChannels() { - // FIXME: support remote player, or internalize haptic channels support and remove - // entirely. - return false; - } - } - -} diff --git a/media/java/android/media/browse/MediaBrowserUtils.java b/media/java/android/media/browse/MediaBrowserUtils.java index 19d9f008d3db..8c008bc3e28d 100644 --- a/media/java/android/media/browse/MediaBrowserUtils.java +++ b/media/java/android/media/browse/MediaBrowserUtils.java @@ -18,6 +18,9 @@ package android.media.browse; import android.os.Bundle; +import java.util.Collections; +import java.util.List; + /** * @hide */ @@ -75,4 +78,29 @@ public class MediaBrowserUtils { } return false; } + + /** + * Returns a paged version of the given {@code list}, using the paging parameters in {@code + * options}. + */ + public static List<MediaBrowser.MediaItem> applyPagingOptions( + List<MediaBrowser.MediaItem> list, final Bundle options) { + if (list == null) { + return null; + } + int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1); + int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1); + if (page == -1 && pageSize == -1) { + return list; + } + int fromIndex = pageSize * page; + int toIndex = fromIndex + pageSize; + if (page < 0 || pageSize < 1 || fromIndex >= list.size()) { + return Collections.EMPTY_LIST; + } + if (toIndex > list.size()) { + toIndex = list.size(); + } + return list.subList(fromIndex, toIndex); + } } diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java index e8ef46499dc8..ba7ab9aa97ed 100644 --- a/media/java/android/service/media/MediaBrowserService.java +++ b/media/java/android/service/media/MediaBrowserService.java @@ -48,7 +48,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -728,7 +727,7 @@ public abstract class MediaBrowserService extends Service { List<MediaBrowser.MediaItem> filteredList = (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 - ? applyOptions(list, options) : list; + ? MediaBrowserUtils.applyPagingOptions(list, options) : list; final ParceledListSlice<MediaBrowser.MediaItem> pls; if (filteredList == null) { pls = null; @@ -762,27 +761,6 @@ public abstract class MediaBrowserService extends Service { } } - private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list, - final Bundle options) { - if (list == null) { - return null; - } - int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1); - int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1); - if (page == -1 && pageSize == -1) { - return list; - } - int fromIndex = pageSize * page; - int toIndex = fromIndex + pageSize; - if (page < 0 || pageSize < 1 || fromIndex >= list.size()) { - return Collections.EMPTY_LIST; - } - if (toIndex > list.size()) { - toIndex = list.size(); - } - return list.subList(fromIndex, toIndex); - } - private void performLoadItem(String itemId, final ConnectionRecord connection, final ResultReceiver receiver) { final Result<MediaBrowser.MediaItem> result = diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java index f105ae9cc33e..02d9fb947f9e 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java @@ -45,8 +45,10 @@ import java.util.List; @RunWith(AndroidJUnit4.class) @RequiresFlagsEnabled(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) public final class FadeManagerConfigurationUnitTest { - private static final long DEFAULT_FADE_OUT_DURATION_MS = 2_000; - private static final long DEFAULT_FADE_IN_DURATION_MS = 1_000; + private static final long DEFAULT_FADE_OUT_DURATION_MS = + FadeManagerConfiguration.getDefaultFadeOutDurationMillis(); + private static final long DEFAULT_FADE_IN_DURATION_MS = + FadeManagerConfiguration.getDefaultFadeInDurationMillis(); private static final long TEST_FADE_OUT_DURATION_MS = 1_500; private static final long TEST_FADE_IN_DURATION_MS = 750; private static final int TEST_INVALID_USAGE = -10; @@ -259,16 +261,6 @@ public final class FadeManagerConfigurationUnitTest { } @Test - public void testSetFadeState_toEnableAuto() { - final int fadeStateAuto = FadeManagerConfiguration.FADE_STATE_ENABLED_AUTO; - FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() - .setFadeState(fadeStateAuto).build(); - - expect.withMessage("Fade state when enabled for audio").that(fmc.getFadeState()) - .isEqualTo(fadeStateAuto); - } - - @Test public void testSetFadeState_toInvalid_fails() { IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> new FadeManagerConfiguration.Builder() diff --git a/media/tests/MediaFrameworkTest/Android.bp b/media/tests/MediaFrameworkTest/Android.bp index 7a329bccf940..1325fc161b77 100644 --- a/media/tests/MediaFrameworkTest/Android.bp +++ b/media/tests/MediaFrameworkTest/Android.bp @@ -22,7 +22,6 @@ android_test { "android-ex-camera2", "android.media.playback.flags-aconfig-java", "flag-junit", - "testables", "testng", "truth", ], diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/OWNERS b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/OWNERS deleted file mode 100644 index 6d5f82ce9b66..000000000000 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Haptics team also works on Ringtone -per-file *Ringtone* = file:/services/core/java/com/android/server/vibrator/OWNERS diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java deleted file mode 100644 index 3c0c6847f557..000000000000 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java +++ /dev/null @@ -1,840 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.mediaframeworktest.unit; - -import static android.media.Ringtone.MEDIA_SOUND; -import static android.media.Ringtone.MEDIA_SOUND_AND_VIBRATION; -import static android.media.Ringtone.MEDIA_VIBRATION; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.AssetFileDescriptor; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.IRingtonePlayer; -import android.media.MediaPlayer; -import android.media.Ringtone; -import android.media.audiofx.HapticGenerator; -import android.net.Uri; -import android.os.IBinder; -import android.os.VibrationAttributes; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.testing.TestableContext; -import android.util.ArrayMap; -import android.util.ArraySet; - -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import com.android.mediaframeworktest.R; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runner.RunWith; -import org.junit.runners.model.Statement; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.io.FileNotFoundException; -import java.util.ArrayDeque; -import java.util.Map; -import java.util.Queue; - -@RunWith(AndroidJUnit4.class) -public class RingtoneTest { - - private static final Uri SOUND_URI = Uri.parse("content://fake-sound-uri"); - - private static final AudioAttributes RINGTONE_ATTRIBUTES = - audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); - private static final AudioAttributes RINGTONE_ATTRIBUTES_WITH_HC = - new AudioAttributes.Builder(RINGTONE_ATTRIBUTES).setHapticChannelsMuted(false).build(); - private static final VibrationAttributes RINGTONE_VIB_ATTRIBUTES = - new VibrationAttributes.Builder(RINGTONE_ATTRIBUTES).build(); - - private static final VibrationEffect VIBRATION_EFFECT = - VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100}, -1); - private static final VibrationEffect VIBRATION_EFFECT_REPEATING = - VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100, 50}, 1); - - @Rule - public final RingtoneInjectablesTrackingTestRule - mMediaPlayerRule = new RingtoneInjectablesTrackingTestRule(); - - @Captor private ArgumentCaptor<IBinder> mIBinderCaptor; - @Mock private IRingtonePlayer mMockRemotePlayer; - @Mock private Vibrator mMockVibrator; - private AudioManager mSpyAudioManager; - private TestableContext mContext; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - TestableContext testContext = - new TestableContext(InstrumentationRegistry.getTargetContext(), null); - testContext.getTestablePermissions().setPermission(Manifest.permission.VIBRATE, - PackageManager.PERMISSION_GRANTED); - AudioManager realAudioManager = testContext.getSystemService(AudioManager.class); - mSpyAudioManager = spy(realAudioManager); - when(mSpyAudioManager.getRingtonePlayer()).thenReturn(mMockRemotePlayer); - testContext.addMockSystemService(AudioManager.class, mSpyAudioManager); - testContext.addMockSystemService(Vibrator.class, mMockVibrator); - - mContext = spy(testContext); - } - - @Test - public void testRingtone_fullLifecycleUsingLocalMediaPlayer() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - Ringtone ringtone = - newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES).setUri(SOUND_URI).build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES); - assertThat(ringtone.getVolume()).isEqualTo(1.0f); - assertThat(ringtone.isLooping()).isEqualTo(false); - assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false); - assertThat(ringtone.getPreferBuiltinDevice()).isFalse(); - assertThat(ringtone.getVolumeShaperConfig()).isNull(); - assertThat(ringtone.isLocalOnly()).isFalse(); - - // Prepare - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES); - verify(mockMediaPlayer).setVolume(1.0f); - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - verifyLocalPlay(mockMediaPlayer); - - // Verify dynamic controls. - ringtone.setVolume(0.8f); - verify(mockMediaPlayer).setVolume(0.8f); - when(mockMediaPlayer.isLooping()).thenReturn(false); - ringtone.setLooping(true); - verify(mockMediaPlayer).isLooping(); - verify(mockMediaPlayer).setLooping(true); - HapticGenerator mockHapticGenerator = - mMediaPlayerRule.expectHapticGenerator(mockMediaPlayer); - ringtone.setHapticGeneratorEnabled(true); - verify(mockHapticGenerator).setEnabled(true); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - - // This test is intended to strictly verify all interactions with MediaPlayer in a local - // playback case. This shouldn't be necessary in other tests that have the same basic - // setup. - verifyNoMoreInteractions(mockMediaPlayer); - verify(mockHapticGenerator).release(); - verifyNoMoreInteractions(mockHapticGenerator); - verifyZeroInteractions(mMockRemotePlayer); - verifyZeroInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaPlayerWithAudioCoupledOverride() throws Exception { - // Audio coupled playback is enabled in the incoming attributes, plus an instruction - // to leave the attributes alone. This test verifies that the attributes reach the - // media player without changing. - final AudioAttributes audioAttributes = RINGTONE_ATTRIBUTES_WITH_HC; - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); - Ringtone ringtone = - newBuilder(MEDIA_SOUND, audioAttributes) - .setUri(SOUND_URI) - .setUseExactAudioAttributes(true) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getAudioAttributes()).isEqualTo(audioAttributes); - - // Prepare - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, audioAttributes); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - verifyLocalPlay(mockMediaPlayer); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - - verifyZeroInteractions(mMockRemotePlayer); - verifyZeroInteractions(mMockVibrator); - } - - @Test - public void testRingtone_fullLifecycleUsingRemoteMediaPlayer() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - setupFileNotFound(mockMediaPlayer, SOUND_URI); - Ringtone ringtone = - newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isTrue(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES); - assertThat(ringtone.getVolume()).isEqualTo(1.0f); - assertThat(ringtone.isLooping()).isEqualTo(false); - assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false); - assertThat(ringtone.getPreferBuiltinDevice()).isFalse(); - assertThat(ringtone.getVolumeShaperConfig()).isNull(); - assertThat(ringtone.isLocalOnly()).isFalse(); - - // Initialization did try to create a local media player. - verify(mockMediaPlayer).setDataSource(mContext, SOUND_URI); - // setDataSource throws file not found, so nothing else will happen on the local player. - verify(mockMediaPlayer).release(); - - // Delegates to remote media player. - ringtone.play(); - verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), eq(SOUND_URI), - eq(RINGTONE_ATTRIBUTES), eq(false), eq(MEDIA_SOUND), isNull(), - eq(1.0f), eq(false), eq(false), isNull()); - IBinder remoteToken = mIBinderCaptor.getValue(); - - // Verify dynamic controls. - ringtone.setVolume(0.8f); - verify(mMockRemotePlayer).setVolume(remoteToken, 0.8f); - ringtone.setLooping(true); - verify(mMockRemotePlayer).setLooping(remoteToken, true); - ringtone.setHapticGeneratorEnabled(true); - verify(mMockRemotePlayer).setHapticGeneratorEnabled(remoteToken, true); - - ringtone.stop(); - verify(mMockRemotePlayer).stop(remoteToken); - verifyNoMoreInteractions(mMockRemotePlayer); - verifyNoMoreInteractions(mockMediaPlayer); - verifyZeroInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibration() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // Prepare - // Uses attributes with haptic channels enabled, but will use the effect when there aren't - // any present. - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); - verify(mockMediaPlayer).setVolume(1.0f); - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - - verifyLocalPlay(mockMediaPlayer); - verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); - - // Verify dynamic controls. - ringtone.setVolume(0.8f); - verify(mockMediaPlayer).setVolume(0.8f); - - // Set looping doesn't affect an already-started vibration. - when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original - ringtone.setLooping(true); - verify(mockMediaPlayer).isLooping(); - verify(mockMediaPlayer).setLooping(true); - - // This is ignored because there's a vibration effect being used. - ringtone.setHapticGeneratorEnabled(true); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); - - // This test is intended to strictly verify all interactions with MediaPlayer in a local - // playback case. This shouldn't be necessary in other tests that have the same basic - // setup. - verifyNoMoreInteractions(mockMediaPlayer); - verifyZeroInteractions(mMockRemotePlayer); - verifyNoMoreInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibrationOnly() throws Exception { - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) - // TODO: set sound uri too in diff test - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); - assertThat(ringtone.getUri()).isNull(); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // Play - ringtone.play(); - - verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); - - // Verify dynamic controls (no-op without sound) - ringtone.setVolume(0.8f); - - // Set looping doesn't affect an already-started vibration. - ringtone.setLooping(true); - - // This is ignored because there's a vibration effect being used and no sound. - ringtone.setHapticGeneratorEnabled(true); - - // Release - ringtone.stop(); - verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); - - // This test is intended to strictly verify all interactions with MediaPlayer in a local - // playback case. This shouldn't be necessary in other tests that have the same basic - // setup. - verifyZeroInteractions(mMockRemotePlayer); - verifyNoMoreInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibrationOnlyAndSoundUriNoHapticChannels() - throws Exception { - // A media player will still be created for vibration-only because the vibration can come - // from haptic channels on the sound file (although in this case it doesn't). - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false); - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // Prepare - // Uses attributes with haptic channels enabled, but will abandon the MediaPlayer when it - // knows there aren't any. - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); - verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted. - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - verify(mockMediaPlayer).release(); // abandoned: no haptic channels. - - // Play - ringtone.play(); - - verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); - - // Verify dynamic controls (no-op without sound) - ringtone.setVolume(0.8f); - - // Set looping doesn't affect an already-started vibration. - ringtone.setLooping(true); - - // This is ignored because there's a vibration effect being used and no sound. - ringtone.setHapticGeneratorEnabled(true); - - // Release - ringtone.stop(); - verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); - - // This test is intended to strictly verify all interactions with MediaPlayer in a local - // playback case. This shouldn't be necessary in other tests that have the same basic - // setup. - verifyZeroInteractions(mMockRemotePlayer); - verifyNoMoreInteractions(mMockVibrator); - verifyNoMoreInteractions(mockMediaPlayer); - } - - @Test - public void testRingtone_localMediaWithVibrationOnlyAndSoundUriWithHapticChannels() - throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - when(mMockVibrator.hasVibrator()).thenReturn(true); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); - Ringtone ringtone = - newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // Prepare - // Uses attributes with haptic channels enabled, but will use the effect when there aren't - // any present. - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); - verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted. - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - // Vibrator.vibrate isn't called because the vibration comes from the sound. - verifyLocalPlay(mockMediaPlayer); - - // Verify dynamic controls (no-op without sound) - ringtone.setVolume(0.8f); - - when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original - ringtone.setLooping(true); - verify(mockMediaPlayer).isLooping(); - verify(mockMediaPlayer).setLooping(true); - - // This is ignored because it's using haptic channels. - ringtone.setHapticGeneratorEnabled(true); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - - // This test is intended to strictly verify all interactions with MediaPlayer in a local - // playback case. This shouldn't be necessary in other tests that have the same basic - // setup. - verifyZeroInteractions(mMockRemotePlayer); - verifyZeroInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibrationPrefersHapticChannels() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true); - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // Prepare - // The attributes here have haptic channels enabled (unlike above) - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - when(mockMediaPlayer.isPlaying()).thenReturn(true); - verifyLocalPlay(mockMediaPlayer); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - - verifyZeroInteractions(mMockRemotePlayer); - // Nothing after the initial hasVibrator - it uses audio-coupled. - verifyNoMoreInteractions(mMockVibrator); - } - - @Test - public void testRingtone_localMediaWithVibrationButSoundMuted() throws Exception { - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false); - doReturn(0).when(mSpyAudioManager) - .getStreamVolume(AudioAttributes.toLegacyStreamType(RINGTONE_ATTRIBUTES)); - when(mMockVibrator.hasVibrator()).thenReturn(true); - Ringtone ringtone = - newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .setVibrationEffect(VIBRATION_EFFECT) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - verify(mMockVibrator).hasVibrator(); - - // Verify all the properties. - assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION); - assertThat(ringtone.getUri()).isEqualTo(SOUND_URI); - assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT); - - // Prepare - // The attributes here have haptic channels enabled (unlike above) - verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - // The media player is never played, because sound is muted. - verify(mockMediaPlayer, never()).start(); - when(mockMediaPlayer.isPlaying()).thenReturn(true); - verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); - - // Release - ringtone.stop(); - verify(mockMediaPlayer).release(); - verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); - - verifyZeroInteractions(mMockRemotePlayer); - // Nothing after the initial hasVibrator - it uses audio-coupled. - verifyNoMoreInteractions(mMockVibrator); - } - - @Test - public void testRingtone_nullMediaOnBuilderUsesFallback() throws Exception { - AssetFileDescriptor testResourceFd = - mContext.getResources().openRawResourceFd(R.raw.shortmp3); - // Ensure it will flow as expected. - assertThat(testResourceFd).isNotNull(); - assertThat(testResourceFd.getDeclaredLength()).isAtLeast(0); - mContext.getOrCreateTestableResources() - .addOverride(com.android.internal.R.raw.fallbackring, testResourceFd); - - MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); - Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) - .setUri(null) - .build(); - assertThat(ringtone).isNotNull(); - assertThat(ringtone.isUsingRemotePlayer()).isFalse(); - - // Delegates straight to fallback in local player. - // Prepare - verifyLocalPlayerFallbackSetup(mockMediaPlayer, testResourceFd, RINGTONE_ATTRIBUTES); - verify(mockMediaPlayer).setVolume(1.0f); - verify(mockMediaPlayer).setLooping(false); - verify(mockMediaPlayer).prepare(); - - // Play - ringtone.play(); - verifyLocalPlay(mockMediaPlayer); - - // Release - ringtone.stop(); - verifyLocalStop(mockMediaPlayer); - - verifyNoMoreInteractions(mockMediaPlayer); - verifyNoMoreInteractions(mMockRemotePlayer); - } - - @Test - public void testRingtone_nullMediaOnBuilderUsesFallbackViaRemote() throws Exception { - mContext.getOrCreateTestableResources() - .addOverride(com.android.internal.R.raw.fallbackring, null); - Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) - .setUri(null) - .setLooping(true) // distinct from haptic generator, to match plumbing - .build(); - assertThat(ringtone).isNotNull(); - // Local player fallback fails as the resource isn't found (no media player creation is - // attempted), and then goes on to create the remote player. - assertThat(ringtone.isUsingRemotePlayer()).isTrue(); - - ringtone.play(); - verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), isNull(), - eq(RINGTONE_ATTRIBUTES), eq(false), - eq(MEDIA_SOUND), isNull(), - eq(1.0f), eq(true), eq(false), isNull()); - ringtone.stop(); - verify(mMockRemotePlayer).stop(mIBinderCaptor.getValue()); - verifyNoMoreInteractions(mMockRemotePlayer); - } - - @Test - public void testRingtone_noMediaSetOnBuilderFallbackFailsAndNoRemote() throws Exception { - mContext.getOrCreateTestableResources() - .addOverride(com.android.internal.R.raw.fallbackring, null); - Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) - .setUri(null) - .setLocalOnly() - .build(); - // Local player fallback fails as the resource isn't found (no media player creation is - // attempted), and since there is no local player, the ringtone ends up having nothing to - // do. - assertThat(ringtone).isNull(); - } - - private Ringtone.Builder newBuilder(@Ringtone.RingtoneMedia int ringtoneMedia, - AudioAttributes audioAttributes) { - return new Ringtone.Builder(mContext, ringtoneMedia, audioAttributes) - .setInjectables(mMediaPlayerRule.injectables); - } - - private static AudioAttributes audioAttributes(int audioUsage) { - return new AudioAttributes.Builder() - .setUsage(audioUsage) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build(); - } - - /** Makes the mock get some sort of file access problem. */ - private void setupFileNotFound(MediaPlayer mockMediaPlayer, Uri uri) throws Exception { - doThrow(new FileNotFoundException("Fake file not found")) - .when(mockMediaPlayer).setDataSource(any(Context.class), eq(uri)); - } - - private void verifyLocalPlayerSetup(MediaPlayer mockPlayer, Uri expectedUri, - AudioAttributes expectedAudioAttributes) throws Exception { - verify(mockPlayer).setDataSource(mContext, expectedUri); - verify(mockPlayer).setAudioAttributes(expectedAudioAttributes); - verify(mockPlayer).setPreferredDevice(null); - verify(mockPlayer).prepare(); - } - - private void verifyLocalPlayerFallbackSetup(MediaPlayer mockPlayer, AssetFileDescriptor afd, - AudioAttributes expectedAudioAttributes) throws Exception { - // This is very specific but it's a simple way to test that the test resource matches. - if (afd.getDeclaredLength() < 0) { - verify(mockPlayer).setDataSource(afd.getFileDescriptor()); - } else { - verify(mockPlayer).setDataSource(afd.getFileDescriptor(), - afd.getStartOffset(), - afd.getDeclaredLength()); - } - verify(mockPlayer).setAudioAttributes(expectedAudioAttributes); - verify(mockPlayer).setPreferredDevice(null); - verify(mockPlayer).prepare(); - } - - private void verifyLocalPlay(MediaPlayer mockMediaPlayer) { - verify(mockMediaPlayer).setOnCompletionListener(any()); - verify(mockMediaPlayer).start(); - } - - private void verifyLocalStop(MediaPlayer mockMediaPlayer) { - verify(mockMediaPlayer).stop(); - verify(mockMediaPlayer).setOnCompletionListener(isNull()); - verify(mockMediaPlayer).reset(); - verify(mockMediaPlayer).release(); - } - - /** - * This rule ensures that all expected media player creations from the factory do actually - * occur. The reason for this level of control is that creating a media player is fairly - * expensive and blocking, so we do want unit tests of this class to "declare" interactions - * of all created media players. - * - * This needs to be a TestRule so that the teardown assertions can be skipped if the test has - * failed (and media player assertions may just be a distracting side effect). Otherwise, the - * teardown failures hide the real test ones. - */ - public static class RingtoneInjectablesTrackingTestRule implements TestRule { - public Ringtone.Injectables injectables = new TestInjectables(); - public boolean hapticGeneratorAvailable = true; - - // Queue of (local) media players, in order of expected creation. Enqueue using - // expectNewMediaPlayer(), dequeued by the media player factory passed to Ringtone. - // This queue is asserted to be empty at the end of the test. - private Queue<MediaPlayer> mMockMediaPlayerQueue = new ArrayDeque<>(); - - // Similar to media players, but for haptic generator, which also needs releasing. - private Map<MediaPlayer, HapticGenerator> mMockHapticGeneratorMap = new ArrayMap<>(); - - // Media players with haptic channels. - private ArraySet<MediaPlayer> mHapticChannels = new ArraySet<>(); - - @Override - public Statement apply(Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - base.evaluate(); - // Only assert if the test didn't fail (base.evaluate() would throw). - assertWithMessage("Test setup an expectLocalMediaPlayer but it wasn't consumed") - .that(mMockMediaPlayerQueue).isEmpty(); - // Only assert if the test didn't fail (base.evaluate() would throw). - assertWithMessage( - "Test setup an expectLocalHapticGenerator but it wasn't consumed") - .that(mMockHapticGeneratorMap).isEmpty(); - } - }; - } - - private TestMediaPlayer expectLocalMediaPlayer() { - TestMediaPlayer mockMediaPlayer = Mockito.mock(TestMediaPlayer.class); - // Delegate to simulated methods. This means they can be verified but also reflect - // realistic transitions from the TestMediaPlayer. - doCallRealMethod().when(mockMediaPlayer).start(); - doCallRealMethod().when(mockMediaPlayer).stop(); - doCallRealMethod().when(mockMediaPlayer).setLooping(anyBoolean()); - when(mockMediaPlayer.isLooping()).thenCallRealMethod(); - when(mockMediaPlayer.isLooping()).thenCallRealMethod(); - mMockMediaPlayerQueue.add(mockMediaPlayer); - return mockMediaPlayer; - } - - private HapticGenerator expectHapticGenerator(MediaPlayer mockMediaPlayer) { - HapticGenerator mockHapticGenerator = Mockito.mock(HapticGenerator.class); - // A test should never want this. - assertWithMessage("Can't expect a second haptic generator created " - + "for one media player") - .that(mMockHapticGeneratorMap.put(mockMediaPlayer, mockHapticGenerator)) - .isNull(); - return mockHapticGenerator; - } - - private void setHasHapticChannels(MediaPlayer mp, boolean hasHapticChannels) { - if (hasHapticChannels) { - mHapticChannels.add(mp); - } else { - mHapticChannels.remove(mp); - } - } - - private class TestInjectables extends Ringtone.Injectables { - @Override - public MediaPlayer newMediaPlayer() { - assertWithMessage( - "Unexpected MediaPlayer creation. Bug or need expectNewMediaPlayer") - .that(mMockMediaPlayerQueue) - .isNotEmpty(); - return mMockMediaPlayerQueue.remove(); - } - - @Override - public boolean isHapticGeneratorAvailable() { - return hapticGeneratorAvailable; - } - - @Override - public HapticGenerator createHapticGenerator(MediaPlayer mediaPlayer) { - HapticGenerator mockHapticGenerator = mMockHapticGeneratorMap.remove(mediaPlayer); - assertWithMessage("Unexpected HapticGenerator creation. " - + "Bug or need expectHapticGenerator") - .that(mockHapticGenerator) - .isNotNull(); - return mockHapticGenerator; - } - - @Override - public boolean isHapticPlaybackSupported() { - return true; - } - - @Override - public boolean hasHapticChannels(MediaPlayer mp) { - return mHapticChannels.contains(mp); - } - } - } - - /** - * MediaPlayer relies on a native backend and so its necessary to intercept calls from - * fake usage hitting them. - * - * Mocks don't work directly on native calls, but if they're overridden then it does work. - * Some basic state faking is also done to make the mocks more realistic. - */ - private static class TestMediaPlayer extends MediaPlayer { - private boolean mIsPlaying = false; - private boolean mIsLooping = false; - - @Override - public void start() { - mIsPlaying = true; - } - - @Override - public void stop() { - mIsPlaying = false; - } - - @Override - public void setLooping(boolean value) { - mIsLooping = value; - } - - @Override - public boolean isLooping() { - return mIsLooping; - } - - @Override - public boolean isPlaying() { - return mIsPlaying; - } - - void simulatePlayingFinished() { - if (!mIsPlaying) { - throw new IllegalStateException( - "Attempted to pretend playing finished when not playing"); - } - mIsPlaying = false; - } - } -} diff --git a/media/tests/ringtone/Android.bp b/media/tests/ringtone/Android.bp deleted file mode 100644 index 55b98c4704b1..000000000000 --- a/media/tests/ringtone/Android.bp +++ /dev/null @@ -1,30 +0,0 @@ -package { - // See: http://go/android-license-faq - default_applicable_licenses: ["frameworks_base_license"], -} - -android_test { - name: "MediaRingtoneTests", - - srcs: ["src/**/*.java"], - - libs: [ - "android.test.runner", - "android.test.base", - ], - - static_libs: [ - "androidx.test.rules", - "testng", - "androidx.test.ext.truth", - "frameworks-base-testutils", - ], - - test_suites: [ - "device-tests", - "automotive-tests", - ], - - platform_apis: true, - certificate: "platform", -} diff --git a/media/tests/ringtone/AndroidManifest.xml b/media/tests/ringtone/AndroidManifest.xml deleted file mode 100644 index 27eda07cd0d3..000000000000 --- a/media/tests/ringtone/AndroidManifest.xml +++ /dev/null @@ -1,41 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright 2023 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.framework.base.media.ringtone.tests"> - - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - <uses-permission android:name="android.permission.MANAGE_USERS" /> - - <application android:debuggable="true"> - <uses-library android:name="android.test.runner" /> - - <activity android:name="MediaRingtoneTests" - android:label="Media Ringtone Tests" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - - </application> - - <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" - android:targetPackage="com.android.framework.base.media.ringtone.tests" - android:label="Media Ringtone Tests"/> -</manifest> diff --git a/media/tests/ringtone/TEST_MAPPING b/media/tests/ringtone/TEST_MAPPING deleted file mode 100644 index 6f25c147076c..000000000000 --- a/media/tests/ringtone/TEST_MAPPING +++ /dev/null @@ -1,20 +0,0 @@ -{ - "presubmit": [ - { - "name": "MediaRingtoneTests", - "options": [ - {"exclude-annotation": "androidx.test.filters.LargeTest"}, - {"exclude-annotation": "androidx.test.filters.FlakyTest"}, - {"exclude-annotation": "org.junit.Ignore"} - ] - } - ], - "postsubmit": [ - { - "name": "MediaRingtoneTests", - "options": [ - {"exclude-annotation": "org.junit.Ignore"} - ] - } - ] -}
\ No newline at end of file diff --git a/media/tests/ringtone/res/raw/test_haptic_file.ahv b/media/tests/ringtone/res/raw/test_haptic_file.ahv deleted file mode 100644 index d6eba1a4d7ba..000000000000 --- a/media/tests/ringtone/res/raw/test_haptic_file.ahv +++ /dev/null @@ -1,17 +0,0 @@ -<vibration-effect> - <waveform-effect> - <waveform-entry durationMs="63" amplitude="255"/> - <waveform-entry durationMs="63" amplitude="231"/> - <waveform-entry durationMs="63" amplitude="208"/> - <waveform-entry durationMs="63" amplitude="185"/> - <waveform-entry durationMs="63" amplitude="162"/> - <waveform-entry durationMs="63" amplitude="139"/> - <waveform-entry durationMs="63" amplitude="115"/> - <waveform-entry durationMs="63" amplitude="92"/> - <waveform-entry durationMs="63" amplitude="69"/> - <waveform-entry durationMs="63" amplitude="46"/> - <waveform-entry durationMs="63" amplitude="23"/> - <waveform-entry durationMs="63" amplitude="0"/> - <waveform-entry durationMs="1250" amplitude="0"/> - </waveform-effect> -</vibration-effect> diff --git a/media/tests/ringtone/res/raw/test_sound_file.mp3 b/media/tests/ringtone/res/raw/test_sound_file.mp3 Binary files differdeleted file mode 100644 index c1b2fdf93991..000000000000 --- a/media/tests/ringtone/res/raw/test_sound_file.mp3 +++ /dev/null diff --git a/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java b/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java deleted file mode 100644 index a92b29883ce7..000000000000 --- a/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.media; - -import static com.google.android.mms.ContentType.AUDIO_MP3; -import static com.google.common.truth.Truth.assertThat; - -import static org.junit.Assert.assertThrows; -import static org.junit.Assume.assumeTrue; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Environment; -import android.os.ParcelFileDescriptor; -import android.os.SystemClock; -import android.os.vibrator.persistence.VibrationXmlParser; -import android.provider.MediaStore; -import android.text.TextUtils; - -import androidx.test.platform.app.InstrumentationRegistry; - -import com.android.framework.base.media.ringtone.tests.R; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.io.FileOutputStream; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -@RunWith(Parameterized.class) -public class RingtoneManagerTest { - @RingtoneManager.MediaType - private final int mMediaType; - private final List<Uri> mAddedFilesUri; - private Context mContext; - private RingtoneManager mRingtoneManager; - private long mTimestamp; - - @Parameterized.Parameters(name = "media = {0}") - public static Iterable<?> data() { - return Arrays.asList(Ringtone.MEDIA_SOUND, Ringtone.MEDIA_VIBRATION); - } - - public RingtoneManagerTest(@RingtoneManager.MediaType int mediaType) { - mMediaType = mediaType; - mAddedFilesUri = new ArrayList<>(); - } - - @Before - public void setUp() throws Exception { - mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - mTimestamp = SystemClock.uptimeMillis(); - mRingtoneManager = new RingtoneManager(mContext); - mRingtoneManager.setMediaType(mMediaType); - } - - @After - public void tearDown() { - // Clean up media store - for (Uri fileUri : mAddedFilesUri) { - mContext.getContentResolver().delete(fileUri, null); - } - } - - @Test - public void testSetMediaType_withValidValue_setsMediaCorrectly() { - mRingtoneManager.setMediaType(mMediaType); - assertThat(mRingtoneManager.getMediaType()).isEqualTo(mMediaType); - } - - @Test - public void testSetMediaType_withInvalidValue_throwsException() { - assertThrows(IllegalArgumentException.class, () -> mRingtoneManager.setMediaType(999)); - } - - @Test - public void testSetMediaType_afterCallingGetCursor_throwsException() { - mRingtoneManager.getCursor(); - assertThrows(IllegalStateException.class, () -> mRingtoneManager.setMediaType(mMediaType)); - } - - @Test - public void testGetRingtone_ringtoneHasCorrectTitle() throws Exception { - String fileName = generateUniqueFileName("new_file"); - Ringtone ringtone = addNewRingtoneToMediaStore(mRingtoneManager, fileName); - - assertThat(ringtone.getTitle(mContext)).isEqualTo(fileName); - } - - @Test - public void testGetRingtone_ringtoneCanBePlayedAndStopped() throws Exception { - //TODO(b/261571543) Remove this assumption once we support playing vibrations. - assumeTrue(mMediaType == Ringtone.MEDIA_SOUND); - String fileName = generateUniqueFileName("new_file"); - Ringtone ringtone = addNewRingtoneToMediaStore(mRingtoneManager, fileName); - - ringtone.play(); - assertThat(ringtone.isPlaying()).isTrue(); - - ringtone.stop(); - assertThat(ringtone.isPlaying()).isFalse(); - } - - @Test - public void testGetCursor_withDifferentMedia_returnsCorrectCursor() throws Exception { - RingtoneManager audioRingtoneManager = new RingtoneManager(mContext); - String audioFileName = generateUniqueFileName("ringtone"); - addNewRingtoneToMediaStore(audioRingtoneManager, audioFileName); - - RingtoneManager vibrationRingtoneManager = new RingtoneManager(mContext); - vibrationRingtoneManager.setMediaType(Ringtone.MEDIA_VIBRATION); - String vibrationFileName = generateUniqueFileName("vibration"); - addNewRingtoneToMediaStore(vibrationRingtoneManager, vibrationFileName); - - Cursor audioCursor = audioRingtoneManager.getCursor(); - Cursor vibrationCursor = vibrationRingtoneManager.getCursor(); - - List<String> audioTitles = extractRecordTitles(audioCursor); - List<String> vibrationTitles = extractRecordTitles(vibrationCursor); - - assertThat(audioTitles).contains(audioFileName); - assertThat(audioTitles).doesNotContain(vibrationFileName); - - assertThat(vibrationTitles).contains(vibrationFileName); - assertThat(vibrationTitles).doesNotContain(audioFileName); - } - - private List<String> extractRecordTitles(Cursor cursor) { - List<String> titles = new ArrayList<>(); - - if (cursor.moveToFirst()) { - do { - String title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX); - titles.add(title); - } while (cursor.moveToNext()); - } - - return titles; - } - - private Ringtone addNewRingtoneToMediaStore(RingtoneManager ringtoneManager, String fileName) - throws Exception { - Uri fileUri = ringtoneManager.getMediaType() == Ringtone.MEDIA_SOUND ? addAudioFile( - fileName) : addVibrationFile(fileName); - mAddedFilesUri.add(fileUri); - - int ringtonePosition = ringtoneManager.getRingtonePosition(fileUri); - Ringtone ringtone = ringtoneManager.getRingtone(ringtonePosition); - // Validate this is the expected ringtone. - assertThat(ringtone.getUri()).isEqualTo(fileUri); - return ringtone; - } - - private Uri addAudioFile(String fileName) throws Exception { - ContentResolver resolver = mContext.getContentResolver(); - ContentValues contentValues = new ContentValues(); - contentValues.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName + ".mp3"); - contentValues.put(MediaStore.Audio.Media.RELATIVE_PATH, Environment.DIRECTORY_RINGTONES); - contentValues.put(MediaStore.Audio.Media.MIME_TYPE, AUDIO_MP3); - contentValues.put(MediaStore.Audio.Media.TITLE, fileName); - contentValues.put(MediaStore.Audio.Media.IS_RINGTONE, 1); - - Uri contentUri = resolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - contentValues); - writeRawDataToFile(resolver, contentUri, R.raw.test_sound_file); - - return resolver.canonicalizeOrElse(contentUri); - } - - private Uri addVibrationFile(String fileName) throws Exception { - ContentResolver resolver = mContext.getContentResolver(); - ContentValues contentValues = new ContentValues(); - contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName + ".ahv"); - contentValues.put(MediaStore.Files.FileColumns.RELATIVE_PATH, - Environment.DIRECTORY_DOWNLOADS); - contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE, - VibrationXmlParser.APPLICATION_VIBRATION_XML_MIME_TYPE); - contentValues.put(MediaStore.Files.FileColumns.TITLE, fileName); - - Uri contentUri = resolver.insert(MediaStore.Files.getContentUri(MediaStore - .VOLUME_EXTERNAL), contentValues); - writeRawDataToFile(resolver, contentUri, R.raw.test_haptic_file); - - return resolver.canonicalizeOrElse(contentUri); - } - - private void writeRawDataToFile(ContentResolver resolver, Uri contentUri, int rawResource) - throws Exception { - try (ParcelFileDescriptor pfd = - resolver.openFileDescriptor(contentUri, "w", null)) { - InputStream inputStream = mContext.getResources().openRawResource(rawResource); - FileOutputStream outputStream = new FileOutputStream(pfd.getFileDescriptor()); - outputStream.write(inputStream.readAllBytes()); - - inputStream.close(); - outputStream.flush(); - outputStream.close(); - - } catch (Exception e) { - throw new Exception("Failed to write data to file", e); - } - } - - private String generateUniqueFileName(String prefix) { - return TextUtils.formatSimple("%s_%d", prefix, mTimestamp); - } - -} diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt index 9a2cf61d2b86..e7d1072c352d 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt @@ -40,7 +40,7 @@ fun Intent.parseCancelUiRequest(packageManager: PackageManager): Request? = Log.d(TAG, "Received UI cancel request, shouldShowCancellationUi: $this") } if (showCancel) { - val appLabel = packageManager.appLabel(cancelUiRequest.appPackageName) + val appLabel = packageManager.appLabel(cancelUiRequest.packageName) if (appLabel == null) { Log.d(TAG, "Received UI cancel request with an invalid package name.") null diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 0ccb07a6d475..30973879de10 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -58,6 +58,7 @@ class CredentialManagerRepo( private val providerEnabledList: List<ProviderData> private val providerDisabledList: List<DisabledProviderData>? val resultReceiver: ResultReceiver? + val finalResponseReceiver: ResultReceiver? var initialUiState: UiState @@ -105,6 +106,11 @@ class CredentialManagerRepo( ResultReceiver::class.java ) + finalResponseReceiver = intent.getParcelableExtra( + Constants.EXTRA_FINAL_RESPONSE_RECEIVER, + ResultReceiver::class.java + ) + isReqForAllOptions = intent.getBooleanExtra( Constants.EXTRA_REQ_FOR_ALL_OPTIONS, /*defaultValue=*/ false @@ -113,7 +119,7 @@ class CredentialManagerRepo( val cancellationRequest = getCancelUiRequest(intent) val cancelUiRequestState = cancellationRequest?.let { - CancelUiRequestState(getAppLabel(context.getPackageManager(), it.appPackageName)) + CancelUiRequestState(getAppLabel(context.getPackageManager(), it.packageName)) } initialUiState = when (requestInfo?.type) { @@ -200,7 +206,7 @@ class CredentialManagerRepo( } fun onCancel(cancelCode: Int) { - sendCancellationCode(cancelCode, requestInfo?.token, resultReceiver) + sendCancellationCode(cancelCode, requestInfo?.token, resultReceiver, finalResponseReceiver) } fun onOptionSelected( @@ -219,6 +225,10 @@ class CredentialManagerRepo( ) val resultDataBundle = Bundle() UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle) + + resultDataBundle.putParcelable(Constants.EXTRA_FINAL_RESPONSE_RECEIVER, + finalResponseReceiver) + resultReceiver?.send( BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, resultDataBundle @@ -286,10 +296,14 @@ class CredentialManagerRepo( fun sendCancellationCode( cancelCode: Int, requestToken: IBinder?, - resultReceiver: ResultReceiver? + resultReceiver: ResultReceiver?, + finalResponseReceiver: ResultReceiver? ) { if (requestToken != null && resultReceiver != null) { val resultData = Bundle() + resultData.putParcelable(Constants.EXTRA_FINAL_RESPONSE_RECEIVER, + finalResponseReceiver) + BaseDialogResult.addToBundle(BaseDialogResult(requestToken), resultData) resultReceiver.send(cancelCode, resultData) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 05aa5489ff36..4771237d449f 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -135,7 +135,7 @@ class CredentialSelectorActivity : ComponentActivity() { Log.d( Constants.LOG_TAG, "Received UI cancellation intent. Should show cancellation" + " ui = $shouldShowCancellationUi") - val appDisplayName = getAppLabel(packageManager, cancelUiRequest.appPackageName) + val appDisplayName = getAppLabel(packageManager, cancelUiRequest.packageName) if (!shouldShowCancellationUi) { this.finish() } @@ -216,13 +216,18 @@ class CredentialSelectorActivity : ComponentActivity() { android.credentials.selection.Constants.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java ) + val finalResponseResultReceiver = intent.getParcelableExtra( + android.credentials.selection.Constants.EXTRA_FINAL_RESPONSE_RECEIVER, + ResultReceiver::class.java + ) + val requestInfo = intent.extras?.getParcelable( RequestInfo.EXTRA_REQUEST_INFO, RequestInfo::class.java ) CredentialManagerRepo.sendCancellationCode( BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE, - requestInfo?.token, resultReceiver + requestInfo?.token, resultReceiver, finalResponseResultReceiver ) this.finish() } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 6c5a984f20f7..f4da1e6c4770 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -72,7 +72,7 @@ class CredentialSelectorViewModel( init { uiMetrics.logNormal(LifecycleEvent.CREDMAN_ACTIVITY_INIT, - credManRepo.requestInfo?.appPackageName) + credManRepo.requestInfo?.packageName) } /**************************************************************************/ @@ -107,7 +107,7 @@ class CredentialSelectorViewModel( if (this.credManRepo.requestInfo?.token != credManRepo.requestInfo?.token) { this.uiMetrics.resetInstanceId() this.uiMetrics.logNormal(LifecycleEvent.CREDMAN_ACTIVITY_NEW_REQUEST, - credManRepo.requestInfo?.appPackageName) + credManRepo.requestInfo?.packageName) } } @@ -189,7 +189,7 @@ class CredentialSelectorViewModel( private fun onInternalError() { Log.w(Constants.LOG_TAG, "UI closed due to illegal internal state") this.uiMetrics.logNormal(LifecycleEvent.CREDMAN_ACTIVITY_INTERNAL_ERROR, - credManRepo.requestInfo?.appPackageName) + credManRepo.requestInfo?.packageName) credManRepo.onParsingFailureCancel() uiState = uiState.copy(dialogState = DialogState.COMPLETE) } @@ -399,6 +399,6 @@ class CredentialSelectorViewModel( @Composable fun logUiEvent(uiEventEnum: UiEventEnum) { - this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo?.appPackageName) + this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo?.packageName) } }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 64595e2642f5..997c45e84180 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -195,7 +195,7 @@ class GetFlowUtils { } return com.android.credentialmanager.getflow.RequestDisplayInfo( appName = originName?.ifEmpty { null } - ?: getAppLabel(context.packageManager, requestInfo.appPackageName) + ?: getAppLabel(context.packageManager, requestInfo.packageName) ?: return null, preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials, preferIdentityDocUi = getCredentialRequest.data.getBoolean( @@ -269,7 +269,7 @@ class CreateFlowUtils { return null } val appLabel = originName?.ifEmpty { null } - ?: getAppLabel(context.packageManager, requestInfo.appPackageName) + ?: getAppLabel(context.packageManager, requestInfo.packageName) ?: return null val createCredentialRequest = requestInfo.createCredentialRequest ?: return null val createCredentialRequestJetpack = CreateCredentialRequest.createFrom( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 2628f0932797..8fde5d78c498 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -19,13 +19,12 @@ package com.android.credentialmanager.autofill import android.app.PendingIntent import android.app.assist.AssistStructure import android.content.Context -import android.credentials.Credential +import android.content.Intent import android.credentials.CredentialManager -import android.credentials.CredentialOption -import android.credentials.GetCandidateCredentialsException -import android.credentials.GetCandidateCredentialsResponse import android.credentials.GetCredentialRequest -import android.credentials.GetCredentialResponse +import android.credentials.GetCandidateCredentialsResponse +import android.credentials.GetCandidateCredentialsException +import android.credentials.CredentialOption import android.credentials.selection.Entry import android.credentials.selection.GetCredentialProviderData import android.credentials.selection.ProviderData @@ -47,7 +46,6 @@ import android.service.autofill.SaveRequest import android.service.credentials.CredentialProviderService import android.util.Log import android.view.autofill.AutofillId -import android.view.autofill.AutofillValue import android.view.autofill.IAutoFillManagerClient import android.widget.RemoteViews import android.widget.inline.InlinePresentationSpec @@ -131,30 +129,7 @@ class CredentialAutofillService : AutofillService() { val outcome = object : OutcomeReceiver<GetCandidateCredentialsResponse, GetCandidateCredentialsException> { override fun onResult(result: GetCandidateCredentialsResponse) { - Log.i(TAG, "getCandidateCredentials onResponse") - - if (result.getCredentialResponse != null) { - val autofillId: AutofillId? = result.getCredentialResponse - .credential.data.getParcelable( - CredentialProviderService.EXTRA_AUTOFILL_ID, - AutofillId::class.java) - Log.i(TAG, "getCandidateCredentials final response, autofillId: " + - autofillId) - - if (autofillId != null) { - autofillCallback.autofill( - sessionId, - mutableListOf(autofillId), - mutableListOf( - AutofillValue.forText( - convertResponseToJson(result.getCredentialResponse) - ) - ), - false) - } - return - } - + Log.i(TAG, "getCandidateCredentials onResult") val fillResponse = convertToFillResponse(result, request, responseClientState) if (fillResponse != null) { @@ -181,57 +156,6 @@ class CredentialAutofillService : AutofillService() { ) } - // TODO(b/318118018): Use from Jetpack - private fun convertResponseToJson(response: GetCredentialResponse): String? { - try { - val jsonObject = JSONObject() - jsonObject.put("type", "get") - val jsonCred = JSONObject() - jsonCred.put("type", response.credential.type) - jsonCred.put("data", credentialToJSON( - response.credential)) - jsonObject.put("credential", jsonCred) - return jsonObject.toString() - } catch (e: JSONException) { - Log.i( - TAG, "Exception while constructing response JSON: " + - e.message - ) - } - return null - } - - // TODO(b/318118018): Replace with calls to Jetpack - private fun credentialToJSON(credential: Credential): JSONObject? { - Log.i(TAG, "credentialToJSON") - try { - if (credential.type == "android.credentials.TYPE_PASSWORD_CREDENTIAL") { - Log.i(TAG, "toJSON PasswordCredential") - - val json = JSONObject() - val id = credential.data.getString("androidx.credentials.BUNDLE_KEY_ID") - val pass = credential.data.getString("androidx.credentials.BUNDLE_KEY_PASSWORD") - json.put("androidx.credentials.BUNDLE_KEY_ID", id) - json.put("androidx.credentials.BUNDLE_KEY_PASSWORD", pass) - return json - } else if (credential.type == "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL") { - Log.i(TAG, "toJSON PublicKeyCredential") - - val json = JSONObject() - val responseJson = credential - .data - .getString("androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON") - json.put("androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON", - responseJson) - return json - } - } catch (e: JSONException) { - Log.i(TAG, "issue while converting credential response to JSON") - } - Log.i(TAG, "Unsupported credential type") - return null - } - private fun getEntryToIconMap( candidateProviderDataList: List<GetCredentialProviderData> ): Map<String, Icon> { @@ -275,6 +199,7 @@ class CredentialAutofillService : AutofillService() { val autofillIdToProvidersMap: Map<AutofillId, ArrayList<GetCredentialProviderData>> = mapAutofillIdToProviders(candidateProviders) val fillResponseBuilder = FillResponse.Builder() + fillResponseBuilder.setFlags(FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE) var validFillResponse = false autofillIdToProvidersMap.forEach { (autofillId, providers) -> validFillResponse = processProvidersForAutofillId( @@ -387,7 +312,7 @@ class CredentialAutofillService : AutofillService() { presentationBuilder.build()) .build()) .setAuthentication(pendingIntent.intentSender) - .setAuthenticationExtras(fillInIntent.extras) + .setCredentialFillInIntent(fillInIntent) .build()) datasetAdded = true i++ @@ -407,11 +332,11 @@ class CredentialAutofillService : AutofillService() { } private fun createInlinePresentation( - primaryEntry: CredentialEntryInfo, - pendingIntent: PendingIntent, - icon: Icon, - spec: InlinePresentationSpec, - duplicateDisplayNameForPasskeys: MutableMap<String, Boolean> + primaryEntry: CredentialEntryInfo, + pendingIntent: PendingIntent, + icon: Icon, + spec: InlinePresentationSpec, + duplicateDisplayNameForPasskeys: MutableMap<String, Boolean> ): InlinePresentation { val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY && primaryEntry.displayName != null) { @@ -437,7 +362,8 @@ class CredentialAutofillService : AutofillService() { fillResponseBuilder: FillResponse.Builder ) { val presentationBuilder = Presentations.Builder() - .setMenuPresentation(RemoteViewsFactory.createMoreSignInOptionsPresentation(this)) + .setMenuPresentation( + RemoteViewsFactory.createMoreSignInOptionsPresentation(this)) fillResponseBuilder.addDataset( Dataset.Builder() @@ -477,9 +403,8 @@ class CredentialAutofillService : AutofillService() { .setInlinePresentation(InlinePresentation( sliceBuilder.build().slice, spec, /* pinned= */ true)) - val extraBundle = Bundle() - extraBundle.putParcelableArrayList( - ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, providerDataList) + val extrasIntent = Intent() + extrasIntent.putExtra(ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, providerDataList) fillResponseBuilder.addDataset( dataSetBuilder @@ -489,7 +414,7 @@ class CredentialAutofillService : AutofillService() { presentationBuilder.build()) .build()) .setAuthentication(bottomSheetPendingIntent.intentSender) - .setAuthenticationExtras(extraBundle) + .setCredentialFillInIntent(extrasIntent) .build() ) } @@ -640,7 +565,6 @@ class CredentialAutofillService : AutofillService() { autofillId: AutofillId, responseClientState: Bundle ): List<CredentialOption> { - // TODO(b/293945193) Replace with isCredential check from viewNode val credentialHints: MutableList<String> = mutableListOf() if (viewNode.autofillHints != null) { for (hint in viewNode.autofillHints!!) { diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt index 3297991e2504..5590219bc011 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt @@ -76,7 +76,7 @@ fun CredentialsScreenChip( Chip( label = labelParam, onClick = onClick, - modifier = modifier, + modifier = modifier.fillMaxWidth(), secondaryLabel = secondaryLabelParam, icon = iconParam, colors = colors, @@ -104,7 +104,6 @@ fun SignInOptionsChip(onClick: () -> Unit) { label = stringResource(R.string.dialog_sign_in_options_button), onClick = onClick, modifier = Modifier - .fillMaxWidth() .padding(top = TOPPADDING) ) } @@ -121,7 +120,6 @@ fun ContinueChip(onClick: () -> Unit) { label = stringResource(R.string.dialog_continue_button), onClick = onClick, modifier = Modifier - .fillMaxWidth() .padding(top = TOPPADDING), colors = ChipDefaults.primaryChipColors(), ) @@ -139,7 +137,6 @@ fun DismissChip(onClick: () -> Unit) { label = stringResource(R.string.dialog_dismiss_button), onClick = onClick, modifier = Modifier - .fillMaxWidth() .padding(top = TOPPADDING), ) } diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml index f425f52804c5..324293532db4 100644 --- a/packages/PackageInstaller/res/values/strings.xml +++ b/packages/PackageInstaller/res/values/strings.xml @@ -288,7 +288,7 @@ cannot happen immediately because the device is offline (has no internet connection. [CHAR LIMIT=none] --> <string name="unarchive_error_offline_body"> - This app will automatically restore when you\'re connected to the internet + To restore this app, check your internet connection and try again </string> <!-- Dialog title shown when the user is trying to restore an app but a generic error happened. diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/model/RoutingSession.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/RoutingSession.kt new file mode 100644 index 000000000000..a98f3e2a4dc6 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/RoutingSession.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.data.model + +import android.media.RoutingSessionInfo + +/** Models a routing session which is created when a media route is selected. */ +data class RoutingSession( + val routingSessionInfo: RoutingSessionInfo, + val isVolumeSeekBarEnabled: Boolean, + val isMediaOutputDisabled: Boolean, +) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt index 6761aa7a1006..f729c04fb849 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -16,15 +16,12 @@ package com.android.settingslib.volume.data.repository -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.AudioManager.OnCommunicationDeviceChangedListener import androidx.concurrent.futures.DirectExecutor import com.android.internal.util.ConcurrentUtils +import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode @@ -32,7 +29,6 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow @@ -40,7 +36,6 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -77,7 +72,7 @@ interface AudioRepository { } class AudioRepositoryImpl( - private val context: Context, + private val audioManagerIntentsReceiver: AudioManagerIntentsReceiver, private val audioManager: AudioManager, private val backgroundCoroutineContext: CoroutineContext, private val coroutineScope: CoroutineScope, @@ -93,30 +88,9 @@ class AudioRepositoryImpl( .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode) - private val audioManagerIntents: SharedFlow<String> = - callbackFlow { - val receiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent) { - intent.action?.let { action -> launch { send(action) } } - } - } - context.registerReceiver( - receiver, - IntentFilter().apply { - for (action in allActions) { - addAction(action) - } - } - ) - - awaitClose { context.unregisterReceiver(receiver) } - } - .shareIn(coroutineScope, SharingStarted.WhileSubscribed()) - override val ringerMode: StateFlow<RingerMode> = - audioManagerIntents - .filter { ringerActions.contains(it) } + audioManagerIntentsReceiver.intents + .filter { AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION == it.action } .map { RingerMode(audioManager.ringerModeInternal) } .flowOn(backgroundCoroutineContext) .stateIn( @@ -146,8 +120,7 @@ class AudioRepositoryImpl( ) override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> { - return audioManagerIntents - .filter { modelActions.contains(it) } + return audioManagerIntentsReceiver.intents .map { getCurrentAudioStream(audioStream) } .flowOn(backgroundCoroutineContext) } @@ -189,20 +162,4 @@ class AudioRepositoryImpl( // return STREAM_VOICE_CALL in getAudioStream audioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL) } - - private companion object { - val modelActions = - setOf( - AudioManager.STREAM_MUTE_CHANGED_ACTION, - AudioManager.MASTER_MUTE_CHANGED_ACTION, - AudioManager.VOLUME_CHANGED_ACTION, - AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, - AudioManager.STREAM_DEVICES_CHANGED_ACTION, - ) - val ringerActions = - setOf( - AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, - ) - val allActions = ringerActions + modelActions - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt index 1597b7712863..aa9ae76c66c4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt @@ -15,8 +15,13 @@ */ package com.android.settingslib.volume.data.repository +import android.media.AudioManager +import android.media.MediaRouter2Manager +import android.media.RoutingSessionInfo import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice +import com.android.settingslib.volume.data.model.RoutingSession +import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose @@ -24,10 +29,15 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext /** Repository providing data about connected media devices. */ interface LocalMediaRepository { @@ -37,43 +47,55 @@ interface LocalMediaRepository { /** Currently connected media device */ val currentConnectedDevice: StateFlow<MediaDevice?> + + val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> + + suspend fun adjustSessionVolume(sessionId: String?, volume: Int) } class LocalMediaRepositoryImpl( + audioManagerIntentsReceiver: AudioManagerIntentsReceiver, private val localMediaManager: LocalMediaManager, + private val mediaRouter2Manager: MediaRouter2Manager, coroutineScope: CoroutineScope, - backgroundContext: CoroutineContext, + private val backgroundContext: CoroutineContext, ) : LocalMediaRepository { - private val deviceUpdates: Flow<DevicesUpdate> = callbackFlow { - val callback = - object : LocalMediaManager.DeviceCallback { - override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) { - trySend(DevicesUpdate.DeviceListUpdate(newDevices ?: emptyList())) - } + private val devicesChanges = + audioManagerIntentsReceiver.intents.filter { + AudioManager.STREAM_DEVICES_CHANGED_ACTION == it.action + } + private val mediaDevicesUpdates: Flow<DevicesUpdate> = + callbackFlow { + val callback = + object : LocalMediaManager.DeviceCallback { + override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) { + trySend(DevicesUpdate.DeviceListUpdate(newDevices ?: emptyList())) + } - override fun onSelectedDeviceStateChanged( - device: MediaDevice?, - state: Int, - ) { - trySend(DevicesUpdate.SelectedDeviceStateChanged) - } + override fun onSelectedDeviceStateChanged( + device: MediaDevice?, + state: Int, + ) { + trySend(DevicesUpdate.SelectedDeviceStateChanged) + } - override fun onDeviceAttributesChanged() { - trySend(DevicesUpdate.DeviceAttributesChanged) + override fun onDeviceAttributesChanged() { + trySend(DevicesUpdate.DeviceAttributesChanged) + } + } + localMediaManager.registerCallback(callback) + localMediaManager.startScan() + + awaitClose { + localMediaManager.stopScan() + localMediaManager.unregisterCallback(callback) } } - localMediaManager.registerCallback(callback) - localMediaManager.startScan() - - awaitClose { - localMediaManager.stopScan() - localMediaManager.unregisterCallback(callback) - } - } + .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0) override val mediaDevices: StateFlow<Collection<MediaDevice>> = - deviceUpdates + mediaDevicesUpdates .mapNotNull { if (it is DevicesUpdate.DeviceListUpdate) { it.newDevices ?: emptyList() @@ -85,7 +107,7 @@ class LocalMediaRepositoryImpl( .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) override val currentConnectedDevice: StateFlow<MediaDevice?> = - deviceUpdates + merge(devicesChanges, mediaDevicesUpdates) .map { localMediaManager.currentConnectedDevice } .stateIn( coroutineScope, @@ -93,6 +115,30 @@ class LocalMediaRepositoryImpl( localMediaManager.currentConnectedDevice ) + override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> = + merge(devicesChanges, mediaDevicesUpdates) + .onStart { emit(Unit) } + .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) } + .flowOn(backgroundContext) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) + + override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) { + withContext(backgroundContext) { + if (sessionId == null) { + localMediaManager.adjustSessionVolume(volume) + } else { + localMediaManager.adjustSessionVolume(sessionId, volume) + } + } + } + + private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession = + RoutingSession( + info, + isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(), + isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info) + ) + private sealed interface DevicesUpdate { data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt index 93aa90d11678..ab8c6b820177 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt @@ -16,30 +16,23 @@ package com.android.settingslib.volume.data.repository -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter import android.media.AudioManager import android.media.session.MediaController import android.media.session.MediaSessionManager import android.media.session.PlaybackState import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.headsetAudioModeChanges +import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch /** Provides controllers for currently active device media sessions. */ interface MediaControllerRepository { @@ -49,40 +42,25 @@ interface MediaControllerRepository { } class MediaControllerRepositoryImpl( - private val context: Context, + audioManagerIntentsReceiver: AudioManagerIntentsReceiver, private val mediaSessionManager: MediaSessionManager, localBluetoothManager: LocalBluetoothManager?, coroutineScope: CoroutineScope, backgroundContext: CoroutineContext, ) : MediaControllerRepository { - private val devicesChanges: Flow<Unit> = - callbackFlow { - val receiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (AudioManager.STREAM_DEVICES_CHANGED_ACTION == intent?.action) { - launch { send(Unit) } - } - } - } - context.registerReceiver( - receiver, - IntentFilter(AudioManager.STREAM_DEVICES_CHANGED_ACTION) - ) - - awaitClose { context.unregisterReceiver(receiver) } - } - .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0) - + private val devicesChanges = + audioManagerIntentsReceiver.intents.filter { + AudioManager.STREAM_DEVICES_CHANGED_ACTION == it.action + } override val activeMediaController: StateFlow<MediaController?> = - combine( - localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) } - ?: emptyFlow(), - devicesChanges.onStart { emit(Unit) }, - ) { _, _ -> - getActiveLocalMediaController() + buildList { + localBluetoothManager?.headsetAudioModeChanges?.let { add(it) } + add(devicesChanges) } + .merge() + .onStart { emit(Unit) } + .map { getActiveLocalMediaController() } .flowOn(backgroundContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt new file mode 100644 index 000000000000..f6213351ae0d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.domain.interactor + +import com.android.settingslib.media.MediaDevice +import com.android.settingslib.volume.data.repository.LocalMediaRepository +import com.android.settingslib.volume.domain.model.RoutingSession +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class LocalMediaInteractor( + private val repository: LocalMediaRepository, + coroutineScope: CoroutineScope, +) { + + /** Available devices list */ + val mediaDevices: StateFlow<Collection<MediaDevice>> + get() = repository.mediaDevices + + /** Currently connected media device */ + val currentConnectedDevice: StateFlow<MediaDevice?> + get() = repository.currentConnectedDevice + + val remoteRoutingSessions: StateFlow<List<RoutingSession>> = + repository.remoteRoutingSessions + .map { sessions -> + sessions.map { + RoutingSession( + routingSessionInfo = it.routingSessionInfo, + isMediaOutputDisabled = it.isMediaOutputDisabled, + isVolumeSeekBarEnabled = + it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0 + ) + } + } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) + + suspend fun adjustSessionVolume(sessionId: String?, volume: Int) = + repository.adjustSessionVolume(sessionId, volume) +} diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/model/RoutingSession.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/model/RoutingSession.kt new file mode 100644 index 000000000000..dfc4703e50a6 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/model/RoutingSession.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.domain.model + +import android.media.RoutingSessionInfo + +/** Models a routing session which is created when a media route is selected. */ +data class RoutingSession( + val routingSessionInfo: RoutingSessionInfo, + val isMediaOutputDisabled: Boolean, + val isVolumeSeekBarEnabled: Boolean, +) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerIntentsReceiver.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerIntentsReceiver.kt new file mode 100644 index 000000000000..9fa4c86cdea1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerIntentsReceiver.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.shared + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.media.AudioManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch + +/** Exposes [AudioManager] intents as a observable shared flow. */ +interface AudioManagerIntentsReceiver { + + val intents: SharedFlow<Intent> +} + +class AudioManagerIntentsReceiverImpl( + private val context: Context, + coroutineScope: CoroutineScope, +) : AudioManagerIntentsReceiver { + + private val allActions: Collection<String> + get() = + setOf( + AudioManager.STREAM_MUTE_CHANGED_ACTION, + AudioManager.MASTER_MUTE_CHANGED_ACTION, + AudioManager.VOLUME_CHANGED_ACTION, + AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION, + AudioManager.STREAM_DEVICES_CHANGED_ACTION, + ) + + override val intents: SharedFlow<Intent> = + callbackFlow { + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + launch { send(intent) } + } + } + context.registerReceiver( + receiver, + IntentFilter().apply { + for (action in allActions) { + addAction(action) + } + } + ) + + awaitClose { context.unregisterReceiver(receiver) } + } + .filterNotNull() + .filter { intent -> allActions.contains(intent.action) } + .shareIn(coroutineScope, SharingStarted.WhileSubscribed()) +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt index 7b70c64f349b..48b04db5b50b 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt @@ -16,13 +16,11 @@ package com.android.settingslib.volume.data.repository -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent import android.media.AudioDeviceInfo import android.media.AudioManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.volume.shared.FakeAudioManagerIntentsReceiver import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode @@ -30,6 +28,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -51,17 +50,16 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class AudioRepositoryTest { - @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver> @Captor private lateinit var modeListenerCaptor: ArgumentCaptor<AudioManager.OnModeChangedListener> @Captor private lateinit var communicationDeviceListenerCaptor: ArgumentCaptor<AudioManager.OnCommunicationDeviceChangedListener> - @Mock private lateinit var context: Context @Mock private lateinit var audioManager: AudioManager @Mock private lateinit var communicationDevice: AudioDeviceInfo + private val intentsReceiver = FakeAudioManagerIntentsReceiver() private val volumeByStream: MutableMap<Int, Int> = mutableMapOf() private val isAffectedByRingerModeByStream: MutableMap<Int, Boolean> = mutableMapOf() private val isMuteByStream: MutableMap<Int, Boolean> = mutableMapOf() @@ -98,7 +96,7 @@ class AudioRepositoryTest { underTest = AudioRepositoryImpl( - context, + intentsReceiver, audioManager, testScope.testScheduler, testScope.backgroundScope, @@ -270,8 +268,7 @@ class AudioRepositoryTest { } private fun triggerIntent(action: String) { - verify(context).registerReceiver(receiverCaptor.capture(), any()) - receiverCaptor.value.onReceive(context, Intent(action)) + testScope.launch { intentsReceiver.triggerIntent(action) } } private companion object { diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeLocalMediaRepository.kt new file mode 100644 index 000000000000..642b72c70e55 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeLocalMediaRepository.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.data.repository + +import com.android.settingslib.media.MediaDevice +import com.android.settingslib.volume.data.model.RoutingSession +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeLocalMediaRepository : LocalMediaRepository { + + private val volumeBySession: MutableMap<String?, Int> = mutableMapOf() + + private val mutableMediaDevices = MutableStateFlow<Collection<MediaDevice>>(emptyList()) + override val mediaDevices: StateFlow<Collection<MediaDevice>> + get() = mutableMediaDevices.asStateFlow() + + private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null) + override val currentConnectedDevice: StateFlow<MediaDevice?> + get() = mutableCurrentConnectedDevice.asStateFlow() + + private val mutableRemoteRoutingSessions = + MutableStateFlow<Collection<RoutingSession>>(emptyList()) + override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> + get() = mutableRemoteRoutingSessions.asStateFlow() + + fun updateMediaDevices(devices: Collection<MediaDevice>) { + mutableMediaDevices.value = devices + } + + fun updateCurrentConnectedDevice(device: MediaDevice?) { + mutableCurrentConnectedDevice.value = device + } + + fun updateRemoteRoutingSessions(sessions: List<RoutingSession>) { + mutableRemoteRoutingSessions.value = sessions + } + + fun getSessionVolume(sessionId: String?): Int = volumeBySession.getOrDefault(sessionId, 0) + + override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) { + volumeBySession[sessionId] = volume + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt index d106bceb6b85..dc9ea10a1074 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt @@ -15,10 +15,15 @@ */ package com.android.settingslib.volume.data.repository +import android.media.MediaRoute2Info +import android.media.MediaRouter2Manager +import android.media.RoutingSessionInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice +import com.android.settingslib.volume.data.model.RoutingSession +import com.android.settingslib.volume.shared.FakeAudioManagerIntentsReceiver import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn @@ -32,6 +37,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyString +import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -44,10 +53,12 @@ class LocalMediaRepositoryImplTest { @Mock private lateinit var localMediaManager: LocalMediaManager @Mock private lateinit var mediaDevice1: MediaDevice @Mock private lateinit var mediaDevice2: MediaDevice + @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager @Captor private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback> + private val intentsReceiver = FakeAudioManagerIntentsReceiver() private val testScope = TestScope() private lateinit var underTest: LocalMediaRepository @@ -58,7 +69,9 @@ class LocalMediaRepositoryImplTest { underTest = LocalMediaRepositoryImpl( + intentsReceiver, localMediaManager, + mediaRouter2Manager, testScope.backgroundScope, testScope.testScheduler, ) @@ -97,4 +110,78 @@ class LocalMediaRepositoryImplTest { assertThat(currentConnectedDevice).isEqualTo(mediaDevice1) } } + + @Test + fun kek() { + testScope.runTest { + `when`(localMediaManager.remoteRoutingSessions) + .thenReturn( + listOf( + testRoutingSessionInfo1, + testRoutingSessionInfo2, + testRoutingSessionInfo3, + ) + ) + `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then { + (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1 + } + `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then { + if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) { + return@then listOf(mock(MediaRoute2Info::class.java)) + } + emptyList<MediaRoute2Info>() + } + var remoteRoutingSessions: Collection<RoutingSession>? = null + underTest.remoteRoutingSessions + .onEach { remoteRoutingSessions = it } + .launchIn(backgroundScope) + + runCurrent() + + assertThat(remoteRoutingSessions) + .containsExactlyElementsIn( + listOf( + RoutingSession( + routingSessionInfo = testRoutingSessionInfo1, + isVolumeSeekBarEnabled = true, + isMediaOutputDisabled = true, + ), + RoutingSession( + routingSessionInfo = testRoutingSessionInfo2, + isVolumeSeekBarEnabled = false, + isMediaOutputDisabled = false, + ), + RoutingSession( + routingSessionInfo = testRoutingSessionInfo3, + isVolumeSeekBarEnabled = false, + isMediaOutputDisabled = true, + ) + ) + ) + } + } + + @Test + fun adjustSessionVolume_adjusts() { + testScope.runTest { + var volume = 0 + `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then { + volume = it.arguments[1] as Int + Unit + } + + underTest.adjustSessionVolume("test_session", 10) + + assertThat(volume).isEqualTo(10) + } + } + + private companion object { + val testRoutingSessionInfo1 = + RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build() + val testRoutingSessionInfo2 = + RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build() + val testRoutingSessionInfo3 = + RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build() + } } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt index f07b1bff0f31..430d733e4a88 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt @@ -16,9 +16,6 @@ package com.android.settingslib.volume.data.repository -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent import android.media.AudioManager import android.media.session.MediaController import android.media.session.MediaController.PlaybackInfo @@ -29,6 +26,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.bluetooth.BluetoothCallback import com.android.settingslib.bluetooth.BluetoothEventManager import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.volume.shared.FakeAudioManagerIntentsReceiver import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn @@ -52,10 +50,8 @@ import org.mockito.MockitoAnnotations @SmallTest class MediaControllerRepositoryImplTest { - @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver> @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback> - @Mock private lateinit var context: Context @Mock private lateinit var mediaSessionManager: MediaSessionManager @Mock private lateinit var localBluetoothManager: LocalBluetoothManager @Mock private lateinit var eventManager: BluetoothEventManager @@ -70,6 +66,7 @@ class MediaControllerRepositoryImplTest { @Mock private lateinit var localPlaybackInfo: PlaybackInfo private val testScope = TestScope() + private val intentsReceiver = FakeAudioManagerIntentsReceiver() private lateinit var underTest: MediaControllerRepository @@ -97,7 +94,7 @@ class MediaControllerRepositoryImplTest { underTest = MediaControllerRepositoryImpl( - context, + intentsReceiver, mediaSessionManager, localBluetoothManager, testScope.backgroundScope, @@ -124,7 +121,7 @@ class MediaControllerRepositoryImplTest { .launchIn(backgroundScope) runCurrent() - triggerDevicesChange() + intentsReceiver.triggerIntent(AudioManager.STREAM_DEVICES_CHANGED_ACTION) triggerOnAudioModeChanged() runCurrent() @@ -149,7 +146,7 @@ class MediaControllerRepositoryImplTest { .launchIn(backgroundScope) runCurrent() - triggerDevicesChange() + intentsReceiver.triggerIntent(AudioManager.STREAM_DEVICES_CHANGED_ACTION) triggerOnAudioModeChanged() runCurrent() @@ -157,22 +154,19 @@ class MediaControllerRepositoryImplTest { } } - private fun triggerDevicesChange() { - verify(context).registerReceiver(receiverCaptor.capture(), any()) - receiverCaptor.value.onReceive(context, Intent(AudioManager.STREAM_DEVICES_CHANGED_ACTION)) - } - private fun triggerOnAudioModeChanged() { verify(eventManager).registerCallback(callbackCaptor.capture()) callbackCaptor.value.onAudioModeChanged() } private companion object { - val statePlaying = + val statePlaying: PlaybackState = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build() - val stateError = PlaybackState.Builder().setState(PlaybackState.STATE_ERROR, 0, 0f).build() - val stateStopped = + val stateError: PlaybackState = + PlaybackState.Builder().setState(PlaybackState.STATE_ERROR, 0, 0f).build() + val stateStopped: PlaybackState = PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0, 0f).build() - val stateNone = PlaybackState.Builder().setState(PlaybackState.STATE_NONE, 0, 0f).build() + val stateNone: PlaybackState = + PlaybackState.Builder().setState(PlaybackState.STATE_NONE, 0, 0f).build() } } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/FakeAudioManagerIntentsReceiver.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/FakeAudioManagerIntentsReceiver.kt new file mode 100644 index 000000000000..530690a5faa9 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/FakeAudioManagerIntentsReceiver.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.volume.shared + +import android.content.Intent +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class FakeAudioManagerIntentsReceiver : AudioManagerIntentsReceiver { + + private val mutableIntents = MutableSharedFlow<Intent>() + override val intents: SharedFlow<Intent> = mutableIntents.asSharedFlow() + + suspend fun triggerIntent(intent: Intent) { + mutableIntents.emit(intent) + } + + suspend fun triggerIntent(action: String) { + triggerIntent(Intent(action)) + } +} diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 8ad5f244b659..dc8116d7f94f 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -261,6 +261,7 @@ public class SecureSettings { Settings.Secure.CREDENTIAL_SERVICE, Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, Settings.Secure.EVEN_DIMMER_ACTIVATED, - Settings.Secure.EVEN_DIMMER_MIN_NITS + Settings.Secure.EVEN_DIMMER_MIN_NITS, + Settings.Secure.STYLUS_POINTER_ICON_ENABLED, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index d854df38a9ef..fabdafc0dbc3 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -416,5 +416,6 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.CREDENTIAL_SERVICE, CREDENTIAL_SERVICE_VALIDATOR); VALIDATORS.put(Secure.CREDENTIAL_SERVICE_PRIMARY, NULLABLE_COMPONENT_NAME_VALIDATOR); VALIDATORS.put(Secure.AUTOFILL_SERVICE, AUTOFILL_SERVICE_VALIDATOR); + VALIDATORS.put(Secure.STYLUS_POINTER_ICON_ENABLED, BOOLEAN_VALIDATOR); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index e4a762ae8118..bc0783619b89 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2601,6 +2601,9 @@ class SettingsProtoDumpUtil { p.end(soundsToken); dumpSetting(s, p, + Settings.Secure.STYLUS_POINTER_ICON_ENABLED, + SecureSettingsProto.STYLUS_POINTER_ICON_ENABLED); + dumpSetting(s, p, Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, SecureSettingsProto.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED); dumpSetting(s, p, diff --git a/packages/SoundPicker/OWNERS b/packages/SoundPicker/OWNERS deleted file mode 100644 index 5bf46e039e96..000000000000 --- a/packages/SoundPicker/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Haptics team works on the SoundPicker -include platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS diff --git a/packages/SoundPicker2/Android.bp b/packages/SoundPicker2/Android.bp deleted file mode 100644 index f4d8bf2c76b5..000000000000 --- a/packages/SoundPicker2/Android.bp +++ /dev/null @@ -1,46 +0,0 @@ -package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], -} - -android_library { - name: "SoundPicker2Lib", - srcs: [ - "src/**/*.java", - ], - resource_dirs: [ - "res", - ], - static_libs: [ - "androidx.appcompat_appcompat", - "hilt_android", - "guava", - "androidx.recyclerview_recyclerview", - "androidx-constraintlayout_constraintlayout", - "androidx.viewpager2_viewpager2", - "com.google.android.material_material", - ], -} - -android_app { - name: "SoundPicker2", - defaults: ["platform_app_defaults"], - manifest: "AndroidManifest.xml", - static_libs: ["SoundPicker2Lib"], - platform_apis: true, - certificate: "media", - privileged: true, - - optimize: { - enabled: true, - optimize: true, - shrink: true, - shrink_resources: true, - obfuscate: false, - proguard_compatibility: false, - }, -} diff --git a/packages/SoundPicker2/AndroidManifest.xml b/packages/SoundPicker2/AndroidManifest.xml deleted file mode 100644 index 934b003c605c..000000000000 --- a/packages/SoundPicker2/AndroidManifest.xml +++ /dev/null @@ -1,43 +0,0 @@ -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.soundpicker" - android:sharedUserId="android.media"> - - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - - <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> - <uses-permission android:name="android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY" /> - <uses-permission android:name="android.permission.WRITE_SETTINGS" /> - - <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> - - <application - android:name=".RingtonePickerApplication" - android:allowBackup="false" - android:label="@string/app_label" - android:theme="@style/Theme.AppCompat" - android:supportsRtl="true"> - <receiver android:name="RingtoneReceiver" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY"/> - </intent-filter> - </receiver> - - <service android:name="RingtoneOverlayService" /> - - <activity android:name="RingtonePickerActivity" - android:theme="@style/Theme.AppCompat.Dialog" - android:enabled="@*android:bool/config_defaultRingtonePickerEnabled" - android:excludeFromRecents="true" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.RINGTONE_PICKER" /> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.RINGTONE_PICKER_SOUND" /> - <category android:name="android.intent.category.RINGTONE_PICKER_VIBRATION" /> - <category android:name="android.intent.category.RINGTONE_PICKER_RINGTONE" /> - </intent-filter> - </activity> - </application> -</manifest> diff --git a/packages/SoundPicker2/OWNERS b/packages/SoundPicker2/OWNERS deleted file mode 100644 index 5bf46e039e96..000000000000 --- a/packages/SoundPicker2/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Haptics team works on the SoundPicker -include platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS diff --git a/packages/SoundPicker2/res/drawable/ic_add.xml b/packages/SoundPicker2/res/drawable/ic_add.xml deleted file mode 100644 index 22b3fe9176e5..000000000000 --- a/packages/SoundPicker2/res/drawable/ic_add.xml +++ /dev/null @@ -1,24 +0,0 @@ -<!-- - Copyright (C) 2016 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24.0dp" - android:height="24.0dp" - android:viewportWidth="48.0" - android:viewportHeight="48.0"> - <path - android:fillColor="?android:attr/colorAccent" - android:pathData="M38.0,26.0L26.0,26.0l0.0,12.0l-4.0,0.0L22.0,26.0L10.0,26.0l0.0,-4.0l12.0,0.0L22.0,10.0l4.0,0.0l0.0,12.0l12.0,0.0l0.0,4.0z"/> -</vector>
\ No newline at end of file diff --git a/packages/SoundPicker2/res/drawable/ic_add_padded.xml b/packages/SoundPicker2/res/drawable/ic_add_padded.xml deleted file mode 100644 index c376867896d0..000000000000 --- a/packages/SoundPicker2/res/drawable/ic_add_padded.xml +++ /dev/null @@ -1,22 +0,0 @@ -<!-- - Copyright (C) 2017 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<inset xmlns:android="http://schemas.android.com/apk/res/android" - android:drawable="@drawable/ic_add" - android:insetTop="4dp" - android:insetRight="4dp" - android:insetBottom="4dp" - android:insetLeft="4dp"/> diff --git a/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml b/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml deleted file mode 100644 index edfc0aba5be7..000000000000 --- a/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml +++ /dev/null @@ -1,36 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2017 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<!-- - Currently, no file manager app on watch could handle ACTION_GET_CONTENT intent. - Make the visibility to "gone" to prevent failures. - --> -<TextView xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/add_new_sound_text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?android:attr/listPreferredItemHeightSmall" - android:textAppearance="?android:attr/textAppearanceMedium" - android:text="@null" - android:textColor="?android:attr/colorAccent" - android:gravity="center_vertical" - android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" - android:drawableStart="@drawable/ic_add_padded" - android:drawablePadding="8dp" - android:ellipsize="marquee" - android:visibility="gone" /> diff --git a/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml b/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml deleted file mode 100644 index ee29a3710143..000000000000 --- a/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<com.android.soundpicker.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:gravity="center_vertical" - android:background="?android:attr/selectableItemBackground" - > - - <CheckedTextView - android:id="@+id/checked_text_view" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?android:attr/listPreferredItemHeightSmall" - android:textAppearance="?android:attr/textAppearanceMedium" - android:textColor="?android:attr/textColorAlertDialogListItem" - android:gravity="center_vertical" - android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" - android:drawableStart="?android:attr/listChoiceIndicatorSingle" - android:drawablePadding="8dp" - android:ellipsize="marquee" - android:layout_toLeftOf="@+id/work_icon" - android:maxLines="3" /> - - <ImageView - android:id="@id/work_icon" - android:layout_width="18dp" - android:layout_height="18dp" - android:layout_alignParentRight="true" - android:layout_centerVertical="true" - android:scaleType="centerCrop" - android:layout_marginRight="20dp" /> -</com.android.soundpicker.CheckedListItem> diff --git a/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml b/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml deleted file mode 100644 index 6fc60801ad3a..000000000000 --- a/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2023 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent"/>
\ No newline at end of file diff --git a/packages/SoundPicker2/res/layout/add_new_sound_item.xml b/packages/SoundPicker2/res/layout/add_new_sound_item.xml deleted file mode 100644 index 024b97ef23be..000000000000 --- a/packages/SoundPicker2/res/layout/add_new_sound_item.xml +++ /dev/null @@ -1,49 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2016 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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="fill_parent" - android:layout_height="wrap_content" - android:gravity="center_vertical" - android:background="?android:attr/selectableItemBackground" - android:focusable="true" - android:clickable="true"> - - <ImageView - android:layout_width="24dp" - android:layout_height="24dp" - android:layout_alignParentRight="true" - android:layout_centerVertical="true" - android:scaleType="centerCrop" - android:layout_marginRight="24dp" - android:layout_marginLeft="24dp" - android:src="@drawable/ic_add"/> - - <TextView - android:id="@+id/add_new_sound_text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?android:attr/listPreferredItemHeightSmall" - android:text="@null" - android:textColor="?android:attr/colorAccent" - android:textAppearance="?android:attr/textAppearanceMedium" - android:maxLines="3" - android:gravity="center_vertical" - android:paddingEnd="?android:attr/dialogPreferredPadding" - android:drawablePadding="20dp" - android:ellipsize="marquee"/> -</LinearLayout>
\ No newline at end of file diff --git a/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml b/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml deleted file mode 100644 index 787f92ec06d6..000000000000 --- a/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2023 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<androidx.recyclerview.widget.RecyclerView - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/recycler_view" - android:layout_width="match_parent" - android:layout_height="match_parent" -/>
\ No newline at end of file diff --git a/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml b/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml deleted file mode 100644 index 7efd91191b79..000000000000 --- a/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml +++ /dev/null @@ -1,31 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2023 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="match_parent"> - <com.google.android.material.tabs.TabLayout - android:id="@+id/tabLayout" - android:layout_width="match_parent" - android:layout_height="wrap_content"/> - <androidx.viewpager2.widget.ViewPager2 - android:id="@+id/masterViewPager" - android:paddingTop="12dp" - android:paddingBottom="12dp" - android:layout_width="match_parent" - android:layout_height="match_parent"/> -</LinearLayout>
\ No newline at end of file diff --git a/packages/SoundPicker2/res/layout/radio_with_work_badge.xml b/packages/SoundPicker2/res/layout/radio_with_work_badge.xml deleted file mode 100644 index 36ac93ed630b..000000000000 --- a/packages/SoundPicker2/res/layout/radio_with_work_badge.xml +++ /dev/null @@ -1,50 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<com.android.soundpicker.CheckedListItem - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:gravity="center_vertical" - android:background="?android:attr/selectableItemBackground" - android:focusable="true" - android:clickable="true"> - - <CheckedTextView - android:id="@+id/checked_text_view" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?android:attr/listPreferredItemHeightSmall" - android:textAppearance="?android:attr/textAppearanceMedium" - android:textColor="?android:attr/textColorAlertDialogListItem" - android:gravity="center_vertical" - android:paddingStart="20dp" - android:paddingEnd="?android:attr/dialogPreferredPadding" - android:drawableStart="?android:attr/listChoiceIndicatorSingle" - android:drawablePadding="20dp" - android:ellipsize="marquee" - android:layout_toLeftOf="@+id/work_icon" - android:maxLines="3"/> - - <ImageView - android:id="@id/work_icon" - android:layout_width="18dp" - android:layout_height="18dp" - android:layout_alignParentRight="true" - android:layout_centerVertical="true" - android:scaleType="centerCrop" - android:layout_marginRight="20dp"/> -</com.android.soundpicker.CheckedListItem> diff --git a/packages/SoundPicker2/res/raw/default_alarm_alert.ogg b/packages/SoundPicker2/res/raw/default_alarm_alert.ogg deleted file mode 100644 index e69de29bb2d1..000000000000 --- a/packages/SoundPicker2/res/raw/default_alarm_alert.ogg +++ /dev/null diff --git a/packages/SoundPicker2/res/raw/default_notification_sound.ogg b/packages/SoundPicker2/res/raw/default_notification_sound.ogg deleted file mode 100644 index e69de29bb2d1..000000000000 --- a/packages/SoundPicker2/res/raw/default_notification_sound.ogg +++ /dev/null diff --git a/packages/SoundPicker2/res/raw/default_ringtone.ogg b/packages/SoundPicker2/res/raw/default_ringtone.ogg deleted file mode 100644 index e69de29bb2d1..000000000000 --- a/packages/SoundPicker2/res/raw/default_ringtone.ogg +++ /dev/null diff --git a/packages/SoundPicker2/res/values/config.xml b/packages/SoundPicker2/res/values/config.xml deleted file mode 100644 index 4e237a2f1644..000000000000 --- a/packages/SoundPicker2/res/values/config.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<!-- These resources are around just to allow their values to be customized - for different hardware and product builds. Do not translate. - - NOTE: The naming convention is "config_camelCaseValue". --> -<resources xmlns:android="http://schemas.android.com/apk/res/android"> - <!-- True if the ringtone picker should show the ok/cancel buttons. If it is not shown, the - ringtone will be automatically selected when the picker is closed. --> - <bool name="config_showOkCancelButtons">true</bool> -</resources> diff --git a/packages/SoundPicker2/res/values/strings.xml b/packages/SoundPicker2/res/values/strings.xml deleted file mode 100644 index ab7b95a09028..000000000000 --- a/packages/SoundPicker2/res/values/strings.xml +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2009 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <!-- Choice in the ringtone picker. If chosen, the default ringtone will be used. --> - <string name="ringtone_default">Default ringtone</string> - - <!-- Choice in the notification sound picker. If chosen, the default notification sound will be - used. --> - <string name="notification_sound_default">Default notification sound</string> - - <!-- Choice in the alarm sound picker. If chosen, the default alarm sound will be used. --> - <string name="alarm_sound_default">Default alarm sound</string> - - <!-- Text for the RingtonePicker item that allows adding a new ringtone. --> - <string name="add_ringtone_text">Add ringtone</string> - <!-- Text for the RingtonePicker item that allows adding a new alarm. --> - <string name="add_alarm_text">Add alarm</string> - <!-- Text for the RingtonePicker item that allows adding a new notification. --> - <string name="add_notification_text">Add notification</string> - <!-- Text for the RingtonePicker item ContextMenu that allows deleting a custom ringtone. --> - <string name="delete_ringtone_text">Delete</string> - <!-- Text for the Toast displayed when adding a custom ringtone fails. --> - <string name="unable_to_add_ringtone">Unable to add custom ringtone</string> - <!-- Text for the Toast displayed when deleting a custom ringtone fails. --> - <string name="unable_to_delete_ringtone">Unable to delete custom ringtone</string> - - <!-- Text for the name of the app. [CHAR LIMIT=12] --> - <string name="app_label">Sounds</string> - - <string name="empty_list">The list is empty</string> - <string name="sound_page_title">Sound</string> - <string name="vibration_page_title">Vibration</string> -</resources> diff --git a/packages/SoundPicker2/res/values/styles.xml b/packages/SoundPicker2/res/values/styles.xml deleted file mode 100644 index d22d9c43d0fb..000000000000 --- a/packages/SoundPicker2/res/values/styles.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2014 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<resources xmlns:android="http://schemas.android.com/apk/res/android"> - - <style name="PickerDialogTheme" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog"> - </style> - -</resources> diff --git a/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java deleted file mode 100644 index 4fc2a86537c1..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import android.app.Activity; -import android.content.ContentProvider; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.MediaStore; -import android.util.Log; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import dagger.hilt.android.AndroidEntryPoint; - -import java.util.Objects; - -/** - * Base class for generic picker fragments. - * - * <p>This fragment displays a recycler view that is populated by a {@link RingtoneListViewAdapter} - * with data provided by a {@link RingtoneListHandler}. Each item can be selected on click, - * which also triggers a ringtone preview performed by the shared {@link RingtonePickerViewModel}. - * The ringtone preview uses the selection state of all picker fragments (e.g. sound selected by - * one fragment and vibration selected by another). - */ -@AndroidEntryPoint(Fragment.class) -public abstract class BasePickerFragment extends Hilt_BasePickerFragment implements - RingtoneListViewAdapter.Callbacks { - - private static final String TAG = "BasePickerFragment"; - private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE; - private boolean mIsManagedProfile; - private Drawable mWorkIconDrawable; - - protected RingtoneListViewAdapter mRingtoneListViewAdapter; - protected RecyclerView mRecyclerView; - protected RingtonePickerViewModel.Config mPickerConfig; - protected RingtonePickerViewModel mRingtonePickerViewModel; - protected RingtoneListHandler.Config mRingtoneListConfig; - protected RingtoneListHandler mRingtoneListHandler; - - public BasePickerFragment() { - super(R.layout.fragment_ringtone_picker); - } - - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get( - RingtonePickerViewModel.class); - mRingtoneListHandler = getRingtoneListHandler(); - mRecyclerView = view.requireViewById(R.id.recycler_view); - - mPickerConfig = mRingtonePickerViewModel.getPickerConfig(); - mRingtoneListConfig = mRingtoneListHandler.getRingtoneListConfig(); - - mIsManagedProfile = UserManager.get(requireActivity()).isManagedProfile( - mPickerConfig.userId); - - mRingtoneListViewAdapter = createRingtoneListViewAdapter(); - mRecyclerView.setHasFixedSize(true); - mRecyclerView.setAdapter(mRingtoneListViewAdapter); - mRecyclerView.setLayoutManager(new LinearLayoutManager(requireActivity())); - setSelectedItem(mRingtoneListHandler.getSelectedItemPosition()); - prepareRecyclerView(mRecyclerView); - } - - @Override - public boolean isWorkRingtone(int position) { - if (!mIsManagedProfile) { - return false; - } - - /* - * Display the work icon if the ringtone belongs to a work profile. We - * can tell that a ringtone belongs to a work profile if the picker user - * is a managed profile, the ringtone Uri is in external storage, and - * either the uri has no user id or has the id of the picker user - */ - Uri currentUri = mRingtoneListHandler.getRingtoneUri(position); - int uriUserId = ContentProvider.getUserIdFromUri(currentUri, - mPickerConfig.userId); - Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri); - - return uriUserId == mPickerConfig.userId - && uriWithoutUserId.toString().startsWith( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString()); - } - - @Override - public Drawable getWorkIconDrawable() { - if (mWorkIconDrawable == null) { - mWorkIconDrawable = requireActivity().getPackageManager() - .getUserBadgeForDensityNoBackground( - UserHandle.of(mPickerConfig.userId), /* density= */ -1); - } - - return mWorkIconDrawable; - } - - @Override - public void onRingtoneSelected(int position) { - setSelectedItem(position); - - // In the buttonless (watch-only) version, preemptively set our result since - // we won't have another chance to do so before the activity closes. - if (!mPickerConfig.showOkCancelButtons) { - setSuccessResultWithSelectedRingtone(); - } - - // Play clip - mRingtonePickerViewModel.playRingtone(); - } - - @Override - public void onAddRingtoneSelected() { - addRingtoneAsync(); - } - - /** - * Sets up the list by adding fixed items to the top and bottom, if required. And sets the - * selected item in the list. - * @param recyclerView The recyclerview that contains the list of displayed items. - */ - protected void prepareRecyclerView(@NonNull RecyclerView recyclerView) { - // Reset the static item count, as this method can be called multiple times - mRingtoneListHandler.resetFixedItems(); - - if (mRingtoneListConfig.hasDefaultItem) { - int defaultItemPos = addDefaultRingtoneItem(); - - if (getSelectedItem() < 0 - && RingtoneManager.isDefault(mRingtoneListConfig.initialSelectedUri)) { - setSelectedItem(defaultItemPos); - } - } - - if (mRingtoneListConfig.hasSilentItem) { - int silentItemPos = addSilentItem(); - - // The 'Silent' item should use a null Uri - if (getSelectedItem() < 0 - && mRingtoneListConfig.initialSelectedUri == null) { - setSelectedItem(silentItemPos); - } - } - - if (getSelectedItem() < 0) { - setSelectedItem(mRingtoneListHandler.getRingtonePosition( - mRingtoneListConfig.initialSelectedUri)); - } - - // In the buttonless (watch-only) version, preemptively set our result since we won't - // have another chance to do so before the activity closes. - if (!mPickerConfig.showOkCancelButtons) { - setSuccessResultWithSelectedRingtone(); - } - - addNewRingtoneItem(); - - // Enable context menu in ringtone items - registerForContextMenu(recyclerView); - } - - /** - * Returns the fragment's sound/vibration list handler. - * @return The ringtone list handler. - */ - protected abstract RingtoneListHandler getRingtoneListHandler(); - - /** - * Starts the process to add a new ringtone to the list of ringtones asynchronously. - * Currently, only works for adding sound files. - */ - protected abstract void addRingtoneAsync(); - - /** - * Adds an item to the end of the list that can be used to add new ringtones to the list. - * Currently, only works for adding sound files. - */ - protected abstract void addNewRingtoneItem(); - - protected int getSelectedItem() { - return mRingtoneListHandler.getSelectedItemPosition(); - } - - /** - * Returns the selected URI to the caller activity. - */ - protected void setSuccessResultWithSelectedRingtone() { - requireActivity().setResult(Activity.RESULT_OK, - new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, - mRingtonePickerViewModel.getSelectedRingtoneUri())); - } - - /** - * Creates a ringtone recyclerview adapter using the ringtone manager cursor. - * @return The created RingtoneListViewAdapter. - */ - protected RingtoneListViewAdapter createRingtoneListViewAdapter() { - LocalizedCursor cursor = new LocalizedCursor( - mRingtoneListHandler.getRingtoneCursor(), getResources(), COLUMN_LABEL); - return new RingtoneListViewAdapter(cursor, /* RingtoneListViewAdapterCallbacks= */ this); - } - - /** - * Sets the selected item in the list and scroll to the position in the recyclerview. - * @param pos the position of the selected item in the list. - */ - protected void setSelectedItem(int pos) { - Objects.requireNonNull(mRingtoneListViewAdapter); - mRingtoneListHandler.setSelectedItemPosition(pos); - mRingtoneListViewAdapter.setSelectedItem(pos); - mRingtoneListHandler.setSelectedItemId(mRingtoneListViewAdapter.getItemId(pos)); - mRecyclerView.scrollToPosition(pos); - } - - /** - * Adds a fixed item to the fixed items list . A fixed item is one that is not from - * the RingtoneManager. - * - * @param textResId The resource ID of the text for the item. - * @return The index of the inserted fixed item in the adapter. - */ - protected int addFixedItem(int textResId) { - return mRingtoneListViewAdapter.addTitleForFixedItem(textResId); - } - - /** - * Re-query RingtoneManager for the most recent set of installed ringtones. May move the - * selected item position to match the new position of the chosen ringtone. - * <p> - * This should only need to happen after adding or removing a ringtone. - */ - protected void requeryForAdapter() { - mRingtonePickerViewModel.reinit(); - // Refresh and set a new cursor, and closing the old one. - mRingtoneListViewAdapter = createRingtoneListViewAdapter(); - mRecyclerView.setAdapter(mRingtoneListViewAdapter); - prepareRecyclerView(mRecyclerView); - - // Update selected item location. - for (int i = 0; i < mRingtoneListViewAdapter.getItemCount(); i++) { - if (mRingtoneListViewAdapter.getItemId(i) - == mRingtoneListHandler.getSelectedItemId()) { - setSelectedItem(i); - return; - } - } - - // If selected item is still unknown, then set it to the default item, if available. - // If it's not available, then attempt to set it to the silent item in the list. - int selectedPosition = mRingtoneListHandler.getDefaultItemPosition(); - - if (selectedPosition < 0) { - selectedPosition = mRingtoneListHandler.getSilentItemPosition(); - } - - setSelectedItem(selectedPosition); - } - - private int addDefaultRingtoneItem() { - int defaultItemPosInAdapter = addFixedItem( - RingtonePickerViewModel.getDefaultRingtoneItemTextByType( - mPickerConfig.ringtoneType)); - int defaultItemPosInListHandler = mRingtoneListHandler.addDefaultItem(); - - if (defaultItemPosInAdapter != defaultItemPosInListHandler) { - Log.wtf(TAG, "Default item position in adapter and list handler must match."); - return RingtoneListHandler.ITEM_POSITION_UNKNOWN; - } - - return defaultItemPosInListHandler; - } - - private int addSilentItem() { - int silentItemPosInAdapter = addFixedItem(com.android.internal.R.string.ringtone_silent); - int silentItemPosInListHandler = mRingtoneListHandler.addSilentItem(); - - if (silentItemPosInAdapter != silentItemPosInListHandler) { - Log.wtf(TAG, "Silent item position in adapter and list handler must match."); - return RingtoneListHandler.ITEM_POSITION_UNKNOWN; - } - - return silentItemPosInListHandler; - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java b/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java deleted file mode 100644 index 819ae987269d..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.soundpicker; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.Checkable; -import android.widget.CheckedTextView; -import android.widget.RelativeLayout; - -/** - * The {@link CheckedListItem} is a layout item that represents a ringtone, and is used in - * {@link RingtonePickerActivity}. It contains the ringtone's name, and a work badge to right of the - * name if the ringtone belongs to a work profile. - */ -public class CheckedListItem extends RelativeLayout implements Checkable { - - public CheckedListItem(Context context) { - super(context); - } - - public CheckedListItem(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public CheckedListItem(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public CheckedListItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - @Override - public void setChecked(boolean checked) { - getCheckedTextView().setChecked(checked); - } - - @Override - public boolean isChecked() { - return getCheckedTextView().isChecked(); - } - - @Override - public void toggle() { - getCheckedTextView().toggle(); - } - - private CheckedTextView getCheckedTextView() { - return (CheckedTextView) findViewById(R.id.checked_text_view); - } - -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java deleted file mode 100644 index afdbf053ac22..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; - -import java.util.concurrent.Executors; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * A factory class used to create {@link ListeningExecutorService}. - */ -@Singleton -public class ListeningExecutorServiceFactory { - - @Inject - ListeningExecutorServiceFactory() { - } - - /** - * Returns a single thread {@link ListeningExecutorService}. - * - */ - public ListeningExecutorService createSingleThreadExecutor() { - return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java b/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java deleted file mode 100644 index 83d04a345f8b..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import android.content.res.Resources; -import android.database.Cursor; -import android.database.CursorWrapper; -import android.util.Log; -import android.util.TypedValue; - -import androidx.annotation.Nullable; - -import java.util.Locale; -import java.util.regex.Pattern; - -/** - * A cursor wrapper class mainly used to guarantee getting a ringtone title - */ -final class LocalizedCursor extends CursorWrapper { - - private static final String TAG = "LocalizedCursor"; - private static final String SOUND_NAME_RES_PREFIX = "sound_name_"; - - private final int mTitleIndex; - private final Resources mResources; - private final Pattern mSanitizePattern; - private final String mNamePrefix; - - LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) { - super(cursor); - mTitleIndex = mCursor.getColumnIndex(columnLabel); - mResources = resources; - mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]"); - if (mTitleIndex == -1) { - Log.e(TAG, "No index for column " + columnLabel); - mNamePrefix = null; - } else { - mNamePrefix = buildNamePrefix(mResources); - } - } - - /** - * Builds the prefix for the name of the resource to look up. - * The format is: "ResourcePackageName::ResourceTypeName/" (the type name is expected to be - * "string" but let's not hardcode it). - * Here we use an existing resource "notification_sound_default" which is always expected to be - * found. - * - * @param resources Application's resources - * @return the built name prefix, or null if failed to build. - */ - @Nullable - private static String buildNamePrefix(Resources resources) { - try { - return String.format("%s:%s/%s", - resources.getResourcePackageName(R.string.notification_sound_default), - resources.getResourceTypeName(R.string.notification_sound_default), - SOUND_NAME_RES_PREFIX); - } catch (Resources.NotFoundException e) { - Log.e(TAG, "Failed to build the prefix for the name of the resource.", e); - } - - return null; - } - - /** - * Process resource name to generate a valid resource name. - * - * @return a non-null String - */ - private String sanitize(String input) { - if (input == null) { - return ""; - } - return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase(Locale.ROOT); - } - - @Override - public String getString(int columnIndex) { - final String defaultName = mCursor.getString(columnIndex); - if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) { - return defaultName; - } - TypedValue value = new TypedValue(); - try { - // the name currently in the database is used to derive a name to match - // against resource names in this package - mResources.getValue(mNamePrefix + sanitize(defaultName), value, - /* resolveRefs= */ false); - } catch (Resources.NotFoundException e) { - Log.d(TAG, "Failed to get localized string. Using default string instead.", e); - return defaultName; - } - if ((value != null) && (value.type == TypedValue.TYPE_STRING)) { - Log.d(TAG, String.format("Replacing name %s with %s", - defaultName, value.string.toString())); - return value.string.toString(); - } else { - Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName); - return defaultName; - } - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java deleted file mode 100644 index 6817f534c00b..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import android.content.Context; -import android.media.AudioAttributes; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; - -import dagger.hilt.android.qualifiers.ApplicationContext; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * A factory class used to create {@link Ringtone}. - */ -@Singleton -public class RingtoneFactory { - - private final Context mApplicationContext; - - @Inject - RingtoneFactory(@ApplicationContext Context applicationContext) { - mApplicationContext = applicationContext; - } - - /** - * Returns a {@link Ringtone} built from the provided URI and audio attributes flags. - * - * @param uri The URI used to build the {@link Ringtone}. - * @param audioAttributesFlags A combination of audio attribute flags that affect the volume - * and settings when playing the ringtone. - * @return the built {@link Ringtone}. - */ - public Ringtone create(Uri uri, int audioAttributesFlags) { - AudioAttributes audioAttributes = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setFlags(audioAttributesFlags) - .build(); - return RingtoneManager.getRingtone(mApplicationContext, uri, - /* volumeShaperConfig= */ null, audioAttributes); - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java deleted file mode 100644 index bb38e0e2ecaa..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import static java.util.Objects.requireNonNull; - -import android.annotation.Nullable; -import android.database.Cursor; -import android.media.RingtoneManager; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.NonNull; - -import javax.inject.Inject; - -/** - * Handles ringtone list state and actions. This includes keeping track of the selected item, - * ringtone manager cursor and added items to the list. - */ -public class RingtoneListHandler { - - // TODO: We're using an empty URI instead of null, because null URIs still produce a sound, - // while empty ones don't (Potentially this might be due to empty URIs being perceived as - // malformed ones). We will switch to using the official silent URIs (SOUND_OFF, VIBRATION_OFF) - // once they become available. - static final Uri SILENT_URI = Uri.EMPTY; - static final int ITEM_POSITION_UNKNOWN = -1; - - private static final String TAG = "RingtoneListHandler"; - - /** The position in the list of the 'Silent' item. */ - private int mSilentItemPosition = ITEM_POSITION_UNKNOWN; - /** The position in the list of the 'Default' item. */ - private int mDefaultItemPosition = ITEM_POSITION_UNKNOWN; - /** The number of fixed items in the list. */ - private int mFixedItemCount; - /** - * Stable ID for the ringtone that is currently selected (may be -1 if no ringtone is selected). - */ - private long mSelectedItemId = -1; - private int mSelectedItemPosition = ITEM_POSITION_UNKNOWN; - - private RingtoneManager mRingtoneManager; - private Config mRingtoneListConfig; - private Cursor mRingtoneCursor; - - /** - * Holds immutable info on the ringtone list that is displayed. - */ - static final class Config { - /** - * Whether this list has the 'Default' item. - */ - public final boolean hasDefaultItem; - /** - * The Uri to play when the 'Default' item is clicked. - */ - public final Uri uriForDefaultItem; - /** - * Whether this list has the 'Silent' item. - */ - public final boolean hasSilentItem; - /** - * The initially selected uri in the list. - */ - public final Uri initialSelectedUri; - - Config(boolean hasDefaultItem, Uri uriForDefaultItem, boolean hasSilentItem, - Uri initialSelectedUri) { - this.hasDefaultItem = hasDefaultItem; - this.uriForDefaultItem = uriForDefaultItem; - this.hasSilentItem = hasSilentItem; - this.initialSelectedUri = initialSelectedUri; - } - } - - @Inject - RingtoneListHandler() { - } - - void init(@NonNull Config ringtoneListConfig, - @NonNull RingtoneManager ringtoneManager, @NonNull Cursor ringtoneCursor) { - mRingtoneManager = requireNonNull(ringtoneManager); - mRingtoneListConfig = requireNonNull(ringtoneListConfig); - mRingtoneCursor = requireNonNull(ringtoneCursor); - } - - Config getRingtoneListConfig() { - return mRingtoneListConfig; - } - - Cursor getRingtoneCursor() { - requireInitCalled(); - return mRingtoneCursor; - } - - Uri getRingtoneUri(int position) { - if (position < 0) { - Log.w(TAG, "Selected item position is unknown."); - // When the selected item is ITEM_POSITION_UNKNOWN, it is not the case we expected. - // We return SILENT_URI for this case. - return SILENT_URI; - } else if (position == mDefaultItemPosition) { - // Use the default Uri that they originally gave us. - return mRingtoneListConfig.uriForDefaultItem; - } else if (position == mSilentItemPosition) { - // Use SILENT_URI for the 'Silent' item. - return SILENT_URI; - } else { - requireInitCalled(); - return mRingtoneManager.getRingtoneUri(mapListPositionToRingtonePosition(position)); - } - } - - int getRingtonePosition(Uri uri) { - requireInitCalled(); - return mapRingtonePositionToListPosition(mRingtoneManager.getRingtonePosition(uri)); - } - - void resetFixedItems() { - mFixedItemCount = 0; - mDefaultItemPosition = ITEM_POSITION_UNKNOWN; - mSilentItemPosition = ITEM_POSITION_UNKNOWN; - } - - int addDefaultItem() { - if (mDefaultItemPosition < 0) { - mDefaultItemPosition = addFixedItem(); - } - return mDefaultItemPosition; - } - - int getDefaultItemPosition() { - return mDefaultItemPosition; - } - - int addSilentItem() { - if (mSilentItemPosition < 0) { - mSilentItemPosition = addFixedItem(); - } - return mSilentItemPosition; - } - - public int getSilentItemPosition() { - return mSilentItemPosition; - } - - int getSelectedItemPosition() { - return mSelectedItemPosition; - } - - void setSelectedItemPosition(int selectedItemPosition) { - mSelectedItemPosition = selectedItemPosition; - } - - void setSelectedItemId(long selectedItemId) { - mSelectedItemId = selectedItemId; - } - - long getSelectedItemId() { - return mSelectedItemId; - } - - @Nullable - Uri getSelectedRingtoneUri() { - return getRingtoneUri(mSelectedItemPosition); - } - - /** - * Maps the item position in the list, to its equivalent position in the RingtoneManager. - * - * @param itemPosition the position of item in the list. - * @return position of the item in the RingtoneManager. - */ - private int mapListPositionToRingtonePosition(int itemPosition) { - // If the manager position is less than add items, then return that. - if (itemPosition < mFixedItemCount) return itemPosition; - - return itemPosition - mFixedItemCount; - } - - /** - * Maps the item position in the RingtoneManager, to its equivalent position in the list. - * - * @param itemPosition the position of the item in the RingtoneManager. - * @return position of the item in the list. - */ - private int mapRingtonePositionToListPosition(int itemPosition) { - // If the manager position is less than add items, then return that. - if (itemPosition < 0) return itemPosition; - - return itemPosition + mFixedItemCount; - } - - /** - * Increments the number of added fixed items and returns the index of the newest added item. - * @return index of the newest added fixed item. - */ - private int addFixedItem() { - return mFixedItemCount++; - } - - private void requireInitCalled() { - requireNonNull(mRingtoneManager); - requireNonNull(mRingtoneCursor); - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java deleted file mode 100644 index 4ca8943b5fd4..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import static com.android.internal.widget.RecyclerView.NO_ID; - -import android.database.Cursor; -import android.graphics.drawable.Drawable; -import android.media.RingtoneManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckedTextView; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.StringRes; -import androidx.recyclerview.widget.RecyclerView; - -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * The adapter presents a list of ringtones which may include fixed item in the list and an action - * button at the end. - * - * The adapter handles three different types of items: - * <ul> - * <li>FIXED: Fixed items are items added to the top of the list. These items can not be modified - * and their position will never change. - * <li>DYNAMIC: Dynamic items are items from the ringtone manager. These items can be modified - * and their position can change. - * <li>FOOTER: A footer item is an added button to the end of the list. This item can be clicked - * but not selected and its position will never change. - * </ul> - */ -final class RingtoneListViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { - - private static final int VIEW_TYPE_FIXED_ITEM = 0; - private static final int VIEW_TYPE_DYNAMIC_ITEM = 1; - private static final int VIEW_TYPE_ADD_RINGTONE_ITEM = 2; - private final Cursor mCursor; - private final List<Integer> mFixedItemTitles; - private final Callbacks mCallbacks; - private final int mRowIDColumn; - private int mSelectedItem = -1; - @StringRes private Integer mAddRingtoneItemTitle; - - /** Provides callbacks for the adapter. */ - interface Callbacks { - void onRingtoneSelected(int position); - void onAddRingtoneSelected(); - boolean isWorkRingtone(int position); - Drawable getWorkIconDrawable(); - } - - RingtoneListViewAdapter(Cursor cursor, - Callbacks callbacks) { - mCursor = cursor; - mCallbacks = callbacks; - mFixedItemTitles = new ArrayList<>(); - mRowIDColumn = mCursor != null ? mCursor.getColumnIndex("_id") : -1; - setHasStableIds(true); - } - - void setSelectedItem(int position) { - notifyItemChanged(mSelectedItem); - mSelectedItem = position; - notifyItemChanged(mSelectedItem); - } - - /** - * Adds title to the fixed items list and returns the index of the newest added item. - * @param textResId the title to add to the fixed items list. - * @return The index of the newest added item in the fixed items list. - */ - int addTitleForFixedItem(@StringRes int textResId) { - mFixedItemTitles.add(textResId); - notifyItemInserted(mFixedItemTitles.size() - 1); - return mFixedItemTitles.size() - 1; - } - - void addTitleForAddRingtoneItem(@StringRes int textResId) { - mAddRingtoneItemTitle = textResId; - notifyItemInserted(getItemCount() - 1); - } - - @NotNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - - if (viewType == VIEW_TYPE_FIXED_ITEM) { - View fixedItemView = inflater.inflate( - com.android.internal.R.layout.select_dialog_singlechoice_material, parent, - false); - - return new FixedItemViewHolder(fixedItemView, mCallbacks); - } - - if (viewType == VIEW_TYPE_ADD_RINGTONE_ITEM) { - View addRingtoneItemView = inflater.inflate(R.layout.add_new_sound_item, parent, false); - - return new AddRingtoneItemViewHolder(addRingtoneItemView, - mCallbacks); - } - - View view = inflater.inflate(R.layout.radio_with_work_badge, parent, false); - - return new DynamicItemViewHolder(view, mCallbacks); - } - - @Override - public void onBindViewHolder(@NotNull RecyclerView.ViewHolder holder, int position) { - if (holder instanceof FixedItemViewHolder) { - FixedItemViewHolder viewHolder = (FixedItemViewHolder) holder; - - viewHolder.onBind(mFixedItemTitles.get(position), - /* isChecked= */ position == mSelectedItem); - return; - } - if (holder instanceof AddRingtoneItemViewHolder) { - AddRingtoneItemViewHolder viewHolder = (AddRingtoneItemViewHolder) holder; - - viewHolder.onBind(mAddRingtoneItemTitle); - return; - } - - if (!(holder instanceof DynamicItemViewHolder)) { - throw new IllegalArgumentException("holder type is not supported"); - } - - DynamicItemViewHolder viewHolder = (DynamicItemViewHolder) holder; - int pos = position - mFixedItemTitles.size(); - if (!mCursor.moveToPosition(pos)) { - throw new IllegalStateException("Could not move cursor to position: " + pos); - } - - Drawable workIcon = (mCallbacks != null) - && mCallbacks.isWorkRingtone(position) - ? mCallbacks.getWorkIconDrawable() : null; - - viewHolder.onBind(mCursor.getString(RingtoneManager.TITLE_COLUMN_INDEX), - /* isChecked= */ position == mSelectedItem, workIcon); - } - - @Override - public int getItemViewType(int position) { - if (!mFixedItemTitles.isEmpty() && position < mFixedItemTitles.size()) { - return VIEW_TYPE_FIXED_ITEM; - } - if (mAddRingtoneItemTitle != null && position == getItemCount() - 1) { - return VIEW_TYPE_ADD_RINGTONE_ITEM; - } - - return VIEW_TYPE_DYNAMIC_ITEM; - } - - @Override - public int getItemCount() { - int itemCount = mFixedItemTitles.size() + mCursor.getCount(); - - if (mAddRingtoneItemTitle != null) { - itemCount++; - } - - return itemCount; - } - - @Override - public long getItemId(int position) { - int itemViewType = getItemViewType(position); - if (itemViewType == VIEW_TYPE_FIXED_ITEM) { - // Since the item is a fixed item, then we can use the position as a stable ID - // since the order of the fixed items should never change. - return position; - } - if (itemViewType == VIEW_TYPE_DYNAMIC_ITEM && mCursor != null - && mCursor.moveToPosition(position - mFixedItemTitles.size()) - && mRowIDColumn != -1) { - return mCursor.getLong(mRowIDColumn) + mFixedItemTitles.size(); - } - - // The position is either invalid or the item is the add ringtone item view, so no stable - // ID is returned. Add ringtone item view cannot be selected and only include an action - // buttons. - return NO_ID; - } - - private static class DynamicItemViewHolder extends RecyclerView.ViewHolder { - private final CheckedTextView mTitleTextView; - private final ImageView mWorkIcon; - - DynamicItemViewHolder(View itemView, Callbacks listener) { - super(itemView); - - mTitleTextView = itemView.requireViewById(R.id.checked_text_view); - mWorkIcon = itemView.requireViewById(R.id.work_icon); - itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition())); - } - - void onBind(String title, boolean isChecked, Drawable workIcon) { - mTitleTextView.setText(title); - mTitleTextView.setChecked(isChecked); - - if (workIcon == null) { - mWorkIcon.setVisibility(View.GONE); - } else { - mWorkIcon.setImageDrawable(workIcon); - mWorkIcon.setVisibility(View.VISIBLE); - } - } - } - - private static class FixedItemViewHolder extends RecyclerView.ViewHolder { - private final CheckedTextView mTitleTextView; - - FixedItemViewHolder(View itemView, Callbacks listener) { - super(itemView); - - mTitleTextView = (CheckedTextView) itemView; - itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition())); - } - - void onBind(@StringRes int title, boolean isChecked) { - Objects.requireNonNull(mTitleTextView); - - mTitleTextView.setText(title); - mTitleTextView.setChecked(isChecked); - } - } - - private static class AddRingtoneItemViewHolder extends RecyclerView.ViewHolder { - private final TextView mTitleTextView; - - AddRingtoneItemViewHolder(View itemView, Callbacks listener) { - super(itemView); - - mTitleTextView = itemView.requireViewById(R.id.add_new_sound_text); - itemView.setOnClickListener(v -> listener.onAddRingtoneSelected()); - } - - void onBind(@StringRes int title) { - mTitleTextView.setText(title); - } - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java deleted file mode 100644 index f08eb24ec20d..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import android.content.Context; -import android.media.RingtoneManager; - -import dagger.hilt.android.qualifiers.ApplicationContext; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * A factory class used to create {@link RingtoneManager}. - */ -@Singleton -public class RingtoneManagerFactory { - - private final Context mApplicationContext; - - @Inject - RingtoneManagerFactory(@ApplicationContext Context applicationContext) { - mApplicationContext = applicationContext; - } - - /** - * Creates a new {@link RingtoneManager} and returns it. - * - * @return a {@link RingtoneManager} - */ - public RingtoneManager create() { - return new RingtoneManager(mApplicationContext, /* includeParentRingtones */ true); - } -} - diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java deleted file mode 100644 index b94ebebd825b..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import android.app.Service; -import android.content.Intent; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Environment; -import android.os.FileUtils; -import android.os.IBinder; -import android.provider.MediaStore; -import android.provider.Settings.System; -import android.util.Log; - -import androidx.annotation.IdRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * Service to copy and set customization of default sounds - */ -public class RingtoneOverlayService extends Service { - private static final String TAG = "RingtoneOverlayService"; - private static final boolean DEBUG = false; - - @Override - public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) { - AsyncTask.execute(() -> { - updateRingtones(); - stopSelf(); - }); - - // Try again later if we are killed before we finish. - return Service.START_REDELIVER_INTENT; - } - - @Override - public IBinder onBind(@Nullable final Intent intent) { - return null; - } - - private void updateRingtones() { - copyResourceAndSetAsSound(R.raw.default_ringtone, - System.RINGTONE, Environment.DIRECTORY_RINGTONES); - copyResourceAndSetAsSound(R.raw.default_notification_sound, - System.NOTIFICATION_SOUND, Environment.DIRECTORY_NOTIFICATIONS); - copyResourceAndSetAsSound(R.raw.default_alarm_alert, - System.ALARM_ALERT, Environment.DIRECTORY_ALARMS); - } - - /* If the resource contains any data, copy a resource to the file system, scan it, and set the - * file URI as the default for a sound. */ - private void copyResourceAndSetAsSound(@IdRes final int id, @NonNull final String name, - @NonNull final String subPath) { - final File destDir = Environment.getExternalStoragePublicDirectory(subPath); - if (!destDir.exists() && !destDir.mkdirs()) { - Log.e(TAG, "can't create " + destDir.getAbsolutePath()); - return; - } - - final File dest = new File(destDir, "default_" + name + ".ogg"); - try ( - InputStream is = getResources().openRawResource(id); - FileOutputStream os = new FileOutputStream(dest); - ) { - if (is.available() > 0) { - FileUtils.copy(is, os); - final Uri uri = scanFile(dest); - if (uri != null) { - set(name, uri); - } - } else { - // TODO Shall we remove any former copied resource in this case and unset - // the defaults if we use this event a second time to clear the data? - if (DEBUG) Log.d(TAG, "Resource for " + name + " has no overlay"); - } - } catch (IOException e) { - Log.e(TAG, "Unable to open resource for " + name + ": " + e); - } - } - - private Uri scanFile(@NonNull final File file) { - return MediaStore.scanFile(getContentResolver(), file); - } - - private void set(@NonNull final String name, @NonNull final Uri uri) { - final Uri settingUri = System.getUriFor(name); - RingtoneManager.setActualDefaultRingtoneUri(this, - RingtoneManager.getDefaultType(settingUri), uri); - System.putInt(getContentResolver(), name + "_set", 1); - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java deleted file mode 100644 index 90a14f9717db..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.soundpicker; - -import android.content.Intent; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.UserHandle; -import android.util.Log; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.lifecycle.ViewModelProvider; - -import dagger.hilt.android.AndroidEntryPoint; - -/** - * The {@link RingtonePickerActivity} allows the user to choose one from all of the - * available ringtones. The chosen ringtone's URI will be persisted as a string. - * - * @see RingtoneManager#ACTION_RINGTONE_PICKER - */ -@AndroidEntryPoint(AppCompatActivity.class) -public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity { - - private static final String TAG = "RingtonePickerActivity"; - // TODO: Use the extra keys from RingtoneManager once they're added. - private static final String EXTRA_RINGTONE_PICKER_CATEGORY = "EXTRA_RINGTONE_PICKER_CATEGORY"; - private static final String EXTRA_VIBRATION_SHOW_DEFAULT = "EXTRA_VIBRATION_SHOW_DEFAULT"; - private static final String EXTRA_VIBRATION_DEFAULT_URI = "EXTRA_VIBRATION_DEFAULT_URI"; - private static final String EXTRA_VIBRATION_SHOW_SILENT = "EXTRA_VIBRATION_SHOW_SILENT"; - private static final String EXTRA_VIBRATION_EXISTING_URI = "EXTRA_VIBRATION_EXISTING_URI"; - private static final boolean RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED = false; - - private RingtonePickerViewModel mRingtonePickerViewModel; - private int mAttributesFlags; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_ringtone_picker); - - mRingtonePickerViewModel = new ViewModelProvider(this).get(RingtonePickerViewModel.class); - - Intent intent = getIntent(); - /** - * Id of the user to which the ringtone picker should list the ringtones - */ - int pickerUserId = UserHandle.myUserId(); - - // Get the types of ringtones to show - int ringtoneType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, - RingtonePickerViewModel.RINGTONE_TYPE_UNKNOWN); - - // AudioAttributes flags - mAttributesFlags |= intent.getIntExtra( - RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, - 0 /*defaultValue == no flags*/); - - boolean showOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons); - - String title = intent.getStringExtra(RingtoneManager.EXTRA_RINGTONE_TITLE); - if (title == null) { - title = getString(RingtonePickerViewModel.getTitleByType(ringtoneType)); - } - String ringtonePickerCategory = intent.getStringExtra(EXTRA_RINGTONE_PICKER_CATEGORY); - RingtonePickerViewModel.PickerType pickerType = mapCategoryToPickerType( - ringtonePickerCategory); - - RingtoneListHandler.Config soundListConfig = getSoundListConfig(pickerType, intent, - ringtoneType); - RingtoneListHandler.Config vibrationListConfig = getVibrationListConfig(pickerType, intent); - - RingtonePickerViewModel.Config pickerConfig = - new RingtonePickerViewModel.Config(title, pickerUserId, ringtoneType, - showOkCancelButtons, mAttributesFlags, pickerType); - - mRingtonePickerViewModel.init(pickerConfig, soundListConfig, vibrationListConfig); - - if (savedInstanceState == null) { - TabbedDialogFragment dialogFragment = new TabbedDialogFragment(); - - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Fragment prev = getSupportFragmentManager().findFragmentByTag(TabbedDialogFragment.TAG); - if (prev != null) { - ft.remove(prev); - } - ft.addToBackStack(null); - dialogFragment.show(ft, TabbedDialogFragment.TAG); - } - - // The volume keys will control the stream that we are choosing a ringtone for - setVolumeControlStream(mRingtonePickerViewModel.getRingtoneStreamType()); - } - - private RingtoneListHandler.Config getSoundListConfig( - RingtonePickerViewModel.PickerType pickerType, Intent intent, int ringtoneType) { - if (pickerType != RingtonePickerViewModel.PickerType.SOUND_PICKER - && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) { - // This ringtone picker does not require a sound picker. - return null; - } - - // Get whether to show the 'Default' sound item, and the URI to play when it's clicked - boolean hasDefaultSoundItem = - intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); - - // The Uri to play when the 'Default' sound item is clicked. - Uri uriForDefaultSoundItem = - intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI); - if (uriForDefaultSoundItem == null) { - uriForDefaultSoundItem = RingtonePickerViewModel.getDefaultItemUriByType(ringtoneType); - } - - // Get whether this list has the 'Silent' sound item. - boolean hasSilentSoundItem = - intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); - - // AudioAttributes flags - mAttributesFlags |= intent.getIntExtra( - RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, - 0 /*defaultValue == no flags*/); - - // Get the sound URI whose list item should have a checkmark - Uri existingSoundUri = intent - .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI); - - return new RingtoneListHandler.Config(hasDefaultSoundItem, - uriForDefaultSoundItem, hasSilentSoundItem, existingSoundUri); - } - - private RingtoneListHandler.Config getVibrationListConfig( - RingtonePickerViewModel.PickerType pickerType, Intent intent) { - if (pickerType != RingtonePickerViewModel.PickerType.VIBRATION_PICKER - && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) { - // This ringtone picker does not require a vibration picker. - return null; - } - - // Get whether to show the 'Default' vibration item, and the URI to play when it's clicked - boolean hasDefaultVibrationItem = - intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_DEFAULT, false); - - // The Uri to play when the 'Default' vibration item is clicked. - Uri uriForDefaultVibrationItem = intent.getParcelableExtra(EXTRA_VIBRATION_DEFAULT_URI); - - // Get whether this list has the 'Silent' vibration item. - boolean hasSilentVibrationItem = - intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_SILENT, true); - - // Get the vibration URI whose list item should have a checkmark - Uri existingVibrationUri = intent.getParcelableExtra(EXTRA_VIBRATION_EXISTING_URI); - - return new RingtoneListHandler.Config( - hasDefaultVibrationItem, uriForDefaultVibrationItem, hasSilentVibrationItem, - existingVibrationUri); - } - - @Override - public void onDestroy() { - mRingtonePickerViewModel.cancelPendingAsyncTasks(); - super.onDestroy(); - } - - @Override - protected void onStop() { - super.onStop(); - mRingtonePickerViewModel.onStop(isChangingConfigurations()); - } - - @Override - protected void onPause() { - super.onPause(); - mRingtonePickerViewModel.onPause(isChangingConfigurations()); - } - - /** - * Maps the ringtone picker category to the appropriate PickerType. - * If the category is null or the feature is still not released, then it defaults to sound - * picker. - * - * @param category the ringtone picker category. - * @return the corresponding picker type. - */ - private static RingtonePickerViewModel.PickerType mapCategoryToPickerType(String category) { - if (category == null || !RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED) { - return RingtonePickerViewModel.PickerType.SOUND_PICKER; - } - - switch (category) { - case "android.intent.category.RINGTONE_PICKER_RINGTONE": - return RingtonePickerViewModel.PickerType.RINGTONE_PICKER; - case "android.intent.category.RINGTONE_PICKER_SOUND": - return RingtonePickerViewModel.PickerType.SOUND_PICKER; - case "android.intent.category.RINGTONE_PICKER_VIBRATION": - return RingtonePickerViewModel.PickerType.VIBRATION_PICKER; - default: - Log.w(TAG, "Unrecognized category: " + category + ". Defaulting to sound picker."); - return RingtonePickerViewModel.PickerType.SOUND_PICKER; - } - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java deleted file mode 100644 index 2c0971121ccd..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import static java.util.Objects.requireNonNull; - -import android.annotation.Nullable; -import android.annotation.StringRes; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.provider.Settings; - -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModel; - -import com.android.internal.annotations.VisibleForTesting; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; - -import dagger.hilt.android.lifecycle.HiltViewModel; - -import java.io.IOException; -import java.util.concurrent.Executor; - -import javax.inject.Inject; - -/** - * A view model which holds immutable info about the picker state and means to retrieve and play - * currently selected ringtones. - */ -@HiltViewModel -public final class RingtonePickerViewModel extends ViewModel { - - static final int RINGTONE_TYPE_UNKNOWN = -1; - - /** - * Keep the currently playing ringtone around when changing orientation, so that it - * can be stopped later, after the activity is recreated. - */ - @VisibleForTesting - static Ringtone sPlayingRingtone; - - private static final String TAG = "RingtonePickerViewModel"; - - private final RingtoneManagerFactory mRingtoneManagerFactory; - private final RingtoneFactory mRingtoneFactory; - private final RingtoneListHandler mSoundListHandler; - private final RingtoneListHandler mVibrationListHandler; - private final ListeningExecutorService mListeningExecutorService; - - private RingtoneManager mRingtoneManager; - - /** - * The ringtone that's currently playing. - */ - private Ringtone mCurrentRingtone; - - private Config mPickerConfig; - - private ListenableFuture<Uri> mAddCustomRingtoneFuture; - - public enum PickerType { - RINGTONE_PICKER, - SOUND_PICKER, - VIBRATION_PICKER - } - - /** - * Holds immutable info on the picker that should be displayed. - */ - static final class Config { - public final String title; - /** - * Id of the user to which the ringtone picker should list the ringtones. - */ - public final int userId; - /** - * Ringtone type. - */ - public final int ringtoneType; - /** - * AudioAttributes flags. - */ - public final int audioAttributesFlags; - /** - * In the buttonless (watch-only) version we don't show the OK/Cancel buttons. - */ - public final boolean showOkCancelButtons; - - public final PickerType mPickerType; - - Config(String title, int userId, int ringtoneType, boolean showOkCancelButtons, - int audioAttributesFlags, PickerType pickerType) { - this.title = title; - this.userId = userId; - this.ringtoneType = ringtoneType; - this.showOkCancelButtons = showOkCancelButtons; - this.audioAttributesFlags = audioAttributesFlags; - this.mPickerType = pickerType; - } - } - - @Inject - RingtonePickerViewModel(RingtoneManagerFactory ringtoneManagerFactory, - RingtoneFactory ringtoneFactory, - ListeningExecutorServiceFactory listeningExecutorServiceFactory, - RingtoneListHandler soundListHandler, - RingtoneListHandler vibrationListHandler) { - mRingtoneManagerFactory = ringtoneManagerFactory; - mRingtoneFactory = ringtoneFactory; - mListeningExecutorService = listeningExecutorServiceFactory.createSingleThreadExecutor(); - mSoundListHandler = soundListHandler; - mVibrationListHandler = vibrationListHandler; - } - - @StringRes - static int getTitleByType(int ringtoneType) { - switch (ringtoneType) { - case RingtoneManager.TYPE_ALARM: - return com.android.internal.R.string.ringtone_picker_title_alarm; - case RingtoneManager.TYPE_NOTIFICATION: - return com.android.internal.R.string.ringtone_picker_title_notification; - default: - return com.android.internal.R.string.ringtone_picker_title; - } - } - - static Uri getDefaultItemUriByType(int ringtoneType) { - switch (ringtoneType) { - case RingtoneManager.TYPE_ALARM: - return Settings.System.DEFAULT_ALARM_ALERT_URI; - case RingtoneManager.TYPE_NOTIFICATION: - return Settings.System.DEFAULT_NOTIFICATION_URI; - default: - return Settings.System.DEFAULT_RINGTONE_URI; - } - } - - @StringRes - static int getAddNewItemTextByType(int ringtoneType) { - switch (ringtoneType) { - case RingtoneManager.TYPE_ALARM: - return R.string.add_alarm_text; - case RingtoneManager.TYPE_NOTIFICATION: - return R.string.add_notification_text; - default: - return R.string.add_ringtone_text; - } - } - - @StringRes - static int getDefaultRingtoneItemTextByType(int ringtoneType) { - switch (ringtoneType) { - case RingtoneManager.TYPE_ALARM: - return R.string.alarm_sound_default; - case RingtoneManager.TYPE_NOTIFICATION: - return R.string.notification_sound_default; - default: - return R.string.ringtone_default; - } - } - - void init(@NonNull Config pickerConfig, - RingtoneListHandler.Config soundListConfig, - RingtoneListHandler.Config vibrationListConfig) { - mRingtoneManager = mRingtoneManagerFactory.create(); - mPickerConfig = pickerConfig; - if (mPickerConfig.ringtoneType != RINGTONE_TYPE_UNKNOWN) { - mRingtoneManager.setType(mPickerConfig.ringtoneType); - } - if (soundListConfig != null) { - mSoundListHandler.init(soundListConfig, mRingtoneManager, - mRingtoneManager.getCursor()); - } - if (vibrationListConfig != null) { - // TODO: Switch to the vibration cursor, once the API is made available. - mVibrationListHandler.init(vibrationListConfig, mRingtoneManager, - mRingtoneManager.getCursor()); - } - } - - /** - * Re-initializes the view model which is required after updating any of the picker lists. - * This could happen when adding a custom ringtone. - */ - void reinit() { - init(mPickerConfig, mSoundListHandler.getRingtoneListConfig(), - mVibrationListHandler.getRingtoneListConfig()); - } - - @NonNull - Config getPickerConfig() { - requireInitCalled(); - return mPickerConfig; - } - - @NonNull - RingtoneListHandler getSoundListHandler() { - return mSoundListHandler; - } - - @NonNull - RingtoneListHandler getVibrationListHandler() { - return mVibrationListHandler; - } - - /** - * Combined the currently selected sound and vibration URIs and returns a unified URI. If the - * picker does not show either sound or vibration, that portion of the URI will be null. - * - * Currently only the sound URI is returned, since we don't have the API to retrieve vibrations - * yet. - * @return Combined sound and vibration URI. - */ - Uri getSelectedRingtoneUri() { - // TODO: Combine sound and vibration URIs before returning. - return mSoundListHandler.getSelectedRingtoneUri(); - } - - int getRingtoneStreamType() { - requireInitCalled(); - return mRingtoneManager.inferStreamType(); - } - - void onPause(boolean isChangingConfigurations) { - if (!isChangingConfigurations) { - stopAnyPlayingRingtone(); - } - } - - void onStop(boolean isChangingConfigurations) { - if (isChangingConfigurations) { - saveAnyPlayingRingtone(); - } else { - stopAnyPlayingRingtone(); - } - } - - /** - * Plays a ringtone which is created using the currently selected sound and vibration URIs. If - * this is a sound or vibration only picker, then the other portion of the URI will be empty - * and should not affect the played ringtone. - * - * Currently, we only use the sound URI to create the ringtone, since we still don't have the - * API to retrieve the available vibrations list. - */ - void playRingtone() { - requireInitCalled(); - stopAnyPlayingRingtone(); - - mCurrentRingtone = mRingtoneFactory.create(getSelectedRingtoneUri(), - mPickerConfig.audioAttributesFlags); - - if (mCurrentRingtone != null) { - mCurrentRingtone.play(); - } - } - - /** - * Cancels all pending async tasks. - */ - void cancelPendingAsyncTasks() { - if (mAddCustomRingtoneFuture != null && !mAddCustomRingtoneFuture.isDone()) { - mAddCustomRingtoneFuture.cancel(/* mayInterruptIfRunning= */ true); - } - } - - /** - * Adds an audio file to the list of ringtones asynchronously. - * Any previous async tasks are canceled before start the new one. - * - * @param uri Uri of the file to be added as ringtone. Must be a media file. - * @param type The type of the ringtone to be added. - * @param callback The callback to invoke when the task is completed. - * @param executor The executor to run the callback on when the task completes. - */ - void addSoundRingtoneAsync(Uri uri, int type, FutureCallback<Uri> callback, Executor executor) { - // Cancel any currently running add ringtone tasks before starting a new one - cancelPendingAsyncTasks(); - mAddCustomRingtoneFuture = mListeningExecutorService.submit( - () -> addRingtone(uri, type)); - Futures.addCallback(mAddCustomRingtoneFuture, callback, executor); - } - - /** - * Adds an audio file to the list of ringtones. - * - * @param uri Uri of the file to be added as ringtone. Must be a media file. - * @param type The type of the ringtone to be added. - * @return The Uri of the installed ringtone, which may be the {@code uri} if it - * is already in ringtone storage. Or null if it failed to add the audio file. - */ - @Nullable - private Uri addRingtone(Uri uri, int type) throws IOException { - requireInitCalled(); - return mRingtoneManager.addCustomExternalRingtone(uri, type); - } - - private void saveAnyPlayingRingtone() { - if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) { - sPlayingRingtone = mCurrentRingtone; - } - mCurrentRingtone = null; - } - - private void stopAnyPlayingRingtone() { - if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) { - sPlayingRingtone.stop(); - } - sPlayingRingtone = null; - - if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) { - mCurrentRingtone.stop(); - } - mCurrentRingtone = null; - } - - private void requireInitCalled() { - requireNonNull(mRingtoneManager); - requireNonNull(mPickerConfig); - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java deleted file mode 100644 index 6a349366e744..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.soundpicker; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class RingtoneReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (Intent.ACTION_DEVICE_CUSTOMIZATION_READY.equals(action)) { - initResourceRingtones(context); - } - } - - private void initResourceRingtones(Context context) { - context.startService( - new Intent(context, RingtoneOverlayService.class)); - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java deleted file mode 100644 index a37191f33668..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.util.Log; -import android.view.View; -import android.widget.Toast; - -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.core.content.ContextCompat; -import androidx.lifecycle.ViewModelProvider; - -import com.google.common.util.concurrent.FutureCallback; - -import org.jetbrains.annotations.NotNull; - -/** - * A fragment that displays a picker used to select sound or silent. It also includes the - * ability to add custom sounds. - */ -public class SoundPickerFragment extends BasePickerFragment { - - private static final String TAG = "SoundPickerFragment"; - - private final FutureCallback<Uri> mAddCustomRingtoneCallback = new FutureCallback<>() { - @Override - public void onSuccess(Uri ringtoneUri) { - requeryForAdapter(); - } - - @Override - public void onFailure(Throwable throwable) { - Log.e(TAG, "Failed to add custom ringtone.", throwable); - // Ringtone was not added, display error Toast - Toast.makeText(requireActivity().getApplicationContext(), - R.string.unable_to_add_ringtone, Toast.LENGTH_SHORT).show(); - } - }; - - ActivityResultLauncher<Intent> mActivityResultLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - new ActivityResultCallback<ActivityResult>() { - @Override - public void onActivityResult(ActivityResult result) { - if (result.getResultCode() == Activity.RESULT_OK) { - // There are no request codes - Intent data = result.getData(); - mRingtonePickerViewModel.addSoundRingtoneAsync(data.getData(), - mPickerConfig.ringtoneType, - mAddCustomRingtoneCallback, - // Causes the callback to be executed on the main thread. - ContextCompat.getMainExecutor( - requireActivity().getApplicationContext())); - } - } - }); - - @Override - public void onViewCreated(@NotNull View view, Bundle savedInstanceState) { - mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get( - RingtonePickerViewModel.class); - super.onViewCreated(view, savedInstanceState); - } - - @Override - protected RingtoneListHandler getRingtoneListHandler() { - return mRingtonePickerViewModel.getSoundListHandler(); - } - - @Override - protected void addRingtoneAsync() { - // The "Add new ringtone" item was clicked. Start a file picker intent to - // select only audio files (MIME type "audio/*") - final Intent chooseFile = getMediaFilePickerIntent(); - mActivityResultLauncher.launch(chooseFile); - } - - @Override - protected void addNewRingtoneItem() { - // If external storage is available, add a button to install sounds from storage. - if (resolvesMediaFilePicker() - && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - mRingtoneListViewAdapter.addTitleForAddRingtoneItem( - RingtonePickerViewModel.getAddNewItemTextByType(mPickerConfig.ringtoneType)); - } - } - - private boolean resolvesMediaFilePicker() { - return getMediaFilePickerIntent().resolveActivity(requireActivity().getPackageManager()) - != null; - } - - private Intent getMediaFilePickerIntent() { - final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT); - chooseFile.setType("audio/*"); - chooseFile.putExtra(Intent.EXTRA_MIME_TYPES, - new String[]{"audio/*", "application/ogg"}); - return chooseFile; - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java deleted file mode 100644 index 50ea9d7d3056..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import static android.app.Activity.RESULT_CANCELED; - -import android.app.Activity; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.media.RingtoneManager; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; -import androidx.lifecycle.ViewModelProvider; -import androidx.viewpager2.widget.ViewPager2; - -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - -import dagger.hilt.android.AndroidEntryPoint; - -import org.jetbrains.annotations.NotNull; - -/** - * A dialog fragment with a sound and/or vibration tab based on the picker type. - * <ul> - * <li> Ringtone Pickers will display both sound and vibration tabs. - * <li> Sound Pickers will only display the sound tab. - * <li> Vibration Pickers will only display the vibration tab. - * </ul> - */ -@AndroidEntryPoint(DialogFragment.class) -public class TabbedDialogFragment extends Hilt_TabbedDialogFragment { - - static final String TAG = "TabbedDialogFragment"; - - private RingtonePickerViewModel mRingtonePickerViewModel; - - private final ViewPager2.OnPageChangeCallback mOnPageChangeCallback = - new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageScrollStateChanged(int state) { - super.onPageScrollStateChanged(state); - if (state == ViewPager2.SCROLL_STATE_IDLE) { - mRingtonePickerViewModel.onStop(/* isChangingConfigurations= */ false); - } - } - }; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get( - RingtonePickerViewModel.class); - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity(), - android.R.style.ThemeOverlay_Material_Dialog) - .setTitle(mRingtonePickerViewModel.getPickerConfig().title); - // Do not show OK/Cancel buttons in the buttonless (watch-only) version. - if (mRingtonePickerViewModel.getPickerConfig().showOkCancelButtons) { - dialogBuilder - .setPositiveButton(getString(com.android.internal.R.string.ok), - (dialog, whichButton) -> { - setSuccessResultWithSelectedRingtone(); - requireActivity().finish(); - }) - .setNegativeButton(getString(com.android.internal.R.string.cancel), - (dialog, whichButton) -> { - requireActivity().setResult(RESULT_CANCELED); - requireActivity().finish(); - }); - } - - View view = buildTabbedView(requireActivity().getLayoutInflater()); - dialogBuilder.setView(view); - - return dialogBuilder.create(); - } - - @Override - public void onCancel(@NonNull @NotNull DialogInterface dialog) { - super.onCancel(dialog); - if (!requireActivity().isChangingConfigurations()) { - requireActivity().finish(); - } - } - - @Override - public void onDismiss(@NonNull @NotNull DialogInterface dialog) { - super.onDismiss(dialog); - if (!requireActivity().isChangingConfigurations()) { - requireActivity().finish(); - } - } - - private void setSuccessResultWithSelectedRingtone() { - requireActivity().setResult(Activity.RESULT_OK, - new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, - mRingtonePickerViewModel.getSelectedRingtoneUri())); - } - - /** - * Inflates the tabbed layout view and adds the required fragments. If there's only one - * fragment to display, then the tab area is hidden. - * @param inflater The LayoutInflater that is used to inflate the tabbed view. - * @return The tabbed view. - */ - private View buildTabbedView(@NonNull LayoutInflater inflater) { - View view = inflater.inflate(R.layout.fragment_tabbed_dialog, null, false); - TabLayout tabLayout = view.requireViewById(R.id.tabLayout); - ViewPager2 viewPager = view.requireViewById(R.id.masterViewPager); - - ViewPagerAdapter adapter = new ViewPagerAdapter(requireActivity()); - addFragments(adapter); - - if (adapter.getItemCount() == 1) { - // Hide the tab area since there's only one fragment to display. - tabLayout.setVisibility(View.GONE); - } - - viewPager.setAdapter(adapter); - viewPager.registerOnPageChangeCallback(mOnPageChangeCallback); - new TabLayoutMediator(tabLayout, viewPager, - (tab, position) -> tab.setText(adapter.getTitle(position))).attach(); - - return view; - } - - /** - * Adds the appropriate fragments to the adapter based on the PickerType. - * - * @param adapter The adapter to add the fragments to. - */ - private void addFragments(ViewPagerAdapter adapter) { - switch (mRingtonePickerViewModel.getPickerConfig().mPickerType) { - case RINGTONE_PICKER: - adapter.addFragment(getString(R.string.sound_page_title), - new SoundPickerFragment()); - adapter.addFragment(getString(R.string.vibration_page_title), - new VibrationPickerFragment()); - break; - case SOUND_PICKER: - adapter.addFragment(getString(R.string.sound_page_title), - new SoundPickerFragment()); - break; - case VIBRATION_PICKER: - adapter.addFragment(getString(R.string.vibration_page_title), - new VibrationPickerFragment()); - break; - default: - adapter.addFragment(getString(R.string.sound_page_title), - new SoundPickerFragment()); - break; - } - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java deleted file mode 100644 index 7412c1995b5a..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import android.os.Bundle; -import android.view.View; - -import androidx.lifecycle.ViewModelProvider; - -import org.jetbrains.annotations.NotNull; - -/** - * A fragment that displays a picker used to select vibration or silent (no vibration). - */ -public class VibrationPickerFragment extends BasePickerFragment { - - @Override - public void onViewCreated(@NotNull View view, Bundle savedInstanceState) { - mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get( - RingtonePickerViewModel.class); - super.onViewCreated(view, savedInstanceState); - } - - @Override - protected RingtoneListHandler getRingtoneListHandler() { - return mRingtonePickerViewModel.getVibrationListHandler(); - } - - @Override - protected void addRingtoneAsync() { - // no-op - } - - @Override - protected void addNewRingtoneItem() { - // no-op - } -} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java b/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java deleted file mode 100644 index 179068e9f20f..000000000000 --- a/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * An adapter used to populate pages inside a ViewPager. - */ -public class ViewPagerAdapter extends FragmentStateAdapter { - - private final List<Fragment> mFragments = new ArrayList<>(); - private final List<String> mTitles = new ArrayList<>(); - - public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) { - super(fragmentActivity); - } - - /** - * Adds a fragment and page title to the adapter. - * @param title the title of the page in the ViewPager. - * @param fragment the fragment that will be inflated on this page. - */ - public void addFragment(String title, Fragment fragment) { - mTitles.add(title); - mFragments.add(fragment); - } - - /** - * Returns the title of the requested page. - * @param position the position of the page in the Viewpager. - * @return The title of the requested page. - */ - public String getTitle(int position) { - return mTitles.get(position); - } - - @NonNull - @Override - public Fragment createFragment(int position) { - return Objects.requireNonNull(mFragments.get(position), - "Could not find a fragment using position: " + position); - } - - @Override - public int getItemCount() { - return mFragments.size(); - } -} diff --git a/packages/SoundPicker2/tests/Android.bp b/packages/SoundPicker2/tests/Android.bp deleted file mode 100644 index d88d442afa17..000000000000 --- a/packages/SoundPicker2/tests/Android.bp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2023, The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package { - default_applicable_licenses: ["frameworks_base_license"], -} - -android_test { - name: "SoundPicker2Tests", - certificate: "platform", - libs: [ - "android.test.runner", - "android.test.base", - ], - static_libs: [ - "androidx.test.core", - "androidx.test.rules", - "androidx.test.ext.junit", - "androidx.test.ext.truth", - "mockito-target-minus-junit4", - "guava-android-testlib", - "SoundPicker2Lib", - ], - srcs: [ - "src/**/*.java", - ], -} diff --git a/packages/SoundPicker2/tests/AndroidManifest.xml b/packages/SoundPicker2/tests/AndroidManifest.xml deleted file mode 100644 index 295aeb1faa55..000000000000 --- a/packages/SoundPicker2/tests/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.soundpicker.tests"> - - <application android:debuggable="true"> - <uses-library android:name="android.test.runner" /> - </application> - <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" - android:targetPackage="com.android.soundpicker.tests" - android:label="Sound picker tests"> - </instrumentation> -</manifest> diff --git a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java deleted file mode 100644 index 80e71e200a53..000000000000 --- a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -import android.database.Cursor; -import android.media.RingtoneManager; -import android.net.Uri; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -public class RingtoneListHandlerTest { - - private static final Uri DEFAULT_URI = Uri.parse("media://custom/ringtone/default_uri"); - private static final Uri RINGTONE_URI = Uri.parse("media://custom/ringtone/uri"); - private static final int SILENT_RINGTONE_POSITION = 0; - private static final int DEFAULT_RINGTONE_POSITION = 1; - private static final int RINGTONE_POSITION = 2; - - @Mock - private RingtoneManager mMockRingtoneManager; - @Mock - private Cursor mMockCursor; - - private RingtoneListHandler mRingtoneListHandler; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - RingtoneListHandler.Config mRingtoneListConfig = createRingtoneListConfig(); - - mRingtoneListHandler = new RingtoneListHandler(); - - // Add silent and default options to the list. - mRingtoneListHandler.addSilentItem(); - mRingtoneListHandler.addDefaultItem(); - - mRingtoneListHandler.init(mRingtoneListConfig, mMockRingtoneManager, mMockCursor); - } - - @Test - public void testGetRingtoneCursor_returnsTheCorrectRingtoneCursor() { - assertThat(mRingtoneListHandler.getRingtoneCursor()).isEqualTo(mMockCursor); - } - - @Test - public void testGetRingtoneUri_returnsTheCorrectRingtoneUri() { - Uri expectedUri = RINGTONE_URI; - when(mMockRingtoneManager.getRingtoneUri(eq(0))).thenReturn(expectedUri); - - // Request 3rd item from list. - Uri actualUri = mRingtoneListHandler.getRingtoneUri(RINGTONE_POSITION); - assertThat(actualUri).isEqualTo(expectedUri); - } - - @Test - public void testGetRingtoneUri_withSelectedItemUnknown_returnsTheCorrectRingtoneUri() { - Uri uri = mRingtoneListHandler.getRingtoneUri(RingtoneListHandler.ITEM_POSITION_UNKNOWN); - assertThat(uri).isEqualTo(RingtoneListHandler.SILENT_URI); - } - - @Test - public void testGetRingtoneUri_withSelectedItemDefaultPosition_returnsTheCorrectRingtoneUri() { - Uri actualUri = mRingtoneListHandler.getRingtoneUri(DEFAULT_RINGTONE_POSITION); - assertThat(actualUri).isEqualTo(DEFAULT_URI); - } - - @Test - public void testGetRingtoneUri_withSelectedItemSilentPosition_returnsTheCorrectRingtoneUri() { - Uri uri = mRingtoneListHandler.getRingtoneUri(SILENT_RINGTONE_POSITION); - assertThat(uri).isEqualTo(RingtoneListHandler.SILENT_URI); - } - - @Test - public void testGetCurrentlySelectedRingtoneUri_returnsTheCorrectRingtoneUri() { - mRingtoneListHandler.setSelectedItemPosition(RingtoneListHandler.ITEM_POSITION_UNKNOWN); - Uri actualUri = mRingtoneListHandler.getSelectedRingtoneUri(); - assertThat(actualUri).isEqualTo(RingtoneListHandler.SILENT_URI); - - mRingtoneListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION); - actualUri = mRingtoneListHandler.getSelectedRingtoneUri(); - assertThat(actualUri).isEqualTo(DEFAULT_URI); - - mRingtoneListHandler.setSelectedItemPosition(SILENT_RINGTONE_POSITION); - actualUri = mRingtoneListHandler.getSelectedRingtoneUri(); - assertThat(actualUri).isEqualTo(RingtoneListHandler.SILENT_URI); - - when(mMockRingtoneManager.getRingtoneUri(eq(0))).thenReturn(RINGTONE_URI); - mRingtoneListHandler.setSelectedItemPosition(RINGTONE_POSITION); - actualUri = mRingtoneListHandler.getSelectedRingtoneUri(); - assertThat(actualUri).isEqualTo(RINGTONE_URI); - } - - @Test - public void testGetRingtonePosition_returnsTheCorrectRingtonePosition() { - when(mMockRingtoneManager.getRingtonePosition(RINGTONE_URI)).thenReturn(0); - - int actualPosition = mRingtoneListHandler.getRingtonePosition(RINGTONE_URI); - - assertThat(actualPosition).isEqualTo(RINGTONE_POSITION); - - } - - @Test - public void testFixedItems_onlyAddsItemsOnceAndInOrder() { - // Clear fixed items before testing the add methods. - mRingtoneListHandler.resetFixedItems(); - - assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo( - RingtoneListHandler.ITEM_POSITION_UNKNOWN); - assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo( - RingtoneListHandler.ITEM_POSITION_UNKNOWN); - - mRingtoneListHandler.addSilentItem(); - mRingtoneListHandler.addDefaultItem(); - mRingtoneListHandler.addSilentItem(); - mRingtoneListHandler.addDefaultItem(); - - assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo( - SILENT_RINGTONE_POSITION); - assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo( - DEFAULT_RINGTONE_POSITION); - } - - @Test - public void testResetFixedItems_resetsSilentAndDefaultItemPositions() { - assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo( - SILENT_RINGTONE_POSITION); - assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo( - DEFAULT_RINGTONE_POSITION); - - mRingtoneListHandler.resetFixedItems(); - - assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo( - RingtoneListHandler.ITEM_POSITION_UNKNOWN); - assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo( - RingtoneListHandler.ITEM_POSITION_UNKNOWN); - } - - private RingtoneListHandler.Config createRingtoneListConfig() { - return new RingtoneListHandler.Config(/* hasDefaultItem= */ true, - /* uriForDefaultItem= */ DEFAULT_URI, /* hasSilentItem= */ true, - /* existingUri= */ DEFAULT_URI); - } -} diff --git a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java deleted file mode 100644 index cde6c76d27ff..000000000000 --- a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java +++ /dev/null @@ -1,534 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.soundpicker; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -import android.database.Cursor; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.provider.Settings; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.testing.TestingExecutors; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.io.IOException; -import java.util.concurrent.ExecutorService; - -@RunWith(AndroidJUnit4.class) -public class RingtonePickerViewModelTest { - - private static final Uri DEFAULT_URI = Uri.parse("media://custom/ringtone/default_uri"); - private static final Uri RINGTONE_URI = Uri.parse("media://custom/ringtone/uri"); - private static final int RINGTONE_TYPE_UNKNOWN = -1; - private static final int DEFAULT_RINGTONE_POSITION = 1; - - @Mock - private RingtoneManagerFactory mMockRingtoneManagerFactory; - @Mock - private RingtoneFactory mMockRingtoneFactory; - @Mock - private RingtoneManager mMockRingtoneManager; - @Mock - private ListeningExecutorServiceFactory mMockListeningExecutorServiceFactory; - @Mock - private Cursor mMockCursor; - - private RingtoneListHandler mSoundListHandler; - private RingtoneListHandler mVibrationListHandler; - private ExecutorService mMainThreadExecutor; - private ListeningExecutorService mBackgroundThreadExecutor; - private Ringtone mMockDefaultRingtone; - private Ringtone mMockRingtone; - private RingtonePickerViewModel mViewModel; - private RingtoneListHandler.Config mSoundListConfig; - private RingtoneListHandler.Config mVibrationListConfig; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mSoundListHandler = new RingtoneListHandler(); - mVibrationListHandler = new RingtoneListHandler(); - mSoundListConfig = createRingtoneListConfig(); - mVibrationListConfig = createRingtoneListConfig(); - mMockDefaultRingtone = createMockRingtone(); - mMockRingtone = createMockRingtone(); - when(mMockRingtoneManagerFactory.create()).thenReturn(mMockRingtoneManager); - when(mMockRingtoneFactory.create(DEFAULT_URI, - AudioAttributes.FLAG_AUDIBILITY_ENFORCED)).thenReturn(mMockDefaultRingtone); - when(mMockRingtoneManager.getRingtoneUri(anyInt())).thenReturn(RINGTONE_URI); - when(mMockRingtoneManager.getCursor()).thenReturn(mMockCursor); - mMainThreadExecutor = TestingExecutors.sameThreadScheduledExecutor(); - mBackgroundThreadExecutor = TestingExecutors.sameThreadScheduledExecutor(); - when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn( - mBackgroundThreadExecutor); - - mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory, - mMockListeningExecutorServiceFactory, mSoundListHandler, - mVibrationListHandler); - - // Add silent and default options to the sound list. - mSoundListHandler.addSilentItem(); - mSoundListHandler.addDefaultItem(); - - // Add silent and default options to the vibration list. - mVibrationListHandler.addSilentItem(); - mVibrationListHandler.addDefaultItem(); - - mSoundListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION); - mVibrationListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION); - } - - @After - public void teardown() { - if (mMainThreadExecutor != null && !mMainThreadExecutor.isShutdown()) { - mMainThreadExecutor.shutdown(); - } - if (mBackgroundThreadExecutor != null && !mBackgroundThreadExecutor.isShutdown()) { - mBackgroundThreadExecutor.shutdown(); - } - } - - @Test - public void testInitRingtoneManager_whenTypeIsUnknown_createManagerButDoNotSetType() { - mViewModel.init(createPickerConfig(RINGTONE_TYPE_UNKNOWN), mSoundListConfig, - mVibrationListConfig); - - verify(mMockRingtoneManagerFactory).create(); - verify(mMockRingtoneManager, never()).setType(anyInt()); - assertNotNull(mViewModel.getSoundListHandler().getRingtoneListConfig()); - assertNotNull(mViewModel.getVibrationListHandler().getRingtoneListConfig()); - } - - @Test - public void testInitRingtoneManager_whenTypeIsNotUnknown_createManagerAndSetType() { - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_NOTIFICATION), mSoundListConfig, - mVibrationListConfig); - - verify(mMockRingtoneManagerFactory).create(); - verify(mMockRingtoneManager).setType(RingtoneManager.TYPE_NOTIFICATION); - assertNotNull(mViewModel.getSoundListHandler().getRingtoneListConfig()); - assertNotNull(mViewModel.getVibrationListHandler().getRingtoneListConfig()); - } - - @Test - public void testInitRingtoneManager_bothListConfigsAreNull_onlyRecreateRingtoneManager() { - mViewModel.init( - createPickerConfig(RingtoneManager.TYPE_NOTIFICATION), - /* soundListConfig= */ null, /* vibrationListConfig= */ null); - - verify(mMockRingtoneManagerFactory).create(); - verify(mMockRingtoneManager).setType(RingtoneManager.TYPE_NOTIFICATION); - assertNull(mViewModel.getSoundListHandler().getRingtoneListConfig()); - assertNull(mViewModel.getVibrationListHandler().getRingtoneListConfig()); - } - - @Test - public void testReinitialize_bothListConfigsInitialized_recreateManagerAndReinitHandlers() { - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_NOTIFICATION), mSoundListConfig, - mVibrationListConfig); - mViewModel.reinit(); - - verify(mMockRingtoneManagerFactory, times(2)).create(); - verify(mMockRingtoneManager, times(2)).setType(RingtoneManager.TYPE_NOTIFICATION); - assertNotNull(mViewModel.getSoundListHandler().getRingtoneListConfig()); - assertNotNull(mViewModel.getVibrationListHandler().getRingtoneListConfig()); - } - - @Test - public void testReinitialize_bothListConfigsAlreadyNull_onlyRecreateRingtoneManager() { - mViewModel.init( - createPickerConfig(RingtoneManager.TYPE_NOTIFICATION), - /* soundListConfig= */ null, /* vibrationListConfig= */ null); - mViewModel.reinit(); - - verify(mMockRingtoneManagerFactory, times(2)).create(); - verify(mMockRingtoneManager, times(2)).setType(RingtoneManager.TYPE_NOTIFICATION); - assertNull(mViewModel.getSoundListHandler().getRingtoneListConfig()); - assertNull(mViewModel.getVibrationListHandler().getRingtoneListConfig()); - } - - @Test - public void testGetStreamType_returnsTheCorrectStreamType() { - when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM); - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - assertEquals(mViewModel.getRingtoneStreamType(), AudioManager.STREAM_ALARM); - } - - @Test - public void testOnPause_withChangingConfigurationTrue_doNotStopPlayingRingtone() { - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone); - mViewModel.onPause(/* isChangingConfigurations= */ true); - verify(mMockDefaultRingtone, never()).stop(); - } - - @Test - public void testOnPause_withChangingConfigurationFalse_stopPlayingRingtone() { - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone); - mViewModel.onPause(/* isChangingConfigurations= */ false); - verify(mMockDefaultRingtone).stop(); - } - - @Test - public void testOnViewModelRecreated_previousRingtoneCanStillBeStopped() { - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - Ringtone mockRingtone1 = createMockRingtone(); - Ringtone mockRingtone2 = createMockRingtone(); - - when(mMockRingtoneFactory.create(any(), anyInt())).thenReturn(mockRingtone1, mockRingtone2); - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mockRingtone1); - // Fake a scenario where the activity is destroyed and recreated due to a config change. - // This will result in a new view model getting created. - mViewModel.onStop(/* isChangingConfigurations= */ true); - verify(mockRingtone1, never()).stop(); - mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory, - mMockListeningExecutorServiceFactory, mSoundListHandler, - mVibrationListHandler); - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mockRingtone2); - verify(mockRingtone1).stop(); - verify(mockRingtone2, never()).stop(); - } - - @Test - public void testOnStop_withChangingConfigurationTrueAndDefaultRingtonePlaying_saveRingtone() { - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone); - mViewModel.onStop(/* isChangingConfigurations= */ true); - assertEquals(RingtonePickerViewModel.sPlayingRingtone, mMockDefaultRingtone); - } - - @Test - public void testOnStop_withChangingConfigurationTrueAndCurrentRingtonePlaying_saveRingtone() { - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone); - mViewModel.onStop(/* isChangingConfigurations= */ true); - assertEquals(RingtonePickerViewModel.sPlayingRingtone, mMockDefaultRingtone); - } - - @Test - public void testOnStop_withChangingConfigurationTrueAndNoPlayingRingtone_saveNothing() { - mViewModel.onStop(/* isChangingConfigurations= */ true); - assertNull(RingtonePickerViewModel.sPlayingRingtone); - } - - @Test - public void testOnStop_withChangingConfigurationFalse_stopPlayingRingtone() { - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone); - mViewModel.onStop(/* isChangingConfigurations= */ false); - verify(mMockDefaultRingtone).stop(); - } - - @Test - public void testGetCurrentlySelectedRingtoneUri_returnsTheCorrectRingtoneUri() { - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - - assertEquals(DEFAULT_URI, mViewModel.getSelectedRingtoneUri()); - } - - @Test - public void testPlayRingtone_playTheCorrectRingtone() { - mSoundListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION); - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone); - } - - @Test - public void testPlayRingtone_stopsPreviouslyRunningRingtone() { - // Start playing the first ringtone - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone); - // Start playing the second ringtone - when(mMockRingtoneFactory.create(DEFAULT_URI, - AudioAttributes.FLAG_AUDIBILITY_ENFORCED)).thenReturn(mMockRingtone); - mViewModel.playRingtone(); - verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone); - - verify(mMockDefaultRingtone).stop(); - } - - @Test - public void testDefaultItemUri_withNotificationIntent_returnDefaultNotificationUri() { - Uri uri = RingtonePickerViewModel.getDefaultItemUriByType( - RingtoneManager.TYPE_NOTIFICATION); - assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, uri); - } - - @Test - public void testDefaultItemUri_withAlarmIntent_returnDefaultAlarmUri() { - Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(RingtoneManager.TYPE_ALARM); - assertEquals(Settings.System.DEFAULT_ALARM_ALERT_URI, uri); - } - - @Test - public void testDefaultItemUri_withRingtoneIntent_returnDefaultRingtoneUri() { - Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(RingtoneManager.TYPE_RINGTONE); - assertEquals(Settings.System.DEFAULT_RINGTONE_URI, uri); - } - - @Test - public void testDefaultItemUri_withInvalidRingtoneType_returnDefaultRingtoneUri() { - Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(-1); - assertEquals(Settings.System.DEFAULT_RINGTONE_URI, uri); - } - - @Test - public void testTitle_withNotificationRingtoneType_returnRingtoneNotificationTitle() { - int title = RingtonePickerViewModel.getTitleByType(RingtoneManager.TYPE_NOTIFICATION); - assertEquals(com.android.internal.R.string.ringtone_picker_title_notification, title); - } - - @Test - public void testTitle_withAlarmRingtoneType_returnRingtoneAlarmTitle() { - int title = RingtonePickerViewModel.getTitleByType(RingtoneManager.TYPE_ALARM); - assertEquals(com.android.internal.R.string.ringtone_picker_title_alarm, title); - } - - @Test - public void testTitle_withInvalidRingtoneType_returnDefaultRingtoneTitle() { - int title = RingtonePickerViewModel.getTitleByType(/*ringtoneType= */ -1); - assertEquals(com.android.internal.R.string.ringtone_picker_title, title); - } - - @Test - public void testAddNewItemText_withAlarmType_returnAlarmAddItemText() { - int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType( - RingtoneManager.TYPE_ALARM); - assertEquals(R.string.add_alarm_text, addNewItemTextResId); - } - - @Test - public void testAddNewItemText_withNotificationType_returnNotificationAddItemText() { - int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType( - RingtoneManager.TYPE_NOTIFICATION); - assertEquals(R.string.add_notification_text, addNewItemTextResId); - } - - @Test - public void testAddNewItemText_withRingtoneType_returnRingtoneAddItemText() { - int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType( - RingtoneManager.TYPE_RINGTONE); - assertEquals(R.string.add_ringtone_text, addNewItemTextResId); - } - - @Test - public void testAddNewItemText_withInvalidType_returnRingtoneAddItemText() { - int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType(-1); - assertEquals(R.string.add_ringtone_text, addNewItemTextResId); - } - - @Test - public void testDefaultItemText_withNotificationType_returnNotificationDefaultItemText() { - int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType( - RingtoneManager.TYPE_NOTIFICATION); - assertEquals(R.string.notification_sound_default, defaultRingtoneItemText); - } - - @Test - public void testDefaultItemText_withAlarmType_returnAlarmDefaultItemText() { - int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType( - RingtoneManager.TYPE_NOTIFICATION); - assertEquals(R.string.notification_sound_default, defaultRingtoneItemText); - } - - @Test - public void testDefaultItemText_withRingtoneType_returnRingtoneDefaultItemText() { - int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType( - RingtoneManager.TYPE_RINGTONE); - assertEquals(R.string.ringtone_default, defaultRingtoneItemText); - } - - @Test - public void testDefaultItemText_withInvalidType_returnRingtoneDefaultItemText() { - int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(-1); - assertEquals(R.string.ringtone_default, defaultRingtoneItemText); - } - - @Test - public void testCancelPendingAsyncTasks_correctlyCancelsPendingTasks() - throws IOException { - FutureCallback<Uri> mockCallback = mock(FutureCallback.class); - - when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn( - TestingExecutors.noOpScheduledExecutor()); - - mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory, - mMockListeningExecutorServiceFactory, mSoundListHandler, - mVibrationListHandler); - mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, - mockCallback, mMainThreadExecutor); - verify(mockCallback, never()).onFailure(any()); - // Calling cancelPendingAsyncTasks should cancel the pending task. Cancelling an async - // task invokes the onFailure method in the callable. - mViewModel.cancelPendingAsyncTasks(); - verify(mockCallback).onFailure(any()); - verify(mockCallback, never()).onSuccess(any()); - - } - - @Test - public void testAddRingtoneAsync_cancelPreviousTaskBeforeStartingNewOne() - throws IOException { - FutureCallback<Uri> mockCallback1 = mock(FutureCallback.class); - FutureCallback<Uri> mockCallback2 = mock(FutureCallback.class); - - when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn( - TestingExecutors.noOpScheduledExecutor()); - - mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory, - mMockListeningExecutorServiceFactory, mSoundListHandler, - mVibrationListHandler); - mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, - mockCallback1, mMainThreadExecutor); - verify(mockCallback1, never()).onFailure(any()); - // We call addRingtoneAsync again to cancel the previous task and start a new one. - // Cancelling an async task invokes the onFailure method in the callable. - mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, - mockCallback2, mMainThreadExecutor); - verify(mockCallback1).onFailure(any()); - verify(mockCallback1, never()).onSuccess(any()); - verifyNoMoreInteractions(mockCallback2); - } - - @Test - public void testAddRingtoneAsync_whenAddRingtoneIsSuccessful_successCallbackIsInvoked() - throws IOException { - Uri expectedUri = DEFAULT_URI; - FutureCallback<Uri> mockCallback = mock(FutureCallback.class); - - when(mMockRingtoneManager.addCustomExternalRingtone(DEFAULT_URI, - RingtoneManager.TYPE_NOTIFICATION)).thenReturn(expectedUri); - - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - - mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, - mockCallback, mMainThreadExecutor); - - verify(mockCallback).onSuccess(expectedUri); - verify(mockCallback, never()).onFailure(any()); - } - - @Test - public void testAddRingtoneAsync_whenAddRingtoneFailed_failureCallbackIsInvoked() - throws IOException { - FutureCallback<Uri> mockCallback = mock(FutureCallback.class); - - when(mMockRingtoneManager.addCustomExternalRingtone(any(), anyInt())).thenThrow( - IOException.class); - - mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig, - mVibrationListConfig); - - mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION, - mockCallback, mMainThreadExecutor); - - verify(mockCallback).onFailure(any(IOException.class)); - verify(mockCallback, never()).onSuccess(any()); - } - - private Ringtone createMockRingtone() { - Ringtone mockRingtone = mock(Ringtone.class); - when(mockRingtone.getAudioAttributes()).thenReturn( - audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, 0)); - - return mockRingtone; - } - - private void verifyRingtonePlayCalledAndMockPlayingState(Ringtone ringtone) { - verify(ringtone).play(); - when(ringtone.isPlaying()).thenReturn(true); - } - - private static AudioAttributes audioAttributes(int audioUsage, int flags) { - return new AudioAttributes.Builder() - .setUsage(audioUsage) - .setFlags(flags) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build(); - } - - private RingtonePickerViewModel.Config createPickerConfig(int ringtoneType, - int audioAttributes) { - return new RingtonePickerViewModel.Config("Phone ringtone", /* userId= */ 1, - ringtoneType, /* showOkCancelButtons= */ true, - audioAttributes, RingtonePickerViewModel.PickerType.RINGTONE_PICKER); - } - - private RingtonePickerViewModel.Config createPickerConfig(int ringtoneType) { - return new RingtonePickerViewModel.Config("Phone ringtone", /* userId= */ 1, - ringtoneType, /* showOkCancelButtons= */ true, - AudioAttributes.FLAG_AUDIBILITY_ENFORCED, - RingtonePickerViewModel.PickerType.RINGTONE_PICKER); - } - - private RingtoneListHandler.Config createRingtoneListConfig() { - return new RingtoneListHandler.Config(/* hasDefaultItem= */ true, - /* uriForDefaultItem= */ DEFAULT_URI, /* hasSilentItem= */ true, - /* existingUri= */ Uri.parse("")); - } -} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 81940553b127..4973cafbb397 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -53,12 +53,12 @@ private const val TAG = "ActivityLaunchAnimator" */ class ActivityLaunchAnimator( /** The animator used when animating a View into an app. */ - private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, + private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR, /** The animator used when animating a Dialog into an app. */ // TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to // TIMINGS.contentBeforeFadeOutDuration. - private val dialogToAppAnimator: LaunchAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR, + private val dialogToAppAnimator: TransitionAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR, /** * Whether we should disable the WindowManager timeout. This should be set to true in tests @@ -71,7 +71,7 @@ class ActivityLaunchAnimator( /** The timings when animating a View into an app. */ @JvmField val TIMINGS = - LaunchAnimator.Timings( + TransitionAnimator.Timings( totalDuration = 500L, contentBeforeFadeOutDelay = 0L, contentBeforeFadeOutDuration = 150L, @@ -89,7 +89,7 @@ class ActivityLaunchAnimator( /** The interpolators when animating a View or a dialog into an app. */ val INTERPOLATORS = - LaunchAnimator.Interpolators( + TransitionAnimator.Interpolators( positionInterpolator = Interpolators.EMPHASIZED, positionXInterpolator = Interpolators.EMPHASIZED_COMPLEMENT, contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN, @@ -99,8 +99,9 @@ class ActivityLaunchAnimator( // TODO(b/288507023): Remove this flag. @JvmField val DEBUG_LAUNCH_ANIMATION = Build.IS_DEBUGGABLE - private val DEFAULT_LAUNCH_ANIMATOR = LaunchAnimator(TIMINGS, INTERPOLATORS) - private val DEFAULT_DIALOG_TO_APP_ANIMATOR = LaunchAnimator(DIALOG_TIMINGS, INTERPOLATORS) + private val DEFAULT_TRANSITION_ANIMATOR = TransitionAnimator(TIMINGS, INTERPOLATORS) + private val DEFAULT_DIALOG_TO_APP_ANIMATOR = + TransitionAnimator(DIALOG_TIMINGS, INTERPOLATORS) /** Durations & interpolators for the navigation bar fading in & out. */ private const val ANIMATION_DURATION_NAV_FADE_IN = 266L @@ -154,7 +155,7 @@ class ActivityLaunchAnimator( * Start an intent and animate the opening window. The intent will be started by running * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch * result. [controller] is responsible from animating the view from which the intent was started - * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window + * in [Controller.onTransitionAnimationProgress]. No animation will start if there is no window * opening. * * If [controller] is null or [animate] is false, then the intent will be started and no @@ -255,7 +256,7 @@ class ActivityLaunchAnimator( private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) { if (Looper.myLooper() != Looper.getMainLooper()) { - this.launchContainer.context.mainExecutor.execute { + this.transitionContainer.context.mainExecutor.execute { callOnIntentStartedOnMainThread(willAnimate) } } else { @@ -306,14 +307,14 @@ class ActivityLaunchAnimator( @VisibleForTesting fun createRunner(controller: Controller): Runner { // Make sure we use the modified timings when animating a dialog into an app. - val launchAnimator = + val transitionAnimator = if (controller.isDialogLaunch) { dialogToAppAnimator } else { - launchAnimator + transitionAnimator } - return Runner(controller, callback!!, launchAnimator, lifecycleListener) + return Runner(controller, callback!!, transitionAnimator, lifecycleListener) } interface PendingIntentStarter { @@ -364,7 +365,7 @@ class ActivityLaunchAnimator( * * Note that all callbacks (onXXX methods) are all called on the main thread. */ - interface Controller : LaunchAnimator.Controller { + interface Controller : TransitionAnimator.Controller { companion object { /** * Return a [Controller] that will animate and expand [view] into the opening window. @@ -427,9 +428,9 @@ class ActivityLaunchAnimator( fun onIntentStarted(willAnimate: Boolean) {} /** - * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after - * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called - * before the cancellation. + * The animation was cancelled. Note that [onTransitionAnimationEnd] will still be called + * after this if the animation was already started, i.e. if [onTransitionAnimationStart] was + * called before the cancellation. * * If this launch animation affected the occlusion state of the keyguard, WM will provide us * with [newKeyguardOccludedState] so that we can set the occluded state appropriately. @@ -475,11 +476,11 @@ class ActivityLaunchAnimator( controller: Controller, callback: Callback, /** The animator to use to animate the window launch. */ - launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, + transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR, /** Listener for animation lifecycle events. */ listener: Listener? = null ) : IRemoteAnimationRunner.Stub() { - private val context = controller.launchContainer.context + private val context = controller.transitionContainer.context // This is being passed across IPC boundaries and cycles (through PendingIntentRecords, // etc.) are possible. So we need to make sure we drop any references that might @@ -492,7 +493,7 @@ class ActivityLaunchAnimator( controller, callback, DelegatingAnimationCompletionListener(listener, this::dispose), - launchAnimator, + transitionAnimator, disableWmTimeout ) } @@ -543,7 +544,7 @@ class ActivityLaunchAnimator( /** Listener for animation lifecycle events. */ private val listener: Listener? = null, /** The animator to use to animate the window launch. */ - private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR, + private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR, /** * Whether we should disable the WindowManager timeout. This should be set to true in tests @@ -552,10 +553,10 @@ class ActivityLaunchAnimator( // TODO(b/301385865): Remove this flag. disableWmTimeout: Boolean = false, ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> { - private val launchContainer = controller.launchContainer - private val context = launchContainer.context + private val transitionContainer = controller.transitionContainer + private val context = transitionContainer.context private val transactionApplierView = - controller.openingWindowSyncView ?: controller.launchContainer + controller.openingWindowSyncView ?: controller.transitionContainer private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView) private val timeoutHandler = if (!disableWmTimeout) { @@ -570,7 +571,7 @@ class ActivityLaunchAnimator( private var windowCropF = RectF() private var timedOut = false private var cancelled = false - private var animation: LaunchAnimator.Animation? = null + private var animation: TransitionAnimator.Animation? = null /** * A timeout to cancel the launch animation if the remote animation is not started or @@ -660,7 +661,7 @@ class ActivityLaunchAnimator( nonApps: Array<out RemoteAnimationTarget>?, iCallback: IRemoteAnimationFinishedCallback? ) { - if (LaunchAnimator.DEBUG) { + if (TransitionAnimator.DEBUG) { Log.d(TAG, "Remote animation started") } @@ -687,7 +688,7 @@ class ActivityLaunchAnimator( val windowBounds = window.screenSpaceBounds val endState = - LaunchAnimator.State( + TransitionAnimator.State( top = windowBounds.top, bottom = windowBounds.bottom, left = windowBounds.left, @@ -699,7 +700,7 @@ class ActivityLaunchAnimator( // TODO(b/184121838): We should somehow get the top and bottom radius of the window // instead of recomputing isExpandingFullyAbove here. val isExpandingFullyAbove = - launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState) + transitionAnimator.isExpandingFullyAbove(controller.transitionContainer, endState) val endRadius = if (isExpandingFullyAbove) { // Most of the time, expanding fully above the root view means expanding in full @@ -718,7 +719,7 @@ class ActivityLaunchAnimator( val delegate = this.controller val controller = object : Controller by delegate { - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { listener?.onLaunchAnimationStart() if (DEBUG_LAUNCH_ANIMATION) { @@ -728,10 +729,10 @@ class ActivityLaunchAnimator( "$isExpandingFullyAbove) [controller=$delegate]" ) } - delegate.onLaunchAnimationStart(isExpandingFullyAbove) + delegate.onTransitionAnimationStart(isExpandingFullyAbove) } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { listener?.onLaunchAnimationEnd() iCallback?.invoke() @@ -742,11 +743,11 @@ class ActivityLaunchAnimator( "$isExpandingFullyAbove) [controller=$delegate]" ) } - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) } - override fun onLaunchAnimationProgress( - state: LaunchAnimator.State, + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, progress: Float, linearProgress: Float ) { @@ -758,12 +759,12 @@ class ActivityLaunchAnimator( navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } listener?.onLaunchAnimationProgress(linearProgress) - delegate.onLaunchAnimationProgress(state, progress, linearProgress) + delegate.onTransitionAnimationProgress(state, progress, linearProgress) } } animation = - launchAnimator.startAnimation( + transitionAnimator.startAnimation( controller, endState, windowBackgroundColor, @@ -774,7 +775,7 @@ class ActivityLaunchAnimator( private fun applyStateToWindow( window: RemoteAnimationTarget, - state: LaunchAnimator.State, + state: TransitionAnimator.State, linearProgress: Float, ) { if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) { @@ -825,7 +826,7 @@ class ActivityLaunchAnimator( val alpha = if (controller.isBelowAnimatingWindow) { val windowProgress = - LaunchAnimator.getProgress( + TransitionAnimator.getProgress( TIMINGS, linearProgress, TIMINGS.contentAfterFadeInDelay, @@ -857,7 +858,7 @@ class ActivityLaunchAnimator( private fun applyStateToNavigationBar( navigationBar: RemoteAnimationTarget, - state: LaunchAnimator.State, + state: TransitionAnimator.State, linearProgress: Float ) { if (transactionApplierView.viewRootImpl == null || !navigationBar.leash.isValid) { @@ -868,7 +869,7 @@ class ActivityLaunchAnimator( } val fadeInProgress = - LaunchAnimator.getProgress( + TransitionAnimator.getProgress( TIMINGS, linearProgress, ANIMATION_DELAY_NAV_FADE_IN, @@ -890,7 +891,7 @@ class ActivityLaunchAnimator( .withVisibility(true) } else { val fadeOutProgress = - LaunchAnimator.getProgress( + TransitionAnimator.getProgress( TIMINGS, linearProgress, 0, diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index 168039ed5d3d..9a36960996b0 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -58,7 +58,7 @@ constructor( private val callback: Callback, private val interactionJankMonitor: InteractionJankMonitor, private val featureFlags: AnimationFeatureFlags, - private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS), + private val transitionAnimator: TransitionAnimator = TransitionAnimator(TIMINGS, INTERPOLATORS), private val isForTesting: Boolean = false, ) { private companion object { @@ -108,21 +108,21 @@ constructor( fun stopDrawingInOverlay() /** - * Create the [LaunchAnimator.Controller] that will be called to animate the source + * Create the [TransitionAnimator.Controller] that will be called to animate the source * controlled by this [Controller] during the dialog launch animation. * * At the end of this animation, the source should *not* be visible anymore (until the * dialog is closed and is animated back into the source). */ - fun createLaunchController(): LaunchAnimator.Controller + fun createTransitionController(): TransitionAnimator.Controller /** - * Create the [LaunchAnimator.Controller] that will be called to animate the source + * Create the [TransitionAnimator.Controller] that will be called to animate the source * controlled by this [Controller] during the dialog exit animation. * * At the end of this animation, the source should be visible again. */ - fun createExitController(): LaunchAnimator.Controller + fun createExitController(): TransitionAnimator.Controller /** * Whether we should animate the dialog back into the source when it is dismissed. If this @@ -270,7 +270,7 @@ constructor( val animatedDialog = AnimatedDialog( - launchAnimator = launchAnimator, + transitionAnimator = transitionAnimator, callback = callback, interactionJankMonitor = interactionJankMonitor, controller = controller, @@ -406,8 +406,8 @@ constructor( dialog.dismiss() } - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { - controller.onLaunchAnimationStart(isExpandingFullyAbove) + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + controller.onTransitionAnimationStart(isExpandingFullyAbove) // Make sure the dialog is not dismissed during the animation. disableDialogDismiss() @@ -420,8 +420,8 @@ constructor( dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { - controller.onLaunchAnimationEnd(isExpandingFullyAbove) + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + controller.onTransitionAnimationEnd(isExpandingFullyAbove) // Hide the dialog then dismiss it to instantly dismiss it without playing the // animation. @@ -492,7 +492,7 @@ constructor( data class DialogCuj(@CujType val cujType: Int, val tag: String? = null) private class AnimatedDialog( - private val launchAnimator: LaunchAnimator, + private val transitionAnimator: TransitionAnimator, private val callback: DialogLaunchAnimator.Callback, private val interactionJankMonitor: InteractionJankMonitor, @@ -892,7 +892,7 @@ private class AnimatedDialog( // Create 2 controllers to animate both the dialog and the source. val startController = if (isLaunching) { - controller.createLaunchController() + controller.createTransitionController() } else { GhostedViewLaunchAnimatorController(dialogContentWithBackground!!) } @@ -902,34 +902,34 @@ private class AnimatedDialog( } else { controller.createExitController() } - startController.launchContainer = decorView - endController.launchContainer = decorView + startController.transitionContainer = decorView + endController.transitionContainer = decorView val endState = endController.createAnimatorState() val controller = - object : LaunchAnimator.Controller { - override var launchContainer: ViewGroup - get() = startController.launchContainer + object : TransitionAnimator.Controller { + override var transitionContainer: ViewGroup + get() = startController.transitionContainer set(value) { - startController.launchContainer = value - endController.launchContainer = value + startController.transitionContainer = value + endController.transitionContainer = value } - override fun createAnimatorState(): LaunchAnimator.State { + override fun createAnimatorState(): TransitionAnimator.State { return startController.createAnimatorState() } - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { // During launch, onLaunchAnimationStart will be used to remove the temporary // touch surface ghost so it is important to call this before calling // onLaunchAnimationStart on the controller (which will create its own ghost). onLaunchAnimationStart() - startController.onLaunchAnimationStart(isExpandingFullyAbove) - endController.onLaunchAnimationStart(isExpandingFullyAbove) + startController.onTransitionAnimationStart(isExpandingFullyAbove) + endController.onTransitionAnimationStart(isExpandingFullyAbove) } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { // onLaunchAnimationEnd is called by an Animator at the end of the animation, // on a Choreographer animation tick. The following calls will move the animated // content from the dialog overlay back to its original position, and this @@ -943,23 +943,23 @@ private class AnimatedDialog( // that the move of the content back to its original window will be reflected in // the next frame right after [onLaunchAnimationEnd] is called. dialog.context.mainExecutor.execute { - startController.onLaunchAnimationEnd(isExpandingFullyAbove) - endController.onLaunchAnimationEnd(isExpandingFullyAbove) + startController.onTransitionAnimationEnd(isExpandingFullyAbove) + endController.onTransitionAnimationEnd(isExpandingFullyAbove) onLaunchAnimationEnd() } } - override fun onLaunchAnimationProgress( - state: LaunchAnimator.State, + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, progress: Float, linearProgress: Float ) { - startController.onLaunchAnimationProgress(state, progress, linearProgress) + startController.onTransitionAnimationProgress(state, progress, linearProgress) // The end view is visible only iff the starting view is not visible. state.visible = !state.visible - endController.onLaunchAnimationProgress(state, progress, linearProgress) + endController.onTransitionAnimationProgress(state, progress, linearProgress) // If the dialog content is complex, its dimension might change during the // launch animation. The animation end position might also change during the @@ -973,7 +973,7 @@ private class AnimatedDialog( } } - launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor) + transitionAnimator.startAnimation(controller, endState, originalDialogBackgroundColor) } private fun shouldAnimateDialogIntoSource(): Boolean { diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt index 055252bbef14..03f10f9ac7e3 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt @@ -66,11 +66,11 @@ constructor( ) : ActivityLaunchAnimator.Controller { /** The container to which we will add the ghost view and expanding background. */ - override var launchContainer = ghostedView.rootView as ViewGroup - private val launchContainerOverlay: ViewGroupOverlay - get() = launchContainer.overlay + override var transitionContainer = ghostedView.rootView as ViewGroup + private val transitionContainerOverlay: ViewGroupOverlay + get() = transitionContainer.overlay - private val launchContainerLocation = IntArray(2) + private val transitionContainerLocation = IntArray(2) /** The ghost view that is drawn and animated instead of the ghosted view. */ private var ghostView: GhostView? = null @@ -78,8 +78,8 @@ constructor( private val ghostViewMatrix = Matrix() /** - * The expanding background view that will be added to [launchContainer] (below [ghostView]) and - * animate. + * The expanding background view that will be added to [transitionContainer] (below [ghostView]) + * and animate. */ private var backgroundView: FrameLayout? = null @@ -92,7 +92,7 @@ constructor( private var startBackgroundAlpha: Int = 0xFF private val ghostedViewLocation = IntArray(2) - private val ghostedViewState = LaunchAnimator.State() + private val ghostedViewState = TransitionAnimator.State() /** * The background of the [ghostedView]. This background will be used to draw the background of @@ -175,9 +175,9 @@ constructor( return radius * ghostedView.scaleX } - override fun createAnimatorState(): LaunchAnimator.State { + override fun createAnimatorState(): TransitionAnimator.State { val state = - LaunchAnimator.State( + TransitionAnimator.State( topCornerRadius = getCurrentTopCornerRadius(), bottomCornerRadius = getCurrentBottomCornerRadius() ) @@ -185,7 +185,7 @@ constructor( return state } - fun fillGhostedViewState(state: LaunchAnimator.State) { + fun fillGhostedViewState(state: TransitionAnimator.State) { // For the animation we are interested in the area that has a non transparent background, // so we have to take the optical insets into account. ghostedView.getLocationOnScreen(ghostedViewLocation) @@ -200,7 +200,7 @@ constructor( insets.right } - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { if (ghostedView.parent !is ViewGroup) { // This should usually not happen, but let's make sure we don't crash if the view was // detached right before we started the animation. @@ -209,7 +209,7 @@ constructor( } backgroundView = - FrameLayout(launchContainer.context).also { launchContainerOverlay.add(it) } + FrameLayout(transitionContainer.context).also { transitionContainerOverlay.add(it) } // We wrap the ghosted view background and use it to draw the expandable background. Its // alpha will be set to 0 as soon as we start drawing the expanding background. @@ -225,7 +225,7 @@ constructor( // Create a ghost of the view that will be moving and fading out. This allows to fade out // the content before fading out the background. - ghostView = GhostView.addGhost(ghostedView, launchContainer) + ghostView = GhostView.addGhost(ghostedView, transitionContainer) // [GhostView.addGhost], the result of which is our [ghostView], creates a [GhostView], and // adds it first to a [FrameLayout] container. It then adds _that_ container to an @@ -244,8 +244,8 @@ constructor( cujType?.let { interactionJankMonitor.begin(ghostedView, it) } } - override fun onLaunchAnimationProgress( - state: LaunchAnimator.State, + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, progress: Float, linearProgress: Float ) { @@ -287,15 +287,15 @@ constructor( if (ghostedView.parent is ViewGroup) { // Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted // view is still attached to a ViewGroup, otherwise calculateMatrix will throw. - GhostView.calculateMatrix(ghostedView, launchContainer, ghostViewMatrix) + GhostView.calculateMatrix(ghostedView, transitionContainer, ghostViewMatrix) } - launchContainer.getLocationOnScreen(launchContainerLocation) + transitionContainer.getLocationOnScreen(transitionContainerLocation) ghostViewMatrix.postScale( scale, scale, - ghostedViewState.centerX - launchContainerLocation[0], - ghostedViewState.centerY - launchContainerLocation[1] + ghostedViewState.centerX - transitionContainerLocation[0], + ghostedViewState.centerY - transitionContainerLocation[1] ) ghostViewMatrix.postTranslate( (leftChange + rightChange) / 2f, @@ -310,10 +310,10 @@ constructor( val rightWithInsets = state.right + insets.right val bottomWithInsets = state.bottom + insets.bottom - backgroundView.top = topWithInsets - launchContainerLocation[1] - backgroundView.bottom = bottomWithInsets - launchContainerLocation[1] - backgroundView.left = leftWithInsets - launchContainerLocation[0] - backgroundView.right = rightWithInsets - launchContainerLocation[0] + backgroundView.top = topWithInsets - transitionContainerLocation[1] + backgroundView.bottom = bottomWithInsets - transitionContainerLocation[1] + backgroundView.left = leftWithInsets - transitionContainerLocation[0] + backgroundView.right = rightWithInsets - transitionContainerLocation[0] val backgroundDrawable = backgroundDrawable!! backgroundDrawable.wrapped?.let { @@ -321,7 +321,7 @@ constructor( } } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { if (ghostView == null) { // We didn't actually run the animation. return @@ -332,7 +332,7 @@ constructor( backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha GhostView.removeGhost(ghostedView) - backgroundView?.let { launchContainerOverlay.remove(it) } + backgroundView?.let { transitionContainerOverlay.remove(it) } if (ghostedView is LaunchableView) { // Restore the ghosted view visibility. diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt index d6eba2e7064d..5e4276ce3dd2 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt @@ -31,17 +31,18 @@ import android.view.animation.Interpolator import com.android.app.animation.Interpolators.LINEAR import kotlin.math.roundToInt -private const val TAG = "LaunchAnimator" +private const val TAG = "TransitionAnimator" -/** A base class to animate a window launch (activity or dialog) from a view . */ -class LaunchAnimator(private val timings: Timings, private val interpolators: Interpolators) { +/** A base class to animate a window (activity or dialog) launch to or return from a view . */ +class TransitionAnimator(private val timings: Timings, private val interpolators: Interpolators) { companion object { internal const val DEBUG = false private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) /** - * Given the [linearProgress] of a launch animation, return the linear progress of the - * sub-animation starting [delay] ms after the launch animation and that lasts [duration]. + * Given the [linearProgress] of a transition animation, return the linear progress of the + * sub-animation starting [delay] ms after the transition animation and that lasts + * [duration]. */ @JvmStatic fun getProgress( @@ -58,7 +59,7 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In } } - private val launchContainerLocation = IntArray(2) + private val transitionContainerLocation = IntArray(2) private val cornerRadii = FloatArray(8) /** @@ -73,7 +74,7 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In * * This will be used to: * - Get the associated [Context]. - * - Compute whether we are expanding fully above the launch container. + * - Compute whether we are expanding fully above the transition container. * - Get to overlay to which we initially put the window background layer, until the opening * window is made visible (see [openingWindowSyncView]). * @@ -81,7 +82,7 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In * inside a different location, for instance to ensure correct layering during the * animation. */ - var launchContainer: ViewGroup + var transitionContainer: ViewGroup /** * The [View] with which the opening app window should be synchronized with once it starts @@ -90,7 +91,7 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In * We will also move the window background layer to this view's overlay once the opening * window is visible. * - * If null, this will default to [launchContainer]. + * If null, this will default to [transitionContainer]. */ val openingWindowSyncView: View? get() = null @@ -99,7 +100,7 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In * Return the [State] of the view that will be animated. We will animate from this state to * the final window state. * - * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the + * Note: This state will be mutated and passed to [onTransitionAnimationProgress] during the * animation. */ fun createAnimatorState(): State @@ -107,22 +108,22 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In /** * The animation started. This is typically used to initialize any additional resource * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding - * fully above the [launchContainer]. + * fully above the [transitionContainer]. */ - fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {} + fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {} /** The animation made progress and the expandable view [state] should be updated. */ - fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {} + fun onTransitionAnimationProgress(state: State, progress: Float, linearProgress: Float) {} /** - * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was - * called previously. This is typically used to clean up the resources initialized when the - * animation was started. + * The animation ended. This will be called *if and only if* [onTransitionAnimationStart] + * was called previously. This is typically used to clean up the resources initialized when + * the animation was started. */ - fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {} + fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {} } - /** The state of an expandable view during a [LaunchAnimator] animation. */ + /** The state of an expandable view during a [TransitionAnimator] animation. */ open class State( /** The position of the view in screen space coordinates. */ var top: Int = 0, @@ -198,13 +199,13 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In ) /** - * Start a launch animation controlled by [controller] towards [endState]. An intermediary layer - * with [windowBackgroundColor] will fade in then (optionally) fade out above the expanding - * view, and should be the same background color as the opening (or closing) window. + * Start a transition animation controlled by [controller] towards [endState]. An intermediary + * layer with [windowBackgroundColor] will fade in then (optionally) fade out above the + * expanding view, and should be the same background color as the opening (or closing) window. * * If [fadeOutWindowBackgroundLayer] is true, then this intermediary layer will fade out during * the second half of the animation, and will have SRC blending mode (ultimately punching a hole - * in the [launch container][Controller.launchContainer]) iff [drawHole] is true. + * in the [transition container][Controller.transitionContainer]) iff [drawHole] is true. */ fun startAnimation( controller: Controller, @@ -251,13 +252,13 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In } } - val launchContainer = controller.launchContainer - val isExpandingFullyAbove = isExpandingFullyAbove(launchContainer, endState) + val transitionContainer = controller.transitionContainer + val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState) // We add an extra layer with the same color as the dialog/app splash screen background // color, which is usually the same color of the app background. We first fade in this layer // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the - // launch container and reveal the opening window. + // transition container and reveal the opening window. val windowBackgroundLayer = GradientDrawable().apply { setColor(windowBackgroundColor) @@ -275,9 +276,9 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay val moveBackgroundLayerWhenAppIsVisible = openingWindowSyncView != null && - openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl + openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl - val launchContainerOverlay = launchContainer.overlay + val transitionContainerOverlay = transitionContainer.overlay var cancelled = false var movedBackgroundLayer = false @@ -287,20 +288,20 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In if (DEBUG) { Log.d(TAG, "Animation started") } - controller.onLaunchAnimationStart(isExpandingFullyAbove) + controller.onTransitionAnimationStart(isExpandingFullyAbove) - // Add the drawable to the launch container overlay. Overlays always draw + // Add the drawable to the transition container overlay. Overlays always draw // drawables after views, so we know that it will be drawn above any view added // by the controller. - launchContainerOverlay.add(windowBackgroundLayer) + transitionContainerOverlay.add(windowBackgroundLayer) } override fun onAnimationEnd(animation: Animator) { if (DEBUG) { Log.d(TAG, "Animation ended") } - controller.onLaunchAnimationEnd(isExpandingFullyAbove) - launchContainerOverlay.remove(windowBackgroundLayer) + controller.onTransitionAnimationEnd(isExpandingFullyAbove) + transitionContainerOverlay.remove(windowBackgroundLayer) if (moveBackgroundLayerWhenAppIsVisible) { openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) @@ -353,17 +354,21 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In // in its new container. movedBackgroundLayer = true - launchContainerOverlay.remove(windowBackgroundLayer) + transitionContainerOverlay.remove(windowBackgroundLayer) openingWindowSyncViewOverlay!!.add(windowBackgroundLayer) - ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {}) + ViewRootSync.synchronizeNextDraw( + transitionContainer, + openingWindowSyncView, + then = {} + ) } val container = if (movedBackgroundLayer) { openingWindowSyncView!! } else { - controller.launchContainer + controller.transitionContainer } applyStateToWindowBackgroundLayer( @@ -374,7 +379,7 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In fadeOutWindowBackgroundLayer, drawHole ) - controller.onLaunchAnimationProgress(state, progress, linearProgress) + controller.onTransitionAnimationProgress(state, progress, linearProgress) } animator.start() @@ -386,30 +391,30 @@ class LaunchAnimator(private val timings: Timings, private val interpolators: In } } - /** Return whether we are expanding fully above the [launchContainer]. */ - internal fun isExpandingFullyAbove(launchContainer: View, endState: State): Boolean { - launchContainer.getLocationOnScreen(launchContainerLocation) - return endState.top <= launchContainerLocation[1] && - endState.bottom >= launchContainerLocation[1] + launchContainer.height && - endState.left <= launchContainerLocation[0] && - endState.right >= launchContainerLocation[0] + launchContainer.width + /** Return whether we are expanding fully above the [transitionContainer]. */ + internal fun isExpandingFullyAbove(transitionContainer: View, endState: State): Boolean { + transitionContainer.getLocationOnScreen(transitionContainerLocation) + return endState.top <= transitionContainerLocation[1] && + endState.bottom >= transitionContainerLocation[1] + transitionContainer.height && + endState.left <= transitionContainerLocation[0] && + endState.right >= transitionContainerLocation[0] + transitionContainer.width } private fun applyStateToWindowBackgroundLayer( drawable: GradientDrawable, state: State, linearProgress: Float, - launchContainer: View, + transitionContainer: View, fadeOutWindowBackgroundLayer: Boolean, drawHole: Boolean ) { // Update position. - launchContainer.getLocationOnScreen(launchContainerLocation) + transitionContainer.getLocationOnScreen(transitionContainerLocation) drawable.setBounds( - state.left - launchContainerLocation[0], - state.top - launchContainerLocation[1], - state.right - launchContainerLocation[0], - state.bottom - launchContainerLocation[1] + state.left - transitionContainerLocation[0], + state.top - transitionContainerLocation[1], + state.right - transitionContainerLocation[0], + state.bottom - transitionContainerLocation[1] ) // Update radius. diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt index 1290f0097536..e2a29ab41f61 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt @@ -68,19 +68,19 @@ internal constructor( } } - override fun createLaunchController(): LaunchAnimator.Controller { + override fun createTransitionController(): TransitionAnimator.Controller { val delegate = GhostedViewLaunchAnimatorController(source) - return object : LaunchAnimator.Controller by delegate { - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + return object : TransitionAnimator.Controller by delegate { + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another // ghost (that ghosts only the source content, and not its background) will // be added right after this by the delegate and will be animated. GhostView.removeGhost(source) - delegate.onLaunchAnimationStart(isExpandingFullyAbove) + delegate.onTransitionAnimationStart(isExpandingFullyAbove) } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) // At this point the view visibility is restored by the delegate, so we delay the // visibility changes again and make it invisible while the dialog is shown. @@ -94,7 +94,7 @@ internal constructor( } } - override fun createExitController(): LaunchAnimator.Controller { + override fun createExitController(): TransitionAnimator.Controller { return GhostedViewLaunchAnimatorController(source) } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt index ac1ef1509415..8eb2f2e3bf2a 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt @@ -75,7 +75,7 @@ import androidx.lifecycle.findViewTreeViewModelStoreOwner import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner import com.android.systemui.animation.Expandable -import com.android.systemui.animation.LaunchAnimator +import com.android.systemui.animation.TransitionAnimator import kotlin.math.max import kotlin.math.min @@ -301,7 +301,7 @@ fun Expandable( private fun AnimatedContentInOverlay( color: Color, sizeInOriginalLayout: Size, - animatorState: State<LaunchAnimator.State?>, + animatorState: State<TransitionAnimator.State?>, overlay: ViewGroupOverlay, controller: ExpandableControllerImpl, content: @Composable (Expandable) -> Unit, @@ -407,7 +407,7 @@ private fun AnimatedContentInOverlay( internal fun measureAndLayoutComposeViewInOverlay( view: View, - state: LaunchAnimator.State, + state: TransitionAnimator.State, ) { val exactWidth = state.width val exactHeight = state.height @@ -449,7 +449,7 @@ private fun Modifier.border(controller: ExpandableControllerImpl): Modifier { } private fun ContentDrawScope.drawBackground( - animatorState: LaunchAnimator.State, + animatorState: TransitionAnimator.State, color: Color, border: BorderStroke?, ) { diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt index 0e7694e7ef46..1020263ee7f6 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt @@ -44,7 +44,7 @@ import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.animation.Expandable -import com.android.systemui.animation.LaunchAnimator +import com.android.systemui.animation.TransitionAnimator import kotlin.math.roundToInt /** A controller that can control animated launches from an [Expandable]. */ @@ -70,7 +70,7 @@ fun rememberExpandableController( val layoutDirection = LocalLayoutDirection.current // The current animation state, if we are currently animating a dialog or activity. - val animatorState = remember { mutableStateOf<LaunchAnimator.State?>(null) } + val animatorState = remember { mutableStateOf<TransitionAnimator.State?>(null) } // Whether a dialog controlled by this ExpandableController is currently showing. val isDialogShowing = remember { mutableStateOf(false) } @@ -123,7 +123,7 @@ internal class ExpandableControllerImpl( internal val borderStroke: BorderStroke?, internal val composeViewRoot: View, internal val density: Density, - internal val animatorState: MutableState<LaunchAnimator.State?>, + internal val animatorState: MutableState<TransitionAnimator.State?>, internal val isDialogShowing: MutableState<Boolean>, internal val overlay: MutableState<ViewGroupOverlay?>, internal val currentComposeViewInOverlay: MutableState<View?>, @@ -153,32 +153,32 @@ internal class ExpandableControllerImpl( } /** - * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog - * animation. This controller will: + * Create a [TransitionAnimator.Controller] that is going to be used to drive an activity or + * dialog animation. This controller will: * 1. Compute the start/end animation state using [boundsInComposeViewRoot] and the location of * composeViewRoot on the screen. * 2. Update [animatorState] with the current animation state if we are animating, or null * otherwise. */ - private fun launchController(): LaunchAnimator.Controller { - return object : LaunchAnimator.Controller { + private fun transitionController(): TransitionAnimator.Controller { + return object : TransitionAnimator.Controller { private val rootLocationOnScreen = intArrayOf(0, 0) - override var launchContainer: ViewGroup = composeViewRoot.rootView as ViewGroup + override var transitionContainer: ViewGroup = composeViewRoot.rootView as ViewGroup - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { animatorState.value = null } - override fun onLaunchAnimationProgress( - state: LaunchAnimator.State, + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, progress: Float, linearProgress: Float ) { // We copy state given that it's always the same object that is mutated by // ActivityLaunchAnimator. animatorState.value = - LaunchAnimator.State( + TransitionAnimator.State( state.top, state.bottom, state.left, @@ -195,7 +195,7 @@ internal class ExpandableControllerImpl( } } - override fun createAnimatorState(): LaunchAnimator.State { + override fun createAnimatorState(): TransitionAnimator.State { val boundsInRoot = boundsInComposeViewRoot.value val outline = shape.createOutline( @@ -236,7 +236,7 @@ internal class ExpandableControllerImpl( } val rootLocation = rootLocationOnScreen() - return LaunchAnimator.State( + return TransitionAnimator.State( top = rootLocation.y.roundToInt(), bottom = (rootLocation.y + boundsInRoot.height).roundToInt(), left = rootLocation.x.roundToInt(), @@ -258,17 +258,18 @@ internal class ExpandableControllerImpl( /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */ private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller { - val delegate = launchController() - return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate { - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { - delegate.onLaunchAnimationStart(isExpandingFullyAbove) + val delegate = transitionController() + return object : + ActivityLaunchAnimator.Controller, TransitionAnimator.Controller by delegate { + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationStart(isExpandingFullyAbove) overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay cujType?.let { InteractionJankMonitor.getInstance().begin(composeViewRoot, it) } } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { cujType?.let { InteractionJankMonitor.getInstance().end(it) } - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) overlay.value = null } } @@ -293,11 +294,11 @@ internal class ExpandableControllerImpl( } } - override fun createLaunchController(): LaunchAnimator.Controller { - val delegate = launchController() - return object : LaunchAnimator.Controller by delegate { - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + override fun createTransitionController(): TransitionAnimator.Controller { + val delegate = transitionController() + return object : TransitionAnimator.Controller by delegate { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) // Make sure we don't draw this expandable when the dialog is showing. isDialogShowing.value = true @@ -305,11 +306,11 @@ internal class ExpandableControllerImpl( } } - override fun createExitController(): LaunchAnimator.Controller { - val delegate = launchController() - return object : LaunchAnimator.Controller by delegate { - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + override fun createExitController(): TransitionAnimator.Controller { + val delegate = transitionController() + return object : TransitionAnimator.Controller by delegate { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) isDialogShowing.value = false } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Color.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Color.kt new file mode 100644 index 000000000000..64b9f2df144b --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Color.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.ui.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.graphics.Color +import com.android.compose.theme.colorAttr + +/** Resolves [com.android.systemui.common.shared.model.Color] into [Color] */ +@Composable +@ReadOnlyComposable +fun com.android.systemui.common.shared.model.Color.toColor(): Color { + return when (this) { + is com.android.systemui.common.shared.model.Color.Attribute -> colorAttr(attribute) + is com.android.systemui.common.shared.model.Color.Loaded -> Color(color) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index ff53ff256931..378a1e4858e8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -34,6 +34,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -43,23 +44,21 @@ class LockscreenScene @Inject constructor( @Application private val applicationScope: CoroutineScope, - private val viewModel: LockscreenSceneViewModel, + viewModel: LockscreenSceneViewModel, private val lockscreenContent: Lazy<LockscreenContent>, ) : ComposableScene { override val key = SceneKey.Lockscreen override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = - viewModel.upDestinationSceneKey - .map { pageKey -> - destinationScenes(up = pageKey, left = viewModel.leftDestinationSceneKey) - } + combine(viewModel.upDestinationSceneKey, viewModel.leftDestinationSceneKey, ::Pair) + .map { (upKey, leftKey) -> destinationScenes(up = upKey, left = leftKey) } .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, initialValue = destinationScenes( up = viewModel.upDestinationSceneKey.value, - left = viewModel.leftDestinationSceneKey, + left = viewModel.leftDestinationSceneKey.value, ) ) 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 d70f82fe8ab7..ef6ae2ecfec9 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 @@ -19,6 +19,7 @@ package com.android.systemui.notifications.ui.composable import android.util.Log import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -140,6 +141,8 @@ fun SceneScope.NotificationScrollingStack( ) { val density = LocalDensity.current val screenCornerRadius = LocalScreenCornerRadius.current + val scrollState = rememberScrollState() + val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f) val expansionFraction by viewModel.expandFraction.collectAsState(0f) val navBarHeight = @@ -180,11 +183,28 @@ fun SceneScope.NotificationScrollingStack( // if contentHeight drops below minimum visible scrim height while scrim is // expanded, reset scrim offset. - LaunchedEffect(contentHeight, screenHeight, maxScrimTop, scrimOffset) { + LaunchedEffect(contentHeight, scrimOffset) { snapshotFlow { contentHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f } .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f } } + // if we receive scroll delta from NSSL, offset the scrim and placeholder accordingly. + LaunchedEffect(syntheticScroll, scrimOffset, scrollState) { + snapshotFlow { syntheticScroll.value } + .collect { delta -> + val minOffset = minScrimOffset() + if (scrimOffset.value > minOffset) { + val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f) + scrimOffset.value = (scrimOffset.value - delta).coerceAtLeast(minOffset) + if (remainingDelta > 0f) { + scrollState.scrollBy(remainingDelta) + } + } else { + scrollState.scrollTo(delta.roundToInt()) + } + } + } + Box( modifier = modifier @@ -260,7 +280,7 @@ fun SceneScope.NotificationScrollingStack( ) } ) - .verticalScroll(rememberScrollState()) + .verticalScroll(scrollState) .fillMaxWidth() .height { (contentHeight.value + navBarHeight).roundToInt() }, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt index c027c499c0b7..de8f2ec6e941 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt @@ -18,21 +18,21 @@ package com.android.systemui.qs.ui.composable import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.MovableElementScenePicker import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.TransitionState +import com.android.compose.modifiers.thenIf import com.android.compose.theme.colorAttr import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing @@ -44,9 +44,16 @@ import com.android.systemui.scene.ui.composable.QuickSettings as QuickSettingsSc import com.android.systemui.scene.ui.composable.Shade object QuickSettings { + private val SCENES = + setOf( + QuickSettingsSceneKey, + Shade, + ) + object Elements { // TODO RENAME - val Content = ElementKey("QuickSettingsContent") + val Content = + ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES)) val CollapsedGrid = ElementKey("QuickSettingsCollapsedGrid") val FooterActions = ElementKey("QuickSettingsFooterActions") } @@ -86,14 +93,22 @@ private fun SceneScope.stateForQuickSettingsContent(): QSSceneAdapter.State { */ @Composable fun SceneScope.QuickSettings( - modifier: Modifier = Modifier, qsSceneAdapter: QSSceneAdapter, + heightProvider: () -> Int, + modifier: Modifier = Modifier, ) { val contentState = stateForQuickSettingsContent() MovableElement( key = QuickSettings.Elements.Content, - modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp) + modifier = + modifier.fillMaxWidth().layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + // Use the height of the correct view based on the scene it is being composed in + val height = heightProvider() + + layout(placeable.width, height) { placeable.placeRelative(0, 0) } + } ) { content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) } } @@ -118,15 +133,7 @@ private fun QuickSettingsContent( qsView?.let { view -> Box( modifier = - modifier - .fillMaxWidth() - .then( - if (isCustomizing) { - Modifier.fillMaxHeight() - } else { - Modifier.wrapContentHeight() - } - ) + modifier.fillMaxWidth().thenIf(isCustomizing) { Modifier.fillMaxHeight() } ) { AndroidView( modifier = Modifier.fillMaxWidth().background(colorAttr(R.attr.underSurface)), 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 969dec31971c..1cbc99298655 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 @@ -213,8 +213,9 @@ private fun SceneScope.QuickSettingsScene( Spacer(modifier = Modifier.height(16.dp)) // This view has its own horizontal padding QuickSettings( - modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"), viewModel.qsSceneAdapter, + { viewModel.qsSceneAdapter.qsHeight }, + modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 9f9e1f5bb56a..da1b417ae190 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -40,6 +40,7 @@ import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction as SceneTransitionUserAction +import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.observableTransitionState import com.android.compose.animation.scene.updateSceneTransitionLayoutState import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon @@ -168,7 +169,7 @@ private fun SceneTransitionObservableTransitionState.toModel(): ObservableTransi private fun toTransitionModels( userAction: UserAction, sceneModel: SceneModel, -): Pair<SceneTransitionUserAction, SceneTransitionSceneKey> { +): Pair<SceneTransitionUserAction, UserActionResult> { return userAction.toTransitionUserAction() to sceneModel.key.toTransitionSceneKey() } 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 677df7ead2da..cac35cb9369f 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 @@ -189,8 +189,8 @@ private fun SceneScope.ShadeScene( ) ) QuickSettings( - modifier = Modifier.height(130.dp), viewModel.qsSceneAdapter, + { viewModel.qsSceneAdapter.qqsHeight }, ) if (viewModel.isMediaVisible()) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt index 2596d4a34b6f..97703992cbf6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt @@ -44,7 +44,7 @@ sealed class Key(val debugName: String, val identity: Any) { class SceneKey( debugName: String, identity: Any = Object(), -) : Key(debugName, identity), UserActionResult { +) : Key(debugName, identity) { @VisibleForTesting // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can // access internal members. @@ -53,11 +53,6 @@ class SceneKey( /** The unique [ElementKey] identifying this scene's root element. */ val rootElementKey = ElementKey(debugName, identity) - // Implementation of [UserActionResult]. - override val toScene: SceneKey = this - override val transitionKey: TransitionKey? = null - override val distance: UserActionDistance? = null - override fun toString(): String { return "SceneKey(debugName=$debugName)" } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index d904c8b770bf..e1f8a0959f6f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -332,7 +332,11 @@ interface ElementBoxScope { @Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope /** An action performed by the user. */ -sealed interface UserAction +sealed interface UserAction { + infix fun to(scene: SceneKey): Pair<UserAction, UserActionResult> { + return this to UserActionResult(toScene = scene) + } +} /** The user navigated back, either using a gesture or by triggering a KEYCODE_BACK event. */ data object Back : UserAction @@ -385,65 +389,26 @@ interface SwipeSourceDetector { ): SwipeSource? } -/** - * The result of performing a [UserAction]. - * - * Note: [UserActionResult] is implemented by [SceneKey], so you can also use scene keys directly - * when defining your [UserActionResult]s. - * - * ``` - * SceneTransitionLayout(...) { - * scene( - * Scenes.Foo, - * userActions = - * mapOf( - * Swipe.Right to Scene.Bar, - * Swipe.Down to Scene.Doe, - * ) - * ) - * ) { ... } - * } - * ``` - */ -interface UserActionResult { +/** The result of performing a [UserAction]. */ +class UserActionResult( /** The scene we should be transitioning to during the [UserAction]. */ - val toScene: SceneKey - - /** The key of the transition that should be used. */ - val transitionKey: TransitionKey? + val toScene: SceneKey, /** * The distance the action takes to animate from 0% to 100%. * * If `null`, a default distance will be used that depends on the [UserAction] performed. */ - val distance: UserActionDistance? -} - -/** Create a [UserActionResult] to [toScene] with the given [distance] and [transitionKey]. */ -fun UserActionResult( - toScene: SceneKey, - distance: UserActionDistance? = null, - transitionKey: TransitionKey? = null, -): UserActionResult { - return object : UserActionResult { - override val toScene: SceneKey = toScene - override val transitionKey: TransitionKey? = transitionKey - override val distance: UserActionDistance? = distance - } -} + val distance: UserActionDistance? = null, -/** Create a [UserActionResult] to [toScene] with the given fixed [distance] and [transitionKey]. */ -fun UserActionResult( - toScene: SceneKey, - distance: Dp, - transitionKey: TransitionKey? = null, -): UserActionResult { - return UserActionResult( - toScene = toScene, - distance = FixedDistance(distance), - transitionKey = transitionKey, - ) + /** The key of the transition that should be used. */ + val transitionKey: TransitionKey? = null, +) { + constructor( + toScene: SceneKey, + distance: Dp, + transitionKey: TransitionKey? = null, + ) : this(toScene, FixedDistance(distance), transitionKey) } interface UserActionDistance { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt index 2dc94a405ae3..dacbdb484d0c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt @@ -54,12 +54,8 @@ class SceneGestureHandlerTest { private val layoutState = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions) - val mutableUserActionsA: MutableMap<UserAction, SceneKey> = - mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC) - - val mutableUserActionsB: MutableMap<UserAction, SceneKey> = - mutableMapOf(Swipe.Up to SceneC, Swipe.Down to SceneA) - + val mutableUserActionsA = mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC) + val mutableUserActionsB = mutableMapOf(Swipe.Up to SceneC, Swipe.Down to SceneA) private val scenesBuilder: SceneTransitionLayoutScope.() -> Unit = { scene( key = SceneA, @@ -507,7 +503,7 @@ class SceneGestureHandlerTest { onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) - mutableUserActionsA[Swipe.Up] = SceneC + mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC) onDelta(pixels = up(fractionOfScreen = 0.1f)) // target stays B even though UserActions changed assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f) @@ -524,7 +520,7 @@ class SceneGestureHandlerTest { onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) - mutableUserActionsA[Swipe.Up] = SceneC + mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC) onDelta(pixels = up(fractionOfScreen = 0.1f)) onDragStopped(velocity = down(fractionOfScreen = 0.1f)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index c4bcb536de78..f86342c0c20e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -31,6 +31,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingCollectorFake; @@ -102,13 +103,15 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { .thenReturn(mOkButton); when(mPinBasedInputView.getResources()).thenReturn(getContext().getResources()); + KeyguardKeyboardInteractor keyguardKeyboardInteractor = + new KeyguardKeyboardInteractor(new FakeKeyboardRepository()); FakeFeatureFlags featureFlags = new FakeFeatureFlags(); mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_REVAMPED_BOUNCER_MESSAGES); mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView, mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener, mEmergencyButtonController, mFalsingCollector, featureFlags, - mSelectedUserInteractor, new FakeKeyboardRepository()) { + mSelectedUserInteractor, keyguardKeyboardInteractor) { @Override public void onResume(int reason) { super.onResume(reason); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt index bd9ca3035a07..b4e2eab22c88 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt @@ -16,26 +16,18 @@ package com.android.systemui.communal.data.repository -import android.content.pm.UserInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.Flags -import com.android.systemui.flags.fakeFeatureFlagsClassic -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.sceneContainerRepository import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.testKosmos -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.data.repository.fakeUserRepository -import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -48,37 +40,20 @@ import org.junit.runner.RunWith class CommunalRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: CommunalRepositoryImpl - private lateinit var secureSettings: FakeSettings - private lateinit var userRepository: FakeUserRepository - private val kosmos = testKosmos() private val testScope = kosmos.testScope private val sceneContainerRepository = kosmos.sceneContainerRepository @Before fun setUp() { - secureSettings = FakeSettings() - userRepository = kosmos.fakeUserRepository - - val listOfUserInfo = listOf(MAIN_USER_INFO) - userRepository.setUserInfos(listOfUserInfo) - - kosmos.fakeFeatureFlagsClassic.apply { set(Flags.COMMUNAL_SERVICE_ENABLED, true) } - mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) - underTest = createRepositoryImpl(false) } private fun createRepositoryImpl(sceneContainerEnabled: Boolean): CommunalRepositoryImpl { return CommunalRepositoryImpl( testScope.backgroundScope, - testScope.backgroundScope, - kosmos.testDispatcher, - kosmos.fakeFeatureFlagsClassic, kosmos.fakeSceneContainerFlags.apply { enabled = sceneContainerEnabled }, sceneContainerRepository, - kosmos.fakeUserRepository, - secureSettings, ) } @@ -159,29 +134,4 @@ class CommunalRepositoryImplTest : SysuiTestCase() { assertThat(transitionState) .isEqualTo(ObservableCommunalTransitionState.Idle(CommunalSceneKey.DEFAULT)) } - - @Test - fun communalEnabledState_false_whenGlanceableHubSettingFalse() = - testScope.runTest { - userRepository.setSelectedUserInfo(MAIN_USER_INFO) - secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, MAIN_USER_INFO.id) - - val communalEnabled by collectLastValue(underTest.communalEnabledState) - assertThat(communalEnabled).isFalse() - } - - @Test - fun communalEnabledState_true_whenGlanceableHubSettingTrue() = - testScope.runTest { - userRepository.setSelectedUserInfo(MAIN_USER_INFO) - secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, MAIN_USER_INFO.id) - - val communalEnabled by collectLastValue(underTest.communalEnabledState) - assertThat(communalEnabled).isTrue() - } - - companion object { - private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled" - private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt new file mode 100644 index 000000000000..0aca16d9aeaa --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE +import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL +import android.app.admin.devicePolicyManager +import android.content.Intent +import android.content.pm.UserInfo +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.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.communal.data.model.DisabledReason +import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_ENABLED +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.fakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalSettingsRepositoryImplTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private lateinit var underTest: CommunalSettingsRepository + + @Before + fun setUp() { + kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) + setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_FEATURES_NONE) + setKeyguardFeaturesDisabled(SECONDARY_USER, KEYGUARD_DISABLE_FEATURES_NONE) + underTest = kosmos.communalSettingsRepository + } + + @EnableFlags(FLAG_COMMUNAL_HUB) + @Test + fun secondaryUserIsInvalid() = + testScope.runTest { + val enabledState by collectLastValue(underTest.getEnabledState(SECONDARY_USER)) + + assertThat(enabledState?.enabled).isFalse() + assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_INVALID_USER) + } + + @EnableFlags(FLAG_COMMUNAL_HUB) + @Test + fun classicFlagIsDisabled() = + testScope.runTest { + kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) + val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) + assertThat(enabledState?.enabled).isFalse() + assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG) + } + + @DisableFlags(FLAG_COMMUNAL_HUB) + @Test + fun communalHubFlagIsDisabled() = + testScope.runTest { + val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) + assertThat(enabledState?.enabled).isFalse() + assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG) + } + + @EnableFlags(FLAG_COMMUNAL_HUB) + @Test + fun hubIsDisabledByUser() = + testScope.runTest { + kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id) + val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) + assertThat(enabledState?.enabled).isFalse() + assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_USER_SETTING) + + kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, SECONDARY_USER.id) + assertThat(enabledState?.enabled).isFalse() + + kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, PRIMARY_USER.id) + assertThat(enabledState?.enabled).isTrue() + } + + @EnableFlags(FLAG_COMMUNAL_HUB) + @Test + fun hubIsDisabledByDevicePolicy() = + testScope.runTest { + val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) + assertThat(enabledState?.enabled).isTrue() + + setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL) + assertThat(enabledState?.enabled).isFalse() + assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_DEVICE_POLICY) + } + + @EnableFlags(FLAG_COMMUNAL_HUB) + @Test + fun hubIsDisabledByUserAndDevicePolicy() = + testScope.runTest { + val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) + assertThat(enabledState?.enabled).isTrue() + + kosmos.fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id) + setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL) + + assertThat(enabledState?.enabled).isFalse() + assertThat(enabledState) + .containsExactly( + DisabledReason.DISABLED_REASON_DEVICE_POLICY, + DisabledReason.DISABLED_REASON_USER_SETTING, + ) + } + + private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { + whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) + .thenReturn(disabledFlags) + kosmos.broadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), + ) + } + + private companion object { + val PRIMARY_USER = + UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN) + val SECONDARY_USER = UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt index 6a3fc2a060eb..824733bcc47b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.communal.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository @@ -59,7 +60,7 @@ class CommunalInteractorCommunalDisabledTest : SysuiTestCase() { widgetRepository = kosmos.fakeCommunalWidgetRepository keyguardRepository = kosmos.fakeKeyguardRepository - communalRepository.setIsCommunalEnabled(false) + mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB) underTest = kosmos.communalInteractor } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index c5485c5c5c33..3ac19e4672c3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -23,6 +23,7 @@ import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository @@ -41,6 +42,8 @@ import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.testScope @@ -109,12 +112,19 @@ class CommunalInteractorTest : SysuiTestCase() { whenever(secondaryUser.isMain).thenReturn(false) userRepository.setUserInfos(listOf(mainUser, secondaryUser)) + kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) + mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) + underTest = kosmos.communalInteractor } @Test fun communalEnabled_true() = - testScope.runTest { assertThat(underTest.isCommunalEnabled).isTrue() } + testScope.runTest { + userRepository.setSelectedUserInfo(mainUser) + runCurrent() + assertThat(underTest.isCommunalEnabled).isTrue() + } @Test fun isCommunalAvailable_storageUnlockedAndMainUser_true() = @@ -125,7 +135,6 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) keyguardRepository.setKeyguardShowing(true) - communalRepository.setCommunalEnabledState(true) assertThat(isAvailable).isTrue() } @@ -139,7 +148,6 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(true) userRepository.setSelectedUserInfo(mainUser) keyguardRepository.setKeyguardShowing(true) - communalRepository.setCommunalEnabledState(true) assertThat(isAvailable).isFalse() } @@ -153,7 +161,6 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(secondaryUser) keyguardRepository.setKeyguardShowing(true) - communalRepository.setCommunalEnabledState(true) assertThat(isAvailable).isFalse() } @@ -167,7 +174,6 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) keyguardRepository.setDreaming(true) - communalRepository.setCommunalEnabledState(true) assertThat(isAvailable).isTrue() } @@ -175,13 +181,14 @@ class CommunalInteractorTest : SysuiTestCase() { @Test fun isCommunalAvailable_communalDisabled_false() = testScope.runTest { + mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB) + val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) keyguardRepository.setKeyguardShowing(true) - communalRepository.setCommunalEnabledState(false) assertThat(isAvailable).isFalse() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index 6c87e0f2eb23..ceb7fac1046f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -22,12 +22,15 @@ import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_STARTED import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.fakeCommunalRepository import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.testScope @@ -35,11 +38,13 @@ import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class CommunalTutorialInteractorTest : SysuiTestCase() { @@ -62,6 +67,8 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { userRepository = kosmos.fakeUserRepository userRepository.setUserInfos(listOf(MAIN_USER_INFO)) + kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) + mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) underTest = kosmos.communalTutorialInteractor } @@ -127,6 +134,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { testScope.runTest { val tutorialSettingState by collectLastValue(communalTutorialRepository.tutorialSettingState) + userRepository.setSelectedUserInfo(MAIN_USER_INFO) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) communalRepository.setIsCommunalHubShowing(true) @@ -139,6 +147,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { testScope.runTest { val tutorialSettingState by collectLastValue(communalTutorialRepository.tutorialSettingState) + userRepository.setSelectedUserInfo(MAIN_USER_INFO) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) communalRepository.setIsCommunalHubShowing(true) @@ -151,6 +160,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { testScope.runTest { val tutorialSettingState by collectLastValue(communalTutorialRepository.tutorialSettingState) + userRepository.setSelectedUserInfo(MAIN_USER_INFO) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) communalRepository.setIsCommunalHubShowing(true) @@ -163,6 +173,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { testScope.runTest { val tutorialSettingState by collectLastValue(communalTutorialRepository.tutorialSettingState) + userRepository.setSelectedUserInfo(MAIN_USER_INFO) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) communalRepository.setIsCommunalHubShowing(false) @@ -175,6 +186,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { testScope.runTest { val tutorialSettingState by collectLastValue(communalTutorialRepository.tutorialSettingState) + userRepository.setSelectedUserInfo(MAIN_USER_INFO) communalRepository.setIsCommunalHubShowing(true) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) @@ -188,6 +200,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { testScope.runTest { val tutorialSettingState by collectLastValue(communalTutorialRepository.tutorialSettingState) + userRepository.setSelectedUserInfo(MAIN_USER_INFO) communalRepository.setIsCommunalHubShowing(true) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) @@ -198,14 +211,11 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { private suspend fun setCommunalAvailable(available: Boolean) { if (available) { - communalRepository.setIsCommunalEnabled(true) - communalRepository.setCommunalEnabledState(true) keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(MAIN_USER_INFO) keyguardRepository.setKeyguardShowing(true) } else { - communalRepository.setIsCommunalEnabled(false) - communalRepository.setCommunalEnabledState(false) + keyguardRepository.setIsEncryptedOrLockdown(true) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 73d309118548..f70b6a5a170f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -22,12 +22,12 @@ import android.provider.Settings import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository -import com.android.systemui.communal.data.repository.fakeCommunalRepository import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.communalInteractor @@ -37,6 +37,8 @@ import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.testScope @@ -92,7 +94,8 @@ class CommunalViewModelTest : SysuiTestCase() { mediaRepository = kosmos.fakeCommunalMediaRepository userRepository = kosmos.fakeUserRepository - kosmos.fakeCommunalRepository.setCommunalEnabledState(true) + kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) + mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) underTest = CommunalViewModel( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt index 032d76f0dceb..8488843905f7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -19,12 +19,15 @@ package com.android.systemui.communal.widgets import android.content.pm.UserInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_ENABLED import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher @@ -33,6 +36,7 @@ import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow @@ -62,6 +66,8 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) + kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) + mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) appWidgetIdToRemove = MutableSharedFlow() whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove) @@ -169,7 +175,8 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { fakeKeyguardRepository.setIsEncryptedOrLockdown(false) fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) fakeKeyguardRepository.setKeyguardShowing(true) - fakeCommunalRepository.setCommunalEnabledState(available) + val settingsValue = if (available) 1 else 0 + fakeSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, settingsValue, MAIN_USER_INFO.id) } private companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt index ea766f8ea9bb..805b4a828bda 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt @@ -26,7 +26,6 @@ import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -75,7 +74,7 @@ class SeekableSliderHapticPluginTest : SysuiTestCase() { fun start_afterStop_startsTheTrackingAgain() = runOnStartedPlugin { // WHEN the plugin is restarted plugin.stop() - plugin.start() + plugin.startInScope(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // THEN the tracking begins again assertThat(plugin.isTracking).isTrue() @@ -131,22 +130,21 @@ class SeekableSliderHapticPluginTest : SysuiTestCase() { private fun runOnStartedPlugin(test: suspend TestScope.() -> Unit) = with(kosmos) { testScope.runTest { - createPlugin(this, UnconfinedTestDispatcher(testScheduler)) - // GIVEN that the plugin is started - plugin.start() + val pluginScope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)) + createPlugin() + // GIVEN that the plugin is started in a test scope + plugin.startInScope(pluginScope) // THEN run the test test() } } - private fun createPlugin(scope: CoroutineScope, dispatcher: CoroutineDispatcher) { + private fun createPlugin() { plugin = SeekableSliderHapticPlugin( vibratorHelper, kosmos.fakeSystemClock, - dispatcher, - scope, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt index 0543bc257440..d52696a0bd87 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt @@ -20,6 +20,7 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -68,6 +69,8 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Before fun setUp() { + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) + MockitoAnnotations.initMocks(this) whenever(burnInInteractor.keyguardBurnIn).thenReturn(burnInFlow) kosmos.burnInInteractor = burnInInteractor diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index 2de013bc7abc..c23ec2290d6a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -116,6 +116,23 @@ class KeyguardRootViewModelTest : SysuiTestCase() { } @Test + fun iconContainer_isNotVisible_onKeyguard_dontShowWhenGoneToAodTransitionRunning() = + testScope.runTest { + val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) + runCurrent() + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + testScope, + ) + whenever(screenOffAnimationController.shouldShowAodIconsWhenShade()).thenReturn(false) + runCurrent() + + assertThat(isVisible?.value).isFalse() + assertThat(isVisible?.isAnimating).isFalse() + } + + @Test fun iconContainer_isVisible_bypassEnabled() = testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 4595fbfab0d7..72617238cbb1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -18,22 +18,28 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.pm.UserInfo +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.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.communal.data.repository.fakeCommunalRepository -import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -49,9 +55,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { private val testScope = kosmos.testScope private val sceneInteractor by lazy { kosmos.sceneInteractor } - private val underTest by lazy { - createLockscreenSceneViewModel() - } + private val underTest by lazy { createLockscreenSceneViewModel() } @Test fun upTransitionSceneKey_canSwipeToUnlock_gone() = @@ -80,29 +84,37 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) } + @EnableFlags(FLAG_COMMUNAL_HUB) @Test fun leftTransitionSceneKey_communalIsEnabled_communal() = testScope.runTest { - kosmos.fakeCommunalRepository.setIsCommunalEnabled(true) - val underTest = createLockscreenSceneViewModel() - - assertThat(underTest.leftDestinationSceneKey).isEqualTo(SceneKey.Communal) + with(kosmos.fakeUserRepository) { + setUserInfos(listOf(PRIMARY_USER)) + setSelectedUserInfo(PRIMARY_USER) + } + kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) + val leftDestinationSceneKey by collectLastValue(underTest.leftDestinationSceneKey) + assertThat(leftDestinationSceneKey).isEqualTo(SceneKey.Communal) } + @DisableFlags(FLAG_COMMUNAL_HUB) @Test fun leftTransitionSceneKey_communalIsDisabled_null() = testScope.runTest { - kosmos.fakeCommunalRepository.setIsCommunalEnabled(false) - val underTest = createLockscreenSceneViewModel() - - assertThat(underTest.leftDestinationSceneKey).isNull() + with(kosmos.fakeUserRepository) { + setUserInfos(listOf(PRIMARY_USER)) + setSelectedUserInfo(PRIMARY_USER) + } + kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) + val leftDestinationSceneKey by collectLastValue(underTest.leftDestinationSceneKey) + assertThat(leftDestinationSceneKey).isNull() } private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel { return LockscreenSceneViewModel( applicationScope = testScope.backgroundScope, deviceEntryInteractor = kosmos.deviceEntryInteractor, - communalInteractor = kosmos.communalInteractor, + communalSettingsInteractor = kosmos.communalSettingsInteractor, longPress = KeyguardLongPressViewModel( interactor = mock(), @@ -110,4 +122,9 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { notifications = kosmos.notificationsPlaceholderViewModel, ) } + + private companion object { + val PRIMARY_USER = + UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index 42200a3d33ec..51f8b11ab72d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -61,7 +61,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { private val sceneInteractor by lazy { kosmos.sceneInteractor } private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } - private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() } + private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() }) private val footerActionsViewModel = mock<FooterActionsViewModel>() private val footerActionsViewModelFactory = mock<FooterActionsViewModel.Factory> { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 9d3f0d6a93f8..006f429ab98a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -40,7 +40,7 @@ import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.classifier.falsingCollector -import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor @@ -130,7 +130,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private val sceneInteractor by lazy { kosmos.sceneInteractor } private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } - private val communalInteractor by lazy { kosmos.communalInteractor } + private val communalSettingsInteractor by lazy { kosmos.communalSettingsInteractor } private val transitionState by lazy { MutableStateFlow<ObservableTransitionState>( @@ -155,7 +155,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { LockscreenSceneViewModel( applicationScope = testScope.backgroundScope, deviceEntryInteractor = deviceEntryInteractor, - communalInteractor = communalInteractor, + communalSettingsInteractor = communalSettingsInteractor, longPress = KeyguardLongPressViewModel( interactor = mock(), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 5ef095fd8201..f1f5dc378e0a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -82,7 +82,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { scope = testScope.backgroundScope, ) - private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() } + private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() }) private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml index dec9930959a0..a80d3b4d0fa8 100644 --- a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml +++ b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml @@ -20,6 +20,7 @@ android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0" + android:alpha="0.3" > <path android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 1b712568190b..15688c5202ac 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1599,6 +1599,15 @@ <!-- Accessibility label for hotspot icon [CHAR LIMIT=NONE] --> <string name="accessibility_status_bar_hotspot">Hotspot</string> + <!-- Accessibility label for no satellite connection [CHAR LIMIT=NONE] --> + <string name="accessibility_status_bar_satellite_no_connection">Satellite, no connection</string> + <!-- Accessibility label for poor satellite connection [CHAR LIMIT=NONE] --> + <string name="accessibility_status_bar_satellite_poor_connection">Satellite, poor connection</string> + <!-- Accessibility label for good satellite connection [CHAR LIMIT=NONE] --> + <string name="accessibility_status_bar_satellite_good_connection">Satellite, good connection</string> + <!-- Accessibility label for available satellite connection [CHAR LIMIT=NONE] --> + <string name="accessibility_status_bar_satellite_available">Satellite, connection available</string> + <!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] --> <string name="accessibility_managed_profile">Work profile</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index 1a10c7aeb573..458a21c5c426 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -38,7 +38,6 @@ import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.log.BouncerLogger; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -212,7 +211,6 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> private final FeatureFlags mFeatureFlags; private final SelectedUserInteractor mSelectedUserInteractor; private final UiEventLogger mUiEventLogger; - private final KeyboardRepository mKeyboardRepository; private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor; @Inject @@ -228,7 +226,6 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> KeyguardViewController keyguardViewController, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger, - KeyboardRepository keyboardRepository, KeyguardKeyboardInteractor keyguardKeyboardInteractor) { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; @@ -246,7 +243,6 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mFeatureFlags = featureFlags; mSelectedUserInteractor = selectedUserInteractor; mUiEventLogger = uiEventLogger; - mKeyboardRepository = keyboardRepository; mKeyguardKeyboardInteractor = keyguardKeyboardInteractor; } @@ -277,7 +273,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, emergencyButtonController, mFalsingCollector, mDevicePostureController, mFeatureFlags, mSelectedUserInteractor, - mUiEventLogger, mKeyboardRepository + mUiEventLogger, mKeyguardKeyboardInteractor ); } else if (keyguardInputView instanceof KeyguardSimPinView) { return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView, @@ -285,14 +281,15 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, - mKeyboardRepository); + mKeyguardKeyboardInteractor); } else if (keyguardInputView instanceof KeyguardSimPukView) { return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, - mKeyboardRepository); + mKeyguardKeyboardInteractor + ); } throw new RuntimeException("Unable to find controller for " + keyguardInputView); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 60dd5686c315..476497dea9c4 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -16,8 +16,8 @@ package com.android.keyguard; -import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import static com.android.systemui.Flags.pinInputFieldStyledFocusState; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.StateListDrawable; @@ -32,9 +32,9 @@ import android.view.ViewGroup; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -43,7 +43,7 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB private final LiftToActivateListener mLiftToActivateListener; private final FalsingCollector mFalsingCollector; - private final KeyboardRepository mKeyboardRepository; + private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor; protected PasswordTextView mPasswordEntry; private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> { @@ -75,13 +75,13 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB FalsingCollector falsingCollector, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, - KeyboardRepository keyboardRepository) { + KeyguardKeyboardInteractor keyguardKeyboardInteractor) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, falsingCollector, emergencyButtonController, featureFlags, selectedUserInteractor); mLiftToActivateListener = liftToActivateListener; mFalsingCollector = falsingCollector; - mKeyboardRepository = keyboardRepository; + mKeyguardKeyboardInteractor = keyguardKeyboardInteractor; mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId()); } @@ -132,7 +132,7 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB okButton.setOnHoverListener(mLiftToActivateListener); } if (pinInputFieldStyledFocusState()) { - collectFlow(mPasswordEntry, mKeyboardRepository.isAnyKeyboardConnected(), + collectFlow(mPasswordEntry, mKeyguardKeyboardInteractor.isAnyKeyboardConnected(), this::setKeyboardBasedFocusOutline); /** diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index b958f55bdf79..f4cda0204036 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -25,10 +25,10 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -61,11 +61,11 @@ public class KeyguardPinViewController FalsingCollector falsingCollector, DevicePostureController postureController, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger, - KeyboardRepository keyboardRepository) { + KeyguardKeyboardInteractor keyguardKeyboardInteractor) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyboardRepository); + keyguardKeyboardInteractor); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mPostureController = postureController; mLockPatternUtils = lockPatternUtils; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java index 1cdcbd06815f..558679e993e1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java @@ -42,9 +42,9 @@ import android.widget.ImageView; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -94,11 +94,12 @@ public class KeyguardSimPinViewController LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, TelephonyManager telephonyManager, FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) { + SelectedUserInteractor selectedUserInteractor, + KeyguardKeyboardInteractor keyguardKeyboardInteractor) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyboardRepository); + keyguardKeyboardInteractor); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java index f019d61a3ccc..cb1c4b3064ce 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -37,9 +37,9 @@ import android.widget.ImageView; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -91,11 +91,12 @@ public class KeyguardSimPukViewController LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, TelephonyManager telephonyManager, FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) { + SelectedUserInteractor selectedUserInteractor, + KeyguardKeyboardInteractor keyguardKeyboardInteractor) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyboardRepository); + keyguardKeyboardInteractor); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt index 7a560e846318..29df49b53882 100644 --- a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt +++ b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt @@ -29,26 +29,28 @@ import java.util.Optional import javax.inject.Inject /** - * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for - * screen on events, this will invoke the onDrawn Runnable after all tasks have completed. This - * should route back to the [com.android.systemui.keyguard.KeyguardService], which informs - * the system_server that keyguard has drawn. + * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for screen + * on events, this will invoke the onDrawn Runnable after all tasks have completed. This should + * route back to the [com.android.systemui.keyguard.KeyguardService], which informs the + * system_server that keyguard has drawn. */ @SysUISingleton -class ScreenOnCoordinator @Inject constructor( +class ScreenOnCoordinator +@Inject +constructor( unfoldComponent: Optional<SysUIUnfoldComponent>, - @Main private val mainHandler: Handler + @Main private val mainHandler: Handler, ) { - private val unfoldLightRevealAnimation = unfoldComponent.map( - SysUIUnfoldComponent::getUnfoldLightRevealOverlayAnimation).getOrNull() - private val foldAodAnimationController = unfoldComponent.map( - SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull() + private val foldAodAnimationController = + unfoldComponent.map(SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull() + private val fullScreenLightRevealAnimations = + unfoldComponent.map(SysUIUnfoldComponent::getFullScreenLightRevealAnimations).getOrNull() private val pendingTasks = PendingTasksContainer() /** - * When turning on, registers tasks that may need to run before invoking [onDrawn]. - * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService]. + * When turning on, registers tasks that may need to run before invoking [onDrawn]. This is + * called on a binder thread from [com.android.systemui.keyguard.KeyguardService]. */ @BinderThread fun onScreenTurningOn(onDrawn: Runnable) { @@ -56,8 +58,10 @@ class ScreenOnCoordinator @Inject constructor( pendingTasks.reset() - unfoldLightRevealAnimation?.onScreenTurningOn(pendingTasks.registerTask("unfold-reveal")) foldAodAnimationController?.onScreenTurningOn(pendingTasks.registerTask("fold-to-aod")) + fullScreenLightRevealAnimations?.forEach { + it.onScreenTurningOn(pendingTasks.registerTask(it::class.java.simpleName)) + } pendingTasks.onTasksComplete { if (Flags.enableBackgroundKeyguardOndrawnCallback()) { @@ -71,8 +75,8 @@ class ScreenOnCoordinator @Inject constructor( } /** - * Called when screen is fully turned on and screen on blocker is removed. - * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService]. + * Called when screen is fully turned on and screen on blocker is removed. This is called on a + * binder thread from [com.android.systemui.keyguard.KeyguardService]. */ @BinderThread fun onScreenTurnedOn() { diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java index 4c9782cdf36c..39e1c4150167 100644 --- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java +++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java @@ -36,7 +36,7 @@ import java.io.PrintWriter; * If your CoreStartable depends on different CoreStartables starting before it, use a * {@link com.android.systemui.startable.Dependencies} annotation to list out those dependencies. * - * @see SystemUIApplication#startServicesIfNeeded() + * @see SystemUIApplication#startSystemUserServicesIfNeeded() */ public interface CoreStartable extends Dumpable { diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt index e88aaf015f87..aab0b1e99e09 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt +++ b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt @@ -22,7 +22,6 @@ import android.content.BroadcastReceiver import android.content.ContentProvider import android.content.Context import android.content.Intent -import android.util.Log import androidx.core.app.AppComponentFactory import com.android.systemui.dagger.ContextComponentHelper import com.android.systemui.dagger.SysUIComponent @@ -91,7 +90,8 @@ abstract class SystemUIAppComponentFactoryBase : AppComponentFactory() { return app } - @UsesReflection(KeepTarget(instanceOfClassConstant = SysUIComponent::class, methodName = "inject")) + @UsesReflection( + KeepTarget(instanceOfClassConstant = SysUIComponent::class, methodName = "inject")) override fun instantiateProviderCompat(cl: ClassLoader, className: String): ContentProvider { val contentProvider = super.instantiateProviderCompat(cl, className) if (contentProvider is ContextInitializer) { @@ -103,11 +103,12 @@ abstract class SystemUIAppComponentFactoryBase : AppComponentFactory() { .getMethod("inject", contentProvider.javaClass) injectMethod.invoke(rootComponent, contentProvider) } catch (e: NoSuchMethodException) { - Log.w(TAG, "No injector for class: " + contentProvider.javaClass, e) + throw RuntimeException("No injector for class: " + contentProvider.javaClass, e) } catch (e: IllegalAccessException) { - Log.w(TAG, "No injector for class: " + contentProvider.javaClass, e) + throw RuntimeException("Injector inaccessible for class: " + + contentProvider.javaClass, e) } catch (e: InvocationTargetException) { - Log.w(TAG, "No injector for class: " + contentProvider.javaClass, e) + throw RuntimeException("Error while injecting: " + contentProvider.javaClass, e) } initializer } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 8aae20651a10..15ef61ed5934 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -42,6 +42,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.systemui.dagger.GlobalRootComponent; import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dump.DumpManager; +import com.android.systemui.process.ProcessWrapper; import com.android.systemui.res.R; import com.android.systemui.startable.Dependencies; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -77,6 +78,7 @@ public class SystemUIApplication extends Application implements private SystemUIAppComponentFactoryBase.ContextAvailableCallback mContextAvailableCallback; private SysUIComponent mSysUIComponent; private SystemUIInitializer mInitializer; + private ProcessWrapper mProcessWrapper; public SystemUIApplication() { super(); @@ -115,6 +117,7 @@ public class SystemUIApplication extends Application implements // Enable Looper trace points. // This allows us to see Handler callbacks on traces. rootComponent.getMainLooper().setTraceTag(Trace.TRACE_TAG_APP); + mProcessWrapper = rootComponent.getProcessWrapper(); // Set the application theme that is inherited by all services. Note that setting the // application theme in the manifest does only work for activities. Keep this in sync with @@ -132,7 +135,7 @@ public class SystemUIApplication extends Application implements View.setTraceLayoutSteps(true); } - if (rootComponent.getProcessWrapper().isSystemUser()) { + if (mProcessWrapper.isSystemUser()) { IntentFilter bootCompletedFilter = new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED); bootCompletedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); @@ -199,7 +202,11 @@ public class SystemUIApplication extends Application implements * <p>This method must only be called from the main thread.</p> */ - public void startServicesIfNeeded() { + public void startSystemUserServicesIfNeeded() { + if (!mProcessWrapper.isSystemUser()) { + Log.wtf(TAG, "Tried starting SystemUser services on non-SystemUser"); + return; // Per-user startables are handled in #startSystemUserServicesIfNeeded. + } final String vendorComponent = mInitializer.getVendorComponent(getResources()); // Sort the startables so that we get a deterministic ordering. @@ -219,6 +226,9 @@ public class SystemUIApplication extends Application implements * <p>This method must only be called from the main thread.</p> */ void startSecondaryUserServicesIfNeeded() { + if (mProcessWrapper.isSystemUser()) { + return; // Per-user startables are handled in #startSystemUserServicesIfNeeded. + } // Sort the startables so that we get a deterministic ordering. Map<Class<?>, Provider<CoreStartable>> sortedStartables = new TreeMap<>( Comparator.comparing(Class::getName)); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java index 872b00518d62..1a9b01fd4996 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java @@ -22,10 +22,10 @@ import android.os.Handler; import android.os.HandlerThread; import android.util.Log; -import com.android.systemui.res.R; import com.android.systemui.dagger.GlobalRootComponent; import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dagger.WMComponent; +import com.android.systemui.res.R; import com.android.systemui.util.InitializationChecker; import com.android.wm.shell.dagger.WMShellConcurrencyModule; import com.android.wm.shell.keyguard.KeyguardTransitions; @@ -124,9 +124,6 @@ public abstract class SystemUIInitializer { .setDesktopMode(Optional.ofNullable(null)); } mSysUIComponent = builder.build(); - if (initializeComponents) { - mSysUIComponent.init(); - } // Every other part of our codebase currently relies on Dependency, so we // really need to ensure the Dependency gets initialized early on. diff --git a/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java b/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java index f4ec6f75b06b..407f7643d9d8 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java @@ -19,12 +19,31 @@ package com.android.systemui; import android.app.Service; import android.content.Intent; import android.os.IBinder; +import android.util.Log; + +import com.android.systemui.process.ProcessWrapper; + +import javax.inject.Inject; public class SystemUISecondaryUserService extends Service { + private static final String TAG = "SysUISecondaryService"; + + private final ProcessWrapper mProcessWrapper; + + @Inject + SystemUISecondaryUserService(ProcessWrapper processWrapper) { + mProcessWrapper = processWrapper; + } + @Override public void onCreate() { super.onCreate(); + if (mProcessWrapper.isSystemUser()) { + Log.w(TAG, "SecondaryServices started for System User. Stopping it."); + stopSelf(); + return; + } ((SystemUIApplication) getApplication()).startSecondaryUserServicesIfNeeded(); } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index 76c228252484..b26be0c74ece 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -77,7 +77,7 @@ public class SystemUIService extends Service { super.onCreate(); // Start all of SystemUI - ((SystemUIApplication) getApplication()).startServicesIfNeeded(); + ((SystemUIApplication) getApplication()).startSystemUserServicesIfNeeded(); // Finish initializing dump logic mLogBufferFreezer.attach(mBroadcastDispatcher); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java index bf121fb8c752..e57323b81490 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java @@ -26,6 +26,7 @@ import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.text.Layout; import android.view.Gravity; import android.view.View; import android.view.ViewTreeObserver; @@ -162,6 +163,7 @@ class MenuMessageView extends LinearLayout implements mTextView.setPadding(/* left= */ 0, textPadding, /* right= */ 0, textPadding); mTextView.setTextSize(COMPLEX_UNIT_PX, textSize); mTextView.setTextColor(textColor); + mTextView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); final ColorStateList colorAccent = Utils.getColorAccent(getContext()); mUndoButton.setText(res.getString(R.string.accessibility_floating_button_undo)); diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt new file mode 100644 index 000000000000..d235c95eb906 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.shared.model + +import android.annotation.AttrRes +import android.annotation.ColorInt + +/** + * Models a color that can be either a specific [Color.Loaded] value or a resolvable theme + * [Color.Attribute] + */ +sealed interface Color { + + data class Loaded(@ColorInt val color: Int) : Color + + data class Attribute(@AttrRes val attribute: Int) : Color +} diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt index 12be32c54b22..964eb6f3a613 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt @@ -24,17 +24,12 @@ import androidx.annotation.DimenRes import androidx.annotation.LayoutRes import com.android.settingslib.Utils import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.onDensityOrFontScaleChanged import com.android.systemui.statusbar.policy.onThemeChanged import com.android.systemui.util.kotlin.emitOnStart -import com.android.systemui.util.view.bindLatest import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -91,46 +86,3 @@ constructor( .map { layoutInflater.inflate(id, root, attachToRoot) as T } } } - -/** - * Perform an inflation right away, then re-inflate whenever the device configuration changes, and - * call [onInflate] on the resulting view each time. Disposes of the [DisposableHandle] returned by - * [onInflate] when done. - * - * This never completes unless cancelled, it just suspends and waits for updates. It runs on a - * background thread using [backgroundDispatcher]. - * - * For parameters [resource], [root] and [attachToRoot], see [LayoutInflater.inflate]. - * - * An example use-case of this is when a view needs to be re-inflated whenever a configuration - * change occurs, which would require the ViewBinder to then re-bind the new view. For example, the - * code in the parent view's binder would look like: - * ``` - * parentView.repeatWhenAttached { - * configurationState - * .reinflateAndBindLatest( - * R.layout.my_layout, - * parentView, - * attachToRoot = false, - * coroutineScope = lifecycleScope, - * configurationController.onThemeChanged, - * ) { view: ChildView -> - * ChildViewBinder.bind(view, childViewModel) - * } - * } - * ``` - * - * In turn, the bind method (passed through [onInflate]) uses [repeatWhenAttached], which returns a - * [DisposableHandle]. - */ -suspend fun <T : View> ConfigurationState.reinflateAndBindLatest( - @LayoutRes resource: Int, - root: ViewGroup?, - attachToRoot: Boolean, - backgroundDispatcher: CoroutineDispatcher, - onInflate: (T) -> DisposableHandle?, -) { - inflateLayout<T>(resource, root, attachToRoot) - .flowOn(backgroundDispatcher) - .bindLatest(onInflate) -} 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 dc07c1b25678..0bad33b8f920 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -20,6 +20,7 @@ import com.android.systemui.communal.data.db.CommunalDatabaseModule import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryModule import com.android.systemui.communal.data.repository.CommunalRepositoryModule +import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule import com.android.systemui.communal.widgets.EditWidgetsActivityStarter @@ -36,6 +37,7 @@ import dagger.Module CommunalWidgetRepositoryModule::class, CommunalDatabaseModule::class, CommunalPrefsRepositoryModule::class, + CommunalSettingsRepositoryModule::class, ] ) interface CommunalModule { diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt new file mode 100644 index 000000000000..83a5bdb14ebd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.model + +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger +import java.util.EnumSet + +/** Reasons that communal is disabled, primarily for logging. */ +enum class DisabledReason(val loggingString: String) { + /** Communal should be disabled due to invalid current user */ + DISABLED_REASON_INVALID_USER("invalidUser"), + /** Communal should be disabled due to the flag being off */ + DISABLED_REASON_FLAG("flag"), + /** Communal should be disabled because the user has turned off the setting */ + DISABLED_REASON_USER_SETTING("userSetting"), + /** Communal is disabled by the device policy app */ + DISABLED_REASON_DEVICE_POLICY("devicePolicy"), +} + +/** + * Model representing the reasons communal hub should be disabled. Allows logging reasons separately + * for debugging. + */ +@JvmInline +value class CommunalEnabledState( + private val disabledReasons: EnumSet<DisabledReason> = + EnumSet.noneOf(DisabledReason::class.java) +) : Diffable<CommunalEnabledState>, Set<DisabledReason> by disabledReasons { + + /** Creates [CommunalEnabledState] with a single reason for being disabled */ + constructor(reason: DisabledReason) : this(EnumSet.of(reason)) + + /** Checks if there are any reasons communal should be disabled. If none, returns true. */ + val enabled: Boolean + get() = isEmpty() + + override fun logDiffs(prevVal: CommunalEnabledState, row: TableRowLogger) { + for (reason in DisabledReason.entries) { + val newVal = contains(reason) + if (newVal != prevVal.contains(reason)) { + row.logChange( + columnName = reason.loggingString, + value = newVal, + ) + } + } + } + + override fun logFull(row: TableRowLogger) { + for (reason in DisabledReason.entries) { + row.logChange(columnName = reason.loggingString, value = contains(reason)) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt index addd880f2079..4a06585f5a70 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt @@ -16,22 +16,14 @@ package com.android.systemui.communal.data.repository -import com.android.systemui.Flags.communalHub import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlagsClassic -import com.android.systemui.flags.Flags import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.util.settings.SecureSettings -import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -39,26 +31,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext /** Encapsulates the state of communal mode. */ interface CommunalRepository { - /** Whether communal features are enabled. */ - val isCommunalEnabled: Boolean - - /** - * A {@link StateFlow} that tracks whether communal hub is enabled (it can be disabled in - * settings). - */ - val communalEnabledState: StateFlow<Boolean> - /** Whether the communal hub is showing. */ val isCommunalHubShowing: Flow<Boolean> @@ -87,37 +66,11 @@ interface CommunalRepository { class CommunalRepositoryImpl @Inject constructor( - @Application private val applicationScope: CoroutineScope, @Background backgroundScope: CoroutineScope, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val featureFlagsClassic: FeatureFlagsClassic, sceneContainerFlags: SceneContainerFlags, sceneContainerRepository: SceneContainerRepository, - userRepository: UserRepository, - private val secureSettings: SecureSettings ) : CommunalRepository { - private val communalEnabledSettingState: Flow<Boolean> = - userRepository.selectedUserInfo - .flatMapLatest { userInfo -> observeSettings(userInfo.id) } - .shareIn(scope = applicationScope, started = SharingStarted.WhileSubscribed()) - - override val communalEnabledState: StateFlow<Boolean> = - if (featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()) { - communalEnabledSettingState - .filterNotNull() - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = true - ) - } else { - MutableStateFlow(false) - } - - override val isCommunalEnabled: Boolean - get() = communalEnabledState.value - private val _desiredScene: MutableStateFlow<CommunalSceneKey> = MutableStateFlow(CommunalSceneKey.DEFAULT) override val desiredScene: StateFlow<CommunalSceneKey> = _desiredScene.asStateFlow() @@ -153,26 +106,4 @@ constructor( } else { desiredScene.map { sceneKey -> sceneKey == CommunalSceneKey.Communal } } - - private fun observeSettings(userId: Int): Flow<Boolean> = - secureSettings - .observerFlow( - userId = userId, - names = - arrayOf( - GLANCEABLE_HUB_ENABLED, - ) - ) - // Force an update - .onStart { emit(Unit) } - .map { readFromSettings(userId) } - - private suspend fun readFromSettings(userId: Int): Boolean = - withContext(backgroundDispatcher) { - secureSettings.getIntForUser(GLANCEABLE_HUB_ENABLED, 1, userId) == 1 - } - - companion object { - private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled" - } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt new file mode 100644 index 000000000000..201b04927ea3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL +import android.content.IntentFilter +import android.content.pm.UserInfo +import com.android.systemui.Flags.communalHub +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.communal.data.model.CommunalEnabledState +import com.android.systemui.communal.data.model.DisabledReason +import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_DEVICE_POLICY +import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_FLAG +import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_INVALID_USER +import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_USER_SETTING +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.Flags +import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import java.util.EnumSet +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +interface CommunalSettingsRepository { + /** A [CommunalEnabledState] for the specified user. */ + fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> +} + +@SysUISingleton +class CommunalSettingsRepositoryImpl +@Inject +constructor( + @Background private val bgDispatcher: CoroutineDispatcher, + private val featureFlagsClassic: FeatureFlagsClassic, + private val secureSettings: SecureSettings, + private val broadcastDispatcher: BroadcastDispatcher, + private val devicePolicyManager: DevicePolicyManager, +) : CommunalSettingsRepository { + + private val flagEnabled: Boolean by lazy { + featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub() + } + + override fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> { + if (!user.isMain) { + return flowOf(CommunalEnabledState(DISABLED_REASON_INVALID_USER)) + } + if (!flagEnabled) { + return flowOf(CommunalEnabledState(DISABLED_REASON_FLAG)) + } + return combine( + getEnabledByUser(user).mapToReason(DISABLED_REASON_USER_SETTING), + getAllowedByDevicePolicy(user).mapToReason(DISABLED_REASON_DEVICE_POLICY), + ) { reasons -> + reasons.filterNotNull() + } + .map { reasons -> + if (reasons.isEmpty()) { + EnumSet.noneOf(DisabledReason::class.java) + } else { + EnumSet.copyOf(reasons) + } + } + .map { reasons -> CommunalEnabledState(reasons) } + .flowOn(bgDispatcher) + } + + private fun getEnabledByUser(user: UserInfo): Flow<Boolean> = + secureSettings + .observerFlow(userId = user.id, names = arrayOf(GLANCEABLE_HUB_ENABLED)) + // Force an update + .onStart { emit(Unit) } + .map { + secureSettings.getIntForUser( + GLANCEABLE_HUB_ENABLED, + ENABLED_SETTING_DEFAULT, + user.id, + ) == 1 + } + + private fun getAllowedByDevicePolicy(user: UserInfo): Flow<Boolean> = + broadcastDispatcher + .broadcastFlow( + filter = + IntentFilter(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), + user = user.userHandle + ) + .emitOnStart() + .map { devicePolicyManager.areKeyguardWidgetsAllowed(user.id) } + + companion object { + const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled" + private const val ENABLED_SETTING_DEFAULT = 1 + } +} + +private fun DevicePolicyManager.areKeyguardWidgetsAllowed(userId: Int): Boolean = + (getKeyguardDisabledFeatures(null, userId) and KEYGUARD_DISABLE_WIDGETS_ALL) == 0 + +private fun Flow<Boolean>.mapToReason(reason: DisabledReason) = map { enabled -> + if (enabled) null else reason +} diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryModule.kt index 48fd4fe2f15e..a931d3f7ec5b 100644 --- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,13 @@ * limitations under the License. */ -package com.android.soundpicker; +package com.android.systemui.communal.data.repository -import android.app.Application; +import dagger.Binds +import dagger.Module -import dagger.hilt.android.HiltAndroidApp; - -/** - * The main application class for the project. - */ -@HiltAndroidApp(Application.class) -public class RingtonePickerApplication extends Hilt_RingtonePickerApplication { +@Module +interface CommunalSettingsRepositoryModule { + @Binds + fun communalSettingsRepository(impl: CommunalSettingsRepositoryImpl): CommunalSettingsRepository } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 950ac3c3aae6..23f590e30235 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -42,7 +42,6 @@ import com.android.systemui.log.dagger.CommunalTableLog import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.smartspace.data.repository.SmartspaceRepository -import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.BooleanFlowOperators.and import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.BooleanFlowOperators.or @@ -74,8 +73,8 @@ constructor( private val communalPrefsRepository: CommunalPrefsRepository, mediaRepository: CommunalMediaRepository, smartspaceRepository: SmartspaceRepository, - userRepository: UserRepository, keyguardInteractor: KeyguardInteractor, + private val communalSettingsInteractor: CommunalSettingsInteractor, private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter, @CommunalLog logBuffer: LogBuffer, @@ -90,13 +89,12 @@ constructor( /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean - get() = communalRepository.isCommunalEnabled + get() = communalSettingsInteractor.isCommunalEnabled.value /** Whether communal features are enabled and available. */ val isCommunalAvailable: Flow<Boolean> = and( - communalRepository.communalEnabledState, - userRepository.selectedUserInfo.map { it.isMain }, + communalSettingsInteractor.isCommunalEnabled, not(keyguardInteractor.isEncryptedOrLockdown), or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming) ) diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt new file mode 100644 index 000000000000..0b096ce67fc5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import com.android.systemui.communal.data.model.CommunalEnabledState +import com.android.systemui.communal.data.repository.CommunalSettingsRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.dagger.CommunalTableLog +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class CommunalSettingsInteractor +@Inject +constructor( + @Background private val bgScope: CoroutineScope, + private val repository: CommunalSettingsRepository, + userInteractor: SelectedUserInteractor, + @CommunalTableLog tableLogBuffer: TableLogBuffer, +) { + /** Whether or not communal is enabled for the currently selected user. */ + val isCommunalEnabled: StateFlow<Boolean> = + userInteractor.selectedUserInfo + .flatMapLatest { user -> repository.getEnabledState(user) } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnPrefix = "disabledReason", + initialValue = CommunalEnabledState() + ) + .map { model -> model.enabled } + // Start this eagerly since the value is accessed synchronously in many places. + .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt index 1404ee20cb72..25dfc02a5d98 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt @@ -28,17 +28,18 @@ import com.android.systemui.log.table.logDiffsForTable import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch /** Encapsulates business-logic related to communal tutorial state. */ @@ -51,6 +52,7 @@ constructor( private val communalTutorialRepository: CommunalTutorialRepository, keyguardInteractor: KeyguardInteractor, private val communalRepository: CommunalRepository, + private val communalSettingsInteractor: CommunalSettingsInteractor, communalInteractor: CommunalInteractor, @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { @@ -110,20 +112,24 @@ constructor( return null } - private var job: Job? = null private fun listenForTransitionToUpdateTutorialState() { - if (!communalRepository.isCommunalEnabled) { - return - } - job = - scope.launch { - tutorialStateToUpdate.collect { - communalTutorialRepository.setTutorialState(it) - if (it == Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) { - job?.cancel() + scope.launch { + communalSettingsInteractor.isCommunalEnabled + .flatMapLatest { enabled -> + if (!enabled) { + emptyFlow() + } else { + tutorialStateToUpdate } } - } + .transformWhile { tutorialState -> + emit(tutorialState) + tutorialState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED + } + .collect { tutorialState -> + communalTutorialRepository.setTutorialState(tutorialState) + } + } } init { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index dd186d624c84..f7bc5cdc69c2 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -26,10 +26,13 @@ import com.android.keyguard.KeyguardViewController; import com.android.systemui.ScreenDecorationsModule; import com.android.systemui.accessibility.SystemActionsModule; import com.android.systemui.battery.BatterySaverModule; +import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; import com.android.systemui.media.dagger.MediaModule; +import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; +import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.navigationbar.NavigationBarControllerModule; import com.android.systemui.navigationbar.gestural.GestureModule; import com.android.systemui.plugins.qs.QSFactory; @@ -63,6 +66,7 @@ import com.android.systemui.statusbar.policy.IndividualSensorPrivacyControllerIm import com.android.systemui.statusbar.policy.SensorPrivacyController; import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl; import com.android.systemui.toast.ToastModule; +import com.android.systemui.unfold.SysUIUnfoldStartableModule; import com.android.systemui.unfold.UnfoldTransitionModule; import com.android.systemui.volume.dagger.VolumeModule; import com.android.systemui.wallpapers.dagger.WallpaperModule; @@ -92,12 +96,15 @@ import javax.inject.Named; AospPolicyModule.class, BatterySaverModule.class, CollapsedStatusBarFragmentStartableModule.class, + ConnectingDisplayViewModel.StartableModule.class, GestureModule.class, HeadsUpModule.class, KeyboardShortcutsModule.class, MediaModule.class, + MediaMuteAwaitConnectionCli.StartableModule.class, MultiUserUtilsModule.class, NavigationBarControllerModule.class, + NearbyMediaDevicesManager.StartableModule.class, PowerModule.class, QSModule.class, RearDisplayModule.class, @@ -108,6 +115,7 @@ import javax.inject.Named; ShadeModule.class, StartCentralSurfacesModule.class, SceneContainerFrameworkModule.class, + SysUIUnfoldStartableModule.class, UnfoldTransitionModule.Startables.class, ToastModule.class, VolumeModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index e7b87730f94b..3b0c281a7057 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -19,25 +19,15 @@ package com.android.systemui.dagger; import com.android.systemui.BootCompleteCacheImpl; import com.android.systemui.CoreStartable; import com.android.systemui.Dependency; -import com.android.systemui.Flags; import com.android.systemui.InitController; import com.android.systemui.SystemUIAppComponentFactoryBase; import com.android.systemui.dagger.qualifiers.PerUser; -import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardSliceProvider; -import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; -import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.people.PeopleProvider; import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.unfold.FoldStateLogger; -import com.android.systemui.unfold.FoldStateLoggingProvider; -import com.android.systemui.unfold.SysUIUnfoldComponent; -import com.android.systemui.unfold.UnfoldTransitionProgressProvider; -import com.android.systemui.unfold.dagger.UnfoldBg; -import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; @@ -126,42 +116,6 @@ public interface SysUIComponent { } /** - * Initializes all the SysUI components. - */ - default void init() { - // Initialize components that have no direct tie to the dagger dependency graph, - // but are critical to this component's operation - getSysUIUnfoldComponent() - .ifPresent( - c -> { - c.getUnfoldLightRevealOverlayAnimation().init(); - c.getUnfoldTransitionWallpaperController().init(); - c.getUnfoldHapticsPlayer(); - c.getNaturalRotationUnfoldProgressProvider().init(); - c.getUnfoldLatencyTracker().init(); - }); - // No init method needed, just needs to be gotten so that it's created. - getMediaMuteAwaitConnectionCli(); - getNearbyMediaDevicesManager(); - getConnectingDisplayViewModel().init(); - getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init); - getFoldStateLogger().ifPresent(FoldStateLogger::init); - - Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider; - - if (Flags.unfoldAnimationBackgroundProgress()) { - unfoldTransitionProgressProvider = getBgUnfoldTransitionProgressProvider(); - } else { - unfoldTransitionProgressProvider = getUnfoldTransitionProgressProvider(); - } - unfoldTransitionProgressProvider - .ifPresent( - (progressProvider) -> - getUnfoldTransitionProgressForwarder() - .ifPresent(progressProvider::addCallback)); - } - - /** * Provides a BootCompleteCache. */ @SysUISingleton @@ -180,37 +134,6 @@ public interface SysUIComponent { ContextComponentHelper getContextComponentHelper(); /** - * Creates a UnfoldTransitionProgressProvider that calculates progress in the background. - */ - @SysUISingleton - @UnfoldBg - Optional<UnfoldTransitionProgressProvider> getBgUnfoldTransitionProgressProvider(); - - /** - * Creates a UnfoldTransitionProgressProvider that calculates progress in the main thread. - */ - @SysUISingleton - Optional<UnfoldTransitionProgressProvider> getUnfoldTransitionProgressProvider(); - - /** - * Creates a UnfoldTransitionProgressForwarder. - */ - @SysUISingleton - Optional<UnfoldTransitionProgressForwarder> getUnfoldTransitionProgressForwarder(); - - /** - * Creates a FoldStateLoggingProvider. - */ - @SysUISingleton - Optional<FoldStateLoggingProvider> getFoldStateLoggingProvider(); - - /** - * Creates a FoldStateLogger. - */ - @SysUISingleton - Optional<FoldStateLogger> getFoldStateLogger(); - - /** * Main dependency providing module. */ @SysUISingleton @@ -227,22 +150,6 @@ public interface SysUIComponent { InitController getInitController(); /** - * For devices with a hinge: access objects within this component - */ - Optional<SysUIUnfoldComponent> getSysUIUnfoldComponent(); - - /** */ - MediaMuteAwaitConnectionCli getMediaMuteAwaitConnectionCli(); - - /** */ - NearbyMediaDevicesManager getNearbyMediaDevicesManager(); - - /** - * Creates a ConnectingDisplayViewModel - */ - ConnectingDisplayViewModel getConnectingDisplayViewModel(); - - /** * Returns {@link CoreStartable}s that should be started with the application. */ Map<Class<?>, Provider<CoreStartable>> getStartables(); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 28fd9a994f8a..1720de880cbd 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -17,6 +17,7 @@ package com.android.systemui.dagger; import android.app.INotificationManager; +import android.app.Service; import android.content.Context; import android.service.dreams.IDreamManager; @@ -28,6 +29,7 @@ import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; import com.android.systemui.CameraProtectionModule; +import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.accessibility.AccessibilityModule; import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule; import com.android.systemui.appops.dagger.AppOpsModule; @@ -150,6 +152,8 @@ import dagger.Binds; import dagger.BindsOptionalOf; import dagger.Module; import dagger.Provides; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; import java.util.Collections; import java.util.Optional; @@ -384,4 +388,9 @@ public abstract class SystemUIModule { @Binds abstract LargeScreenShadeInterpolator largeScreensShadeInterpolator( LargeScreenShadeInterpolatorImpl impl); + + @Binds + @IntoMap + @ClassKey(SystemUISecondaryUserService.class) + abstract Service bindsSystemUISecondaryUserService(SystemUISecondaryUserService service); } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt index 684627ba27bf..2461c26a56ca 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt @@ -39,4 +39,6 @@ constructor( /** Provide the current status of fingerprint authentication. */ val authenticationStatus: Flow<FingerprintAuthenticationStatus> = repository.authenticationStatus + + val isLockedOut: Flow<Boolean> = repository.isLockedOut } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index 98130eb10f3d..cf91e147d1b3 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -36,7 +36,6 @@ import com.android.systemui.deviceentry.shared.FaceAuthUiEvent import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.log.FaceAuthenticationLogger @@ -78,7 +77,7 @@ constructor( private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val faceAuthenticationLogger: FaceAuthenticationLogger, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, + private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, private val userRepository: UserRepository, private val facePropertyRepository: FacePropertyRepository, private val faceWakeUpTriggersConfig: FaceWakeUpTriggersConfig, @@ -149,14 +148,24 @@ constructor( } .launchIn(applicationScope) - deviceEntryFingerprintAuthRepository.isLockedOut - .onEach { - if (it) { + deviceEntryFingerprintAuthInteractor.isLockedOut + .sample(biometricSettingsRepository.isFaceAuthEnrolledAndEnabled, ::Pair) + .filter { (_, faceEnabledAndEnrolled) -> + // We don't care about this if face auth is not enabled. + faceEnabledAndEnrolled + } + .map { (fpLockedOut, _) -> fpLockedOut } + .sample(userRepository.selectedUser, ::Pair) + .onEach { (fpLockedOut, currentUser) -> + if (fpLockedOut) { faceAuthenticationLogger.faceLockedOut("Fingerprint locked out") - // We don't care about this if face auth is not enabled. if (isFaceAuthEnabledAndEnrolled()) { repository.setLockedOut(true) } + } else { + // Fingerprint is not locked out anymore, revert face lockout state back to + // previous value. + resetLockedOutState(currentUser.userInfo.id) } } .launchIn(applicationScope) @@ -169,10 +178,7 @@ constructor( val wasSwitching = previous.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS val isSwitching = curr.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS if (wasSwitching && !isSwitching) { - val lockoutMode = facePropertyRepository.getLockoutMode(curr.userInfo.id) - repository.setLockedOut( - lockoutMode == LockoutMode.PERMANENT || lockoutMode == LockoutMode.TIMED - ) + resetLockedOutState(curr.userInfo.id) yield() runFaceAuth( FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING, @@ -185,6 +191,13 @@ constructor( .launchIn(applicationScope) } + private suspend fun resetLockedOutState(currentUserId: Int) { + val lockoutMode = facePropertyRepository.getLockoutMode(currentUserId) + repository.setLockedOut( + lockoutMode == LockoutMode.PERMANENT || lockoutMode == LockoutMode.TIMED + ) + } + override fun onSwipeUpOnBouncer() { runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false) } diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt index 10aa70391f01..190062cdcca1 100644 --- a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.display.ui.viewmodel import android.app.Dialog import android.content.Context import com.android.server.policy.feature.flags.Flags +import com.android.systemui.CoreStartable import com.android.systemui.biometrics.Utils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -26,6 +27,10 @@ import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.ui.view.MirroringConfirmationDialog import com.android.systemui.statusbar.policy.ConfigurationController +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -47,12 +52,12 @@ constructor( @Application private val scope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, private val configurationController: ConfigurationController, -) { +) : CoreStartable { private var dialog: Dialog? = null /** Starts listening for pending displays. */ - fun init() { + override fun start() { val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val concurrentDisplaysInProgessFlow = if (Flags.enableDualDisplayBlocking()) { @@ -96,4 +101,12 @@ constructor( dialog?.hide() dialog = null } + + @Module + interface StartableModule { + @Binds + @IntoMap + @ClassKey(ConnectingDisplayViewModel::class) + fun bindsConnectingDisplayViewModel(impl: ConnectingDisplayViewModel): CoreStartable + } } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt new file mode 100644 index 000000000000..304fdd61a992 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt @@ -0,0 +1,40 @@ +/* + * 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.haptics.slider + +import android.view.View +import androidx.lifecycle.lifecycleScope +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.awaitCancellation + +object HapticSliderViewBinder { + /** + * Binds a [SeekableSliderHapticPlugin] to a [View]. The binded view should be a + * [android.widget.SeekBar] or a container of a [android.widget.SeekBar] + */ + @JvmStatic + fun bind(view: View?, plugin: SeekableSliderHapticPlugin) { + view?.repeatWhenAttached { + plugin.startInScope(lifecycleScope) + try { + awaitCancellation() + } finally { + plugin.stop() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt index 58fb6a95b872..931a86938592 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt @@ -20,11 +20,8 @@ import android.view.MotionEvent import android.view.VelocityTracker import android.widget.SeekBar import androidx.annotation.VisibleForTesting -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.time.SystemClock -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -43,10 +40,8 @@ class SeekableSliderHapticPlugin constructor( vibratorHelper: VibratorHelper, systemClock: SystemClock, - @Main private val mainDispatcher: CoroutineDispatcher, - @Application private val applicationScope: CoroutineScope, sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(), - sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(), + private val sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(), ) { private val velocityTracker = VelocityTracker.obtain() @@ -61,19 +56,15 @@ constructor( systemClock, ) - private val sliderTracker = - SeekableSliderTracker( - sliderHapticFeedbackProvider, - sliderEventProducer, - mainDispatcher, - sliderTrackerConfig, - ) + private var sliderTracker: SeekableSliderTracker? = null + + private var pluginScope: CoroutineScope? = null val isTracking: Boolean - get() = sliderTracker.isTracking + get() = sliderTracker?.isTracking == true - val trackerState: SliderState - get() = sliderTracker.currentState + val trackerState: SliderState? + get() = sliderTracker?.currentState /** * A waiting [Job] for a timer that estimates the key-up event when a key-down event is @@ -89,14 +80,20 @@ constructor( get() = keyUpJob != null && keyUpJob?.isActive == true /** - * Start the plugin. - * - * This starts the tracking of slider states, events and triggering of haptic feedback. + * Specify the scope for the plugin's operations and start the slider tracker in this scope. + * This also involves the key-up timer job. */ - fun start() { - if (!isTracking) { - sliderTracker.startTracking() - } + fun startInScope(scope: CoroutineScope) { + if (sliderTracker != null) stop() + sliderTracker = + SeekableSliderTracker( + sliderHapticFeedbackProvider, + sliderEventProducer, + scope, + sliderTrackerConfig, + ) + pluginScope = scope + sliderTracker?.startTracking() } /** @@ -104,7 +101,7 @@ constructor( * * This stops the tracking of slider states, events and triggers of haptic feedback. */ - fun stop() = sliderTracker.stopTracking() + fun stop() = sliderTracker?.stopTracking() /** React to a touch event */ fun onTouchEvent(event: MotionEvent?) { @@ -147,9 +144,9 @@ constructor( /** * An external key was pressed (e.g., a volume key). * - * This event is used to estimate the key-up event based on by running a timer as a waiting - * coroutine in the [keyUpTimerScope]. A key-up event in a slider corresponds to an onArrowUp - * event. Therefore, [onArrowUp] must be called after the timeout. + * This event is used to estimate the key-up event based on a running a timer as a waiting + * coroutine in the [pluginScope]. A key-up event in a slider corresponds to an onArrowUp event. + * Therefore, [onArrowUp] must be called after the timeout. */ fun onKeyDown() { if (!isTracking) return @@ -159,7 +156,7 @@ constructor( keyUpJob?.cancel() } keyUpJob = - applicationScope.launch { + pluginScope?.launch { delay(KEY_UP_TIMEOUT) onArrowUp() } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt index 10098faaa05e..0af303843a45 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt @@ -17,9 +17,7 @@ package com.android.systemui.haptics.slider import androidx.annotation.VisibleForTesting -import com.android.systemui.dagger.qualifiers.Main import kotlin.math.abs -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -31,21 +29,20 @@ import kotlinx.coroutines.launch * * The tracker runs a state machine to execute actions on touch-based events typical of a seekable * slider such as [android.widget.SeekBar]. Coroutines responsible for running the state machine, - * collecting slider events and maintaining waiting states are run on the main thread via the - * [com.android.systemui.dagger.qualifiers.Main] coroutine dispatcher. + * collecting slider events and maintaining waiting states are run on the provided [CoroutineScope]. * * @param[sliderStateListener] Listener of the slider state. * @param[sliderEventProducer] Producer of slider events arising from the slider. - * @param[mainDispatcher] [CoroutineDispatcher] used to launch coroutines for the collection of - * slider events and the launch of timer jobs. + * @param[trackerScope] [CoroutineScope] used to launch coroutines for the collection of slider + * events and the launch of timer jobs. * @property[config] Configuration parameters of the slider tracker. */ class SeekableSliderTracker( sliderStateListener: SliderStateListener, sliderEventProducer: SliderEventProducer, - @Main mainDispatcher: CoroutineDispatcher, + trackerScope: CoroutineScope, private val config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(), -) : SliderTracker(CoroutineScope(mainDispatcher), sliderStateListener, sliderEventProducer) { +) : SliderTracker(trackerScope, sliderStateListener, sliderEventProducer) { // History of the latest progress collected from slider events private var latestProgress = 0f diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 4cabd70cb142..2e233d8e5dd2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -327,7 +327,7 @@ public class KeyguardService extends Service { @Override public void onCreate() { - ((SystemUIApplication) getApplication()).startServicesIfNeeded(); + ((SystemUIApplication) getApplication()).startSystemUserServicesIfNeeded(); if (mShellTransitions == null || !Transitions.ENABLE_SHELL_TRANSITIONS) { RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 8f08efa92ad6..39bbf077656e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -131,7 +131,7 @@ import com.android.systemui.DejankUtils; import com.android.systemui.Dumpable; import com.android.systemui.EventLogTags; import com.android.systemui.animation.ActivityLaunchAnimator; -import com.android.systemui.animation.LaunchAnimator; +import com.android.systemui.animation.TransitionAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; @@ -960,7 +960,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, final ActivityLaunchAnimator.Controller mOccludeAnimationController = new ActivityLaunchAnimator.Controller() { @Override - public void onLaunchAnimationStart(boolean isExpandingFullyAbove) { + public void onTransitionAnimationStart(boolean isExpandingFullyAbove) { mOccludeAnimationPlaying = true; mScrimControllerLazy.get().setOccludeAnimationPlaying(true); } @@ -977,7 +977,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } @Override - public void onLaunchAnimationEnd(boolean launchIsFullScreen) { + public void onTransitionAnimationEnd(boolean launchIsFullScreen) { if (launchIsFullScreen) { mShadeController.get().instantCollapseShade(); } @@ -994,13 +994,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @NonNull @Override - public ViewGroup getLaunchContainer() { + public ViewGroup getTransitionContainer() { return ((ViewGroup) mKeyguardViewControllerLazy.get() .getViewRootImpl().getView()); } @Override - public void setLaunchContainer(@NonNull ViewGroup launchContainer) { + public void setTransitionContainer(@NonNull ViewGroup transitionContainer) { // No-op, launch container is always the shade. Log.wtf(TAG, "Someone tried to change the launch container for the " + "ActivityLaunchAnimator, which should never happen."); @@ -1008,9 +1008,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @NonNull @Override - public LaunchAnimator.State createAnimatorState() { - final int fullWidth = getLaunchContainer().getWidth(); - final int fullHeight = getLaunchContainer().getHeight(); + public TransitionAnimator.State createAnimatorState() { + final int fullWidth = getTransitionContainer().getWidth(); + final int fullHeight = getTransitionContainer().getHeight(); if (mUpdateMonitor.isSecureCameraLaunchedOverKeyguard()) { final float initialHeight = fullHeight / 3f; @@ -1018,7 +1018,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // Start the animation near the power button, at one-third size, since the // camera was launched from the power button. - return new LaunchAnimator.State( + return new TransitionAnimator.State( (int) (mPowerButtonY - initialHeight / 2f) /* top */, (int) (mPowerButtonY + initialHeight / 2f) /* bottom */, (int) (fullWidth - initialWidth) /* left */, @@ -1030,7 +1030,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // Start the animation in the center of the screen, scaled down to half // size. - return new LaunchAnimator.State( + return new TransitionAnimator.State( (int) (fullHeight - initialHeight) / 2, (int) (initialHeight + (fullHeight - initialHeight) / 2), (int) (fullWidth - initialWidth) / 2, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt index cc1cf911f1c1..7ae70a9a3e7c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt @@ -74,6 +74,7 @@ constructor( BurnInModel(translationX, translationY, burnInHelperWrapper.burnInScale()) } .distinctUntilChanged() + .stateIn(scope, SharingStarted.Lazily, BurnInModel()) /** * Use for max burn-in offsets that are NOT specified in pixels. This flow will recalculate the diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 5606d4301cfa..e0b5c0e1f4c6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -98,6 +98,8 @@ constructor( val modeOnCanceled = if (lastStartedStep.from == KeyguardState.LOCKSCREEN) { TransitionModeOnCanceled.REVERSE + } else if (lastStartedStep.from == KeyguardState.GONE) { + TransitionModeOnCanceled.RESET } else { TransitionModeOnCanceled.LAST_VALUE } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index 00b798901352..8b278cdb9a6c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -112,6 +112,38 @@ constructor( interpolator: Interpolator = LINEAR, name: String? = null ): Flow<Float> { + return sharedFlowWithState( + duration = duration, + onStep = onStep, + startTime = startTime, + onStart = onStart, + onCancel = onCancel, + onFinish = onFinish, + interpolator = interpolator, + name = name, + ) + .mapNotNull { stateToValue -> stateToValue.value } + } + + /** + * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted + * in the range of [0, 1]. View animations should begin and end within a subset of this + * range. This function maps the [startTime] and [duration] into [0, 1], when this subset is + * valid. + * + * Will return a [StateToValue], which encompasses the calculated value as well as the + * transitionState that is associated with it. + */ + fun sharedFlowWithState( + duration: Duration, + onStep: (Float) -> Float, + startTime: Duration = 0.milliseconds, + onStart: (() -> Unit)? = null, + onCancel: (() -> Float)? = null, + onFinish: (() -> Float)? = null, + interpolator: Interpolator = LINEAR, + name: String? = null + ): Flow<StateToValue> { if (!duration.isPositive()) { throw IllegalArgumentException("duration must be a positive number: $duration") } @@ -164,7 +196,6 @@ constructor( .also { logger.logTransitionStep(name, step, it.value) } } .distinctUntilChanged() - .mapNotNull { stateToValue -> stateToValue.value } } /** @@ -174,9 +205,9 @@ constructor( return sharedFlow(duration = 1.milliseconds, onStep = { value }, onFinish = { value }) } } - - data class StateToValue( - val transitionState: TransitionState, - val value: Float?, - ) } + +data class StateToValue( + val transitionState: TransitionState = TransitionState.FINISHED, + val value: Float? = 0f, +) 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 789d30ff1a31..9e7c70d67156 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 @@ -145,9 +145,7 @@ object KeyguardRootViewBinder { launch { viewModel.burnInLayerVisibility.collect { visibility -> childViews[burnInLayerId]?.visibility = visibility - // Reset alpha only for the icons, as they currently have their - // own animator - childViews[aodNotificationIconContainerId]?.alpha = 0f + childViews[aodNotificationIconContainerId]?.visibility = visibility } } @@ -313,6 +311,12 @@ object KeyguardRootViewBinder { } } + if (KeyguardShadeMigrationNssl.isEnabled) { + burnInParams.update { current -> + current.copy(translationY = { childViews[burnInLayerId]?.translationY }) + } + } + onLayoutChangeListener = OnLayoutChange(viewModel, burnInParams) view.addOnLayoutChangeListener(onLayoutChangeListener) @@ -435,11 +439,17 @@ object KeyguardRootViewBinder { } when { !isVisible.isAnimating -> { - alpha = 1f if (!KeyguardShadeMigrationNssl.isEnabled) { translationY = 0f } - visibility = if (isVisible.value) View.VISIBLE else View.INVISIBLE + visibility = + if (isVisible.value) { + alpha = 1f + View.VISIBLE + } else { + alpha = 0f + View.INVISIBLE + } } newAodTransition() -> { animateInIconTranslation() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt index 9cf3c955b35c..d4ea728bbffb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart /** Models UI state for the alpha of the AOD (always-on display). */ @SysUISingleton @@ -42,13 +43,15 @@ constructor( /** The alpha level for the entire lockscreen while in AOD. */ val alpha: Flow<Float> = combine( - keyguardTransitionInteractor.currentKeyguardState, + keyguardTransitionInteractor.transitionValue(KeyguardState.GONE).onStart { + emit(0f) + }, merge( keyguardInteractor.keyguardAlpha, occludedToLockscreenTransitionViewModel.lockscreenAlpha, ) - ) { currentKeyguardState, alpha -> - if (currentKeyguardState == KeyguardState.GONE) { + ) { transitionToGone, alpha -> + if (transitionToGone == 1f) { // Ensures content is not visible when in GONE state 0f } else { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index 828e03301b8e..8110de23be13 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt @@ -31,6 +31,10 @@ import com.android.systemui.keyguard.shared.model.BurnInModel import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.GONE +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.ui.StateToValue import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R import javax.inject.Inject @@ -58,6 +62,7 @@ constructor( private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel, + private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, private val keyguardClockViewModel: KeyguardClockViewModel, ) { @@ -83,21 +88,22 @@ constructor( burnIn(params).map { it.translationY.toFloat() }.onStart { emit(0f) }, goneToAodTransitionViewModel .enterFromTopTranslationY(enterFromTopAmount) - .onStart { emit(0f) }, + .onStart { emit(StateToValue()) }, occludedToLockscreenTransitionViewModel.lockscreenTranslationY.onStart { emit(0f) }, - ) { - keyguardTransitionY, - burnInTranslationY, - goneToAodTransitionTranslationY, - occludedToLockscreenTransitionTranslationY -> - - // All values need to be combined for a smooth translation - keyguardTransitionY + - burnInTranslationY + - goneToAodTransitionTranslationY + - occludedToLockscreenTransitionTranslationY + aodToLockscreenTransitionViewModel.translationY(params.translationY).onStart { + emit(StateToValue()) + }, + ) { keyguardTranslationY, burnInY, goneToAod, occludedToLockscreen, aodToLockscreen + -> + if (isInTransition(aodToLockscreen.transitionState)) { + aodToLockscreen.value ?: 0f + } else if (isInTransition(goneToAod.transitionState)) { + (goneToAod.value ?: 0f) + burnInY + } else { + burnInY + occludedToLockscreen + keyguardTranslationY + } } } .distinctUntilChanged() @@ -115,6 +121,10 @@ constructor( } } + private fun isInTransition(state: TransitionState): Boolean { + return state == STARTED || state == RUNNING + } + private fun burnIn( params: BurnInParameters, ): Flow<BurnInModel> { @@ -185,6 +195,8 @@ data class BurnInParameters( val topInset: Int = 0, /** Status view top, without translation added in */ val statusViewTop: Int = 0, + /** The current y translation of the view */ + val translationY: () -> Float? = { null } ) /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt index 266fd02d5bbf..6d1d3cb00a68 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt @@ -16,11 +16,14 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.MathUtils +import com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.StateToValue import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -48,6 +51,22 @@ constructor( to = KeyguardState.LOCKSCREEN, ) + /** + * Begin the transition from wherever the y-translation value is currently. This helps ensure a + * smooth transition if a transition in canceled. + */ + fun translationY(currentTranslationY: () -> Float?): Flow<StateToValue> { + var startValue = 0f + return transitionAnimation.sharedFlowWithState( + duration = 500.milliseconds, + onStart = { + startValue = currentTranslationY() ?: 0f + startValue + }, + onStep = { MathUtils.lerp(startValue, 0f, FAST_OUT_SLOW_IN.getInterpolation(it)) }, + ) + } + /** Ensure alpha is set to be visible */ val lockscreenAlpha: Flow<Float> = transitionAnimation.sharedFlow( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt index f5e61355df37..85885b065264 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt @@ -22,6 +22,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsIntera import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_AOD_DURATION import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.StateToValue import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -48,8 +49,8 @@ constructor( ) /** y-translation from the top of the screen for AOD */ - fun enterFromTopTranslationY(translatePx: Int): Flow<Float> { - return transitionAnimation.sharedFlow( + fun enterFromTopTranslationY(translatePx: Int): Flow<StateToValue> { + return transitionAnimation.sharedFlowWithState( startTime = 600.milliseconds, duration = 500.milliseconds, onStart = { translatePx }, @@ -63,8 +64,8 @@ constructor( /** alpha animation upon entering AOD */ val enterFromTopAnimationAlpha: Flow<Float> = transitionAnimation.sharedFlow( - startTime = 600.milliseconds, - duration = 500.milliseconds, + startTime = 700.milliseconds, + duration = 400.milliseconds, onStart = { 0f }, onStep = { it }, onFinish = { 1f }, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index f8a12bd226ad..ec13228c6216 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -30,6 +30,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScreenOffAnimationController @@ -48,6 +50,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton @@ -78,6 +81,12 @@ constructor( val goneToAodTransition = keyguardTransitionInteractor.transition(from = GONE, to = AOD) + private val goneToAodTransitionRunning: Flow<Boolean> = + goneToAodTransition + .map { it.transitionState == STARTED || it.transitionState == RUNNING } + .onStart { emit(false) } + .distinctUntilChanged() + /** Last point that the root view was tapped */ val lastRootViewTapPosition: Flow<Point?> = keyguardInteractor.lastRootViewTapPosition @@ -138,6 +147,7 @@ constructor( /** Is the notification icon container visible? */ val isNotifIconContainerVisible: Flow<AnimatedValue<Boolean>> = combine( + goneToAodTransitionRunning, keyguardTransitionInteractor.finishedKeyguardState.map { KeyguardState.lockscreenVisibleInState(it) }, @@ -145,6 +155,7 @@ constructor( areNotifsFullyHiddenAnimated(), isPulseExpandingAnimated(), ) { + goneToAodTransitionRunning: Boolean, onKeyguard: Boolean, isBypassEnabled: Boolean, notifsFullyHidden: AnimatedValue<Boolean>, @@ -154,7 +165,9 @@ constructor( // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off // animation is playing, in which case we want them to be visible if we're // animating in the AOD UI and will be switching to KEYGUARD shortly. - !onKeyguard && !screenOffAnimationController.shouldShowAodIconsWhenShade() -> + goneToAodTransitionRunning || + (!onKeyguard && + !screenOffAnimationController.shouldShowAodIconsWhenShade()) -> AnimatedValue.NotAnimating(false) else -> zip(notifsFullyHidden, pulseExpanding) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt index 2b28a71b4a3d..fa185570e7fc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt @@ -16,7 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor @@ -36,7 +36,7 @@ class LockscreenSceneViewModel constructor( @Application applicationScope: CoroutineScope, deviceEntryInteractor: DeviceEntryInteractor, - communalInteractor: CommunalInteractor, + communalSettingsInteractor: CommunalSettingsInteractor, val longPress: KeyguardLongPressViewModel, val notifications: NotificationsPlaceholderViewModel, ) { @@ -55,10 +55,12 @@ constructor( } /** The key of the scene we should switch to when swiping left. */ - val leftDestinationSceneKey: SceneKey? = - if (communalInteractor.isCommunalEnabled) { - SceneKey.Communal - } else { - null - } + val leftDestinationSceneKey: StateFlow<SceneKey?> = + communalSettingsInteractor.isCommunalEnabled + .map { available -> if (available) SceneKey.Communal else null } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java index 7a488365c740..3ab0420e12b7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java +++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java @@ -16,9 +16,6 @@ package com.android.systemui.media; -import static java.util.Objects.requireNonNull; - -import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; @@ -37,8 +34,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import android.os.VibrationEffect; -import android.os.vibrator.Flags; import android.provider.MediaStore; import android.util.Log; @@ -58,7 +53,7 @@ import javax.inject.Inject; @SysUISingleton public class RingtonePlayer implements CoreStartable { private static final String TAG = "RingtonePlayer"; - private static final boolean LOGD = true; + private static final boolean LOGD = false; private final Context mContext; // TODO: support Uri switching under same IBinder @@ -91,11 +86,20 @@ public class RingtonePlayer implements CoreStartable { */ private class Client implements IBinder.DeathRecipient { private final IBinder mToken; - private Ringtone mRingtone; + private final Ringtone mRingtone; + + public Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa) { + this(token, uri, user, aa, null); + } + + Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa, + @Nullable VolumeShaper.Configuration volumeShaperConfig) { + mToken = token; - Client(@NonNull IBinder token, @NonNull Ringtone ringtone) { - mToken = requireNonNull(token); - mRingtone = requireNonNull(ringtone); + mRingtone = new Ringtone(getContextForUser(user), false); + mRingtone.setAudioAttributesField(aa); + mRingtone.setUri(uri, volumeShaperConfig); + mRingtone.createLocalMediaPlayer(); } @Override @@ -112,48 +116,24 @@ public class RingtonePlayer implements CoreStartable { @Override public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping) throws RemoteException { - if (Flags.hapticsCustomizationRingtoneV2Enabled()) { - playRemoteRingtone(token, uri, aa, true, Ringtone.MEDIA_SOUND, - null, volume, looping, /* hapticGenerator= */ false, - null); - } else { - playWithVolumeShaping(token, uri, aa, volume, looping, null); - } + playWithVolumeShaping(token, uri, aa, volume, looping, null); } - @Override - public void playWithVolumeShaping( - IBinder token, Uri uri, AudioAttributes aa, float volume, + public void playWithVolumeShaping(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping, @Nullable VolumeShaper.Configuration volumeShaperConfig) throws RemoteException { if (LOGD) { - Log.d(TAG, "playWithVolumeShaping(token=" + token + ", uri=" + uri + ", uid=" + Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid=" + Binder.getCallingUid() + ")"); } Client client; synchronized (mClients) { client = mClients.get(token); - } - // Don't hold the lock while constructing the ringtone, since it can be slow. The caller - // shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and - // waste the build. - if (client == null) { - final UserHandle user = Binder.getCallingUserHandle(); - Ringtone ringtone = Ringtone.createV1WithCustomAudioAttributes( - getContextForUser(user), aa, uri, volumeShaperConfig, - /* allowRemote= */ false); - synchronized (mClients) { - client = mClients.get(token); - if (client == null) { - client = new Client(token, ringtone); - token.linkToDeath(client, 0); - mClients.put(token, client); - ringtone = null; // "owned" by the client now. - } - } - // Clean up ringtone if it was abandoned (a client already existed). - if (ringtone != null) { - ringtone.stop(); + if (client == null) { + final UserHandle user = Binder.getCallingUserHandle(); + client = new Client(token, uri, user, aa, volumeShaperConfig); + token.linkToDeath(client, 0); + mClients.put(token, client); } } client.mRingtone.setLooping(looping); @@ -162,54 +142,6 @@ public class RingtonePlayer implements CoreStartable { } @Override - public void playRemoteRingtone(IBinder token, Uri uri, AudioAttributes aa, - boolean useExactAudioAttributes, - @Ringtone.RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect, - float volume, - boolean looping, boolean isHapticGeneratorEnabled, - @Nullable VolumeShaper.Configuration volumeShaperConfig) - throws RemoteException { - if (LOGD) { - Log.d(TAG, "playRemoteRingtone(token=" + token + ", uri=" + uri + ", uid=" - + Binder.getCallingUid() + ")"); - } - - // Don't hold the lock while constructing the ringtone, since it can be slow. The caller - // shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and - // waste the build. - Client client; - synchronized (mClients) { - client = mClients.get(token); - } - if (client == null) { - final UserHandle user = Binder.getCallingUserHandle(); - Ringtone ringtone = new Ringtone.Builder(getContextForUser(user), enabledMedia, aa) - .setLocalOnly() - .setUri(uri) - .setLooping(looping) - .setInitialSoundVolume(volume) - .setUseExactAudioAttributes(useExactAudioAttributes) - .setEnableHapticGenerator(isHapticGeneratorEnabled) - .setVibrationEffect(vibrationEffect) - .setVolumeShaperConfig(volumeShaperConfig) - .build(); - if (ringtone == null) { - return; - } - synchronized (mClients) { - client = mClients.get(token); - if (client == null) { - client = new Client(token, ringtone); - token.linkToDeath(client, 0); - mClients.put(token, client); - } - } - } - // Ensure the client is initialized outside the all-clients lock, as it can be slow. - client.mRingtone.play(); - } - - @Override public void stop(IBinder token) { if (LOGD) Log.d(TAG, "stop(token=" + token + ")"); Client client; @@ -235,10 +167,10 @@ public class RingtonePlayer implements CoreStartable { return false; } } + @Override public void setPlaybackProperties(IBinder token, float volume, boolean looping, - boolean hapticGeneratorEnabled) { - // RingtoneV1-exclusive path. + boolean hapticGeneratorEnabled) { Client client; synchronized (mClients) { client = mClients.get(token); @@ -252,39 +184,6 @@ public class RingtonePlayer implements CoreStartable { } @Override - public void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled) { - Client client; - synchronized (mClients) { - client = mClients.get(token); - } - if (client != null) { - client.mRingtone.setHapticGeneratorEnabled(hapticGeneratorEnabled); - } - } - - @Override - public void setLooping(IBinder token, boolean looping) { - Client client; - synchronized (mClients) { - client = mClients.get(token); - } - if (client != null) { - client.mRingtone.setLooping(looping); - } - } - - @Override - public void setVolume(IBinder token, float volume) { - Client client; - synchronized (mClients) { - client = mClients.get(token); - } - if (client != null) { - client.mRingtone.setVolume(volume); - } - } - - @Override public void playAsync(Uri uri, UserHandle user, boolean looping, AudioAttributes aa, float volume) { if (LOGD) Log.d(TAG, "playAsync(uri=" + uri + ", user=" + user + ")"); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index 523414cfddbf..35e0271c1b8f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -1155,7 +1155,7 @@ constructor( onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS // TODO(b/311234666): revisit logic once interactions between the hub and // shade/keyguard state are finalized - isCommunalShowing && communalInteractor.isCommunalEnabled -> LOCATION_COMMUNAL_HUB + isCommunalShowing -> LOCATION_COMMUNAL_HUB onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN else -> LOCATION_QQS } diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt index 2ae3a631fa5a..e47564730c4d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt @@ -16,13 +16,18 @@ package com.android.systemui.media.muteawait -import android.content.Context +import android.annotation.SuppressLint import android.media.AudioAttributes.USAGE_MEDIA import android.media.AudioDeviceAttributes import android.media.AudioManager +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import java.io.PrintWriter import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -30,14 +35,15 @@ import javax.inject.Inject /** A command line interface to manually test [MediaMuteAwaitConnectionManager]. */ @SysUISingleton class MediaMuteAwaitConnectionCli @Inject constructor( - commandRegistry: CommandRegistry, - private val context: Context -) { - init { + private val commandRegistry: CommandRegistry, + private val audioManager: AudioManager, +) : CoreStartable { + override fun start() { commandRegistry.registerCommand(MEDIA_MUTE_AWAIT_COMMAND) { MuteAwaitCommand() } } inner class MuteAwaitCommand : Command { + @SuppressLint("MissingPermission") override fun execute(pw: PrintWriter, args: List<String>) { val device = AudioDeviceAttributes( AudioDeviceAttributes.ROLE_OUTPUT, @@ -49,8 +55,6 @@ class MediaMuteAwaitConnectionCli @Inject constructor( ) val startOrCancel = args[2] - val audioManager: AudioManager = - context.getSystemService(Context.AUDIO_SERVICE) as AudioManager when (startOrCancel) { START -> audioManager.muteAwaitConnection( @@ -65,6 +69,14 @@ class MediaMuteAwaitConnectionCli @Inject constructor( "[type] [name] [$START|$CANCEL]") } } + + @Module + interface StartableModule { + @Binds + @IntoMap + @ClassKey(MediaMuteAwaitConnectionCli::class) + fun bindsMediaMuteAwaitConnectionCli(impl: MediaMuteAwaitConnectionCli): CoreStartable + } } private const val MEDIA_MUTE_AWAIT_COMMAND = "media-mute-await" diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt index 64b772b6a02c..0dc10f6b3f56 100644 --- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt @@ -18,9 +18,14 @@ package com.android.systemui.media.nearby import android.media.INearbyMediaDevicesProvider import android.media.INearbyMediaDevicesUpdateCallback -import com.android.systemui.dagger.SysUISingleton import android.os.IBinder +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.CommandQueue +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import javax.inject.Inject /** @@ -30,9 +35,9 @@ import javax.inject.Inject */ @SysUISingleton class NearbyMediaDevicesManager @Inject constructor( - commandQueue: CommandQueue, + private val commandQueue: CommandQueue, private val logger: NearbyMediaDevicesLogger -) { +) : CoreStartable { private var providers: MutableList<INearbyMediaDevicesProvider> = mutableListOf() private var activeCallbacks: MutableList<INearbyMediaDevicesUpdateCallback> = mutableListOf() @@ -69,7 +74,7 @@ class NearbyMediaDevicesManager @Inject constructor( } } - init { + override fun start() { commandQueue.addCallback(commandQueueCallbacks) } @@ -108,4 +113,12 @@ class NearbyMediaDevicesManager @Inject constructor( } } } + + @Module + interface StartableModule { + @Binds + @IntoMap + @ClassKey(NearbyMediaDevicesManager::class) + fun bindsNearbyMediaDevicesManager(impl: NearbyMediaDevicesManager): CoreStartable + } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index 01672875fd44..aa03e6e00859 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -395,7 +395,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, } if (displayId == mDisplayId) { mLightBarController.onNavigationBarAppearanceChanged(appearance, nbModeChanged, - BarTransitions.MODE_TRANSPARENT, navbarColorManagedByIme); + mTransitionMode, navbarColorManagedByIme); } if (mBehavior != behavior) { mBehavior = behavior; diff --git a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java index 27510720ae2f..3671dd439239 100644 --- a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java @@ -16,6 +16,9 @@ package com.android.systemui.process; +import android.os.Process; +import android.os.UserHandle; + import javax.inject.Inject; /** @@ -30,6 +33,15 @@ public class ProcessWrapper { * Returns {@code true} if System User is running the current process. */ public boolean isSystemUser() { - return android.os.Process.myUserHandle().isSystem(); + return myUserHandle().isSystem(); + } + + /** + * Returns {@link UserHandle} as returned statically by {@link Process#myUserHandle()}. + * + * Please strongly consider using {@link com.android.systemui.settings.UserTracker} instead. + */ + public UserHandle myUserHandle() { + return Process.myUserHandle(); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index a2dfc0159c6e..c0d964405ab5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -157,6 +157,12 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed); } + } else { + // Don't measure the customizer with all the children, it will be measured separately + if (child != mQSCustomizer) { + super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, + parentHeightMeasureSpec, heightUsed); + } } } @@ -248,6 +254,25 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { + mHeader.getHeight(); } + // These next two methods are used with Scene container to determine the size of QQS and QS . + + /** + * Returns the size of the QQS container, regardless of the measured size of this view. + * @return size in pixels of QQS + */ + public int getQqsHeight() { + return mHeader.getHeight(); + } + + /** + * Returns the size of QS (or the QSCustomizer), regardless of the measured size of this view + * @return size in pixels of QS (or QSCustomizer) + */ + public int getQsHeight() { + return mQSCustomizer.isCustomizing() ? mQSCustomizer.getMeasuredHeight() + : mQSPanel.getMeasuredHeight(); + } + public void setExpansion(float expansion) { mQsExpansion = expansion; if (mQSPanelContainer != null) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java index 290821e4ab13..4d55714b01e1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java @@ -984,6 +984,14 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl return mListeningAndVisibilityLifecycleOwner; } + public int getQQSHeight() { + return mContainer.getQqsHeight(); + } + + public int getQSHeight() { + return mContainer.getQsHeight(); + } + @NeverCompile @Override public void dump(PrintWriter pw, String[] args) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt index 0d4339680dac..3d12eed4851f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt @@ -28,6 +28,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.qs.QSContainerController +import com.android.systemui.qs.QSContainerImpl import com.android.systemui.qs.QSImpl import com.android.systemui.qs.dagger.QSSceneComponent import com.android.systemui.res.R @@ -68,6 +69,15 @@ interface QSSceneAdapter { /** Set the current state for QS. [state]. */ fun setState(state: State) + /** The current height of QQS in the current [qsView], or 0 if there's no view. */ + val qqsHeight: Int + + /** + * The current height of QS in the current [qsView], or 0 if there's no view. If customizing, it + * will return the height allocated to the customizer. + */ + val qsHeight: Int + sealed class State( val isVisible: Boolean, val expansion: Float, @@ -121,6 +131,11 @@ constructor( val qsImpl = _qsImpl.asStateFlow() override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull() + override val qqsHeight: Int + get() = qsImpl.value?.qqsHeight ?: 0 + override val qsHeight: Int + get() = qsImpl.value?.qsHeight ?: 0 + // Same config changes as in FragmentHostManager private val interestingChanges = InterestingConfigChanges( diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index fcbe9a6675ab..356eb858d78f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -45,8 +45,8 @@ object KeyguardlessSceneContainerFrameworkModule { sceneKeys = listOf( SceneKey.Gone, - SceneKey.Shade, SceneKey.QuickSettings, + SceneKey.Shade, ), initialSceneKey = SceneKey.Gone, ) diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 0f3acaf7fc0c..c7d3a4af24c9 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -69,8 +69,8 @@ interface SceneContainerFrameworkModule { SceneKey.Communal, SceneKey.Lockscreen, SceneKey.Bouncer, - SceneKey.Shade, SceneKey.QuickSettings, + SceneKey.Shade, ), initialSceneKey = SceneKey.Lockscreen, ) diff --git a/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt index 76d1d3dd145e..6199a83f9075 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java index e9af295d1a1a..861a2edebf14 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java @@ -31,8 +31,8 @@ import com.android.internal.logging.UiEventLogger; import com.android.settingslib.RestrictedLockUtils; import com.android.systemui.Gefingerpoken; import com.android.systemui.classifier.Classifier; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.haptics.slider.SeekableSliderEventProducer; +import com.android.systemui.haptics.slider.HapticSliderViewBinder; +import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.res.R; import com.android.systemui.statusbar.VibratorHelper; @@ -42,8 +42,6 @@ import com.android.systemui.util.time.SystemClock; import javax.inject.Inject; -import kotlinx.coroutines.CoroutineDispatcher; - /** * {@code ViewController} for a {@code BrightnessSliderView} * @@ -63,23 +61,16 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV private final FalsingManager mFalsingManager; private final UiEventLogger mUiEventLogger; - private final BrightnessSliderHapticPlugin mBrightnessSliderHapticPlugin; + private final SeekableSliderHapticPlugin mBrightnessSliderHapticPlugin; private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { + mBrightnessSliderHapticPlugin.onTouchEvent(ev); int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { mFalsingManager.isFalseTouch(Classifier.BRIGHTNESS_SLIDER); - if (mBrightnessSliderHapticPlugin.getVelocityTracker() != null) { - mBrightnessSliderHapticPlugin.getVelocityTracker().clear(); - } - } else if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { - if (mBrightnessSliderHapticPlugin.getVelocityTracker() != null) { - mBrightnessSliderHapticPlugin.getVelocityTracker().addMovement(ev); - } } - return false; } @@ -93,7 +84,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV BrightnessSliderView brightnessSliderView, FalsingManager falsingManager, UiEventLogger uiEventLogger, - BrightnessSliderHapticPlugin brightnessSliderHapticPlugin) { + SeekableSliderHapticPlugin brightnessSliderHapticPlugin) { super(brightnessSliderView); mFalsingManager = falsingManager; mUiEventLogger = uiEventLogger; @@ -112,7 +103,6 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV protected void onViewAttached() { mView.setOnSeekBarChangeListener(mSeekListener); mView.setOnInterceptListener(mOnInterceptListener); - mBrightnessSliderHapticPlugin.start(); } @Override @@ -120,7 +110,6 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV mView.setOnSeekBarChangeListener(null); mView.setOnDispatchTouchEventListener(null); mView.setOnInterceptListener(null); - mBrightnessSliderHapticPlugin.stop(); } @Override @@ -225,10 +214,8 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (mListener != null) { mListener.onChanged(mTracking, progress, false); - SeekableSliderEventProducer eventProducer = - mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer(); - if (eventProducer != null && fromUser) { - eventProducer.onProgressChanged(seekBar, progress, fromUser); + if (fromUser) { + mBrightnessSliderHapticPlugin.onProgressChanged(seekBar, progress, fromUser); } } } @@ -239,11 +226,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV mUiEventLogger.log(BrightnessSliderEvent.BRIGHTNESS_SLIDER_STARTED_TRACKING_TOUCH); if (mListener != null) { mListener.onChanged(mTracking, getValue(), false); - SeekableSliderEventProducer eventProducer = - mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer(); - if (eventProducer != null) { - eventProducer.onStartTrackingTouch(seekBar); - } + mBrightnessSliderHapticPlugin.onStartTrackingTouch(seekBar); } if (mMirrorController != null) { @@ -258,11 +241,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV mUiEventLogger.log(BrightnessSliderEvent.BRIGHTNESS_SLIDER_STOPPED_TRACKING_TOUCH); if (mListener != null) { mListener.onChanged(mTracking, getValue(), true); - SeekableSliderEventProducer eventProducer = - mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer(); - if (eventProducer != null) { - eventProducer.onStopTrackingTouch(seekBar); - } + mBrightnessSliderHapticPlugin.onStopTrackingTouch(seekBar); } if (mMirrorController != null) { @@ -280,21 +259,18 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV private final UiEventLogger mUiEventLogger; private final VibratorHelper mVibratorHelper; private final SystemClock mSystemClock; - private final CoroutineDispatcher mMainDispatcher; @Inject public Factory( FalsingManager falsingManager, UiEventLogger uiEventLogger, VibratorHelper vibratorHelper, - SystemClock clock, - @Main CoroutineDispatcher mainDispatcher + SystemClock clock ) { mFalsingManager = falsingManager; mUiEventLogger = uiEventLogger; mVibratorHelper = vibratorHelper; mSystemClock = clock; - mMainDispatcher = mainDispatcher; } /** @@ -310,15 +286,11 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV int layout = getLayout(); BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context) .inflate(layout, viewRoot, false); - BrightnessSliderHapticPlugin plugin; - if (hapticBrightnessSlider()) { - plugin = new BrightnessSliderHapticPluginImpl( + SeekableSliderHapticPlugin plugin = new SeekableSliderHapticPlugin( mVibratorHelper, - mSystemClock, - mMainDispatcher - ); - } else { - plugin = new BrightnessSliderHapticPlugin() {}; + mSystemClock); + if (hapticBrightnessSlider()) { + HapticSliderViewBinder.bind(viewRoot, plugin); } return new BrightnessSliderController(root, mFalsingManager, mUiEventLogger, plugin); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPlugin.kt deleted file mode 100644 index f77511420491..000000000000 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPlugin.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.settings.brightness - -import android.view.VelocityTracker -import com.android.systemui.haptics.slider.SeekableSliderEventProducer - -/** Plugin component for the System UI brightness slider to incorporate dynamic haptics */ -interface BrightnessSliderHapticPlugin { - - /** Finger velocity tracker */ - val velocityTracker: VelocityTracker? - get() = null - - /** Producer of slider events from the underlying [android.widget.SeekBar] */ - val seekableSliderEventProducer: SeekableSliderEventProducer? - get() = null - - /** - * Start the plugin. - * - * This starts the tracking of slider states, events and triggering of haptic feedback. - */ - fun start() {} - - /** - * Stop the plugin - * - * This stops the tracking of slider states, events and triggers of haptic feedback. - */ - fun stop() {} -} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImpl.kt deleted file mode 100644 index 32561f0b4c4f..000000000000 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImpl.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.settings.brightness - -import android.view.VelocityTracker -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.haptics.slider.SeekableSliderEventProducer -import com.android.systemui.haptics.slider.SeekableSliderTracker -import com.android.systemui.haptics.slider.SliderHapticFeedbackProvider -import com.android.systemui.haptics.slider.SliderTracker -import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.util.time.SystemClock -import kotlinx.coroutines.CoroutineDispatcher - -/** - * Implementation of the [BrightnessSliderHapticPlugin]. - * - * For the specifics of the brightness slider in System UI, a [SeekableSliderEventProducer] is used - * as the producer of slider events, a [SliderHapticFeedbackProvider] is used as the listener of - * slider states to play haptic feedback depending on the state, and a [SeekableSliderTracker] is - * used as the state machine handler that tracks and manipulates the slider state. - */ -class BrightnessSliderHapticPluginImpl -@JvmOverloads -constructor( - vibratorHelper: VibratorHelper, - systemClock: SystemClock, - @Main mainDispatcher: CoroutineDispatcher, - override val velocityTracker: VelocityTracker = VelocityTracker.obtain(), - override val seekableSliderEventProducer: SeekableSliderEventProducer = - SeekableSliderEventProducer(), -) : BrightnessSliderHapticPlugin { - - private val sliderHapticFeedbackProvider: SliderHapticFeedbackProvider = - SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, clock = systemClock) - private val sliderTracker: SliderTracker = - SeekableSliderTracker( - sliderHapticFeedbackProvider, - seekableSliderEventProducer, - mainDispatcher, - ) - - val isTracking: Boolean - get() = sliderTracker.isTracking - - override fun start() { - if (!sliderTracker.isTracking) { - sliderTracker.startTracking() - } - } - - override fun stop() { - sliderTracker.stopTracking() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 1c7cc007cbc7..df845f559f2e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -35,6 +35,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.util.kotlin.collectFlow import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf /** * Controller that's responsible for the glanceable hub container view and its touch handling. @@ -105,13 +106,9 @@ constructor( */ private var shadeShowing = false - /** Returns true if the glanceable hub is enabled and the container view can be created. */ - fun isEnabled(): Boolean { - return communalInteractor.isCommunalEnabled && isComposeAvailable() - } - - /** Returns a {@link StateFlow} that tracks whether communal hub is available. */ - fun communalAvailable(): Flow<Boolean> = communalInteractor.isCommunalAvailable + /** Returns a flow that tracks whether communal hub is available. */ + fun communalAvailable(): Flow<Boolean> = + if (isComposeAvailable()) communalInteractor.isCommunalAvailable else flowOf(false) /** * Creates the container view containing the glanceable hub UI. @@ -127,9 +124,6 @@ constructor( /** Override for testing. */ @VisibleForTesting internal fun initView(containerView: View): View { - if (!isEnabled()) { - throw RuntimeException("Glanceable hub is not enabled") - } if (communalContainerView != null) { throw RuntimeException("Communal view has already been initialized") } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 133fa12f8a87..73d229eae972 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -113,7 +113,7 @@ import com.android.systemui.DejankUtils; import com.android.systemui.Dumpable; import com.android.systemui.Gefingerpoken; import com.android.systemui.animation.ActivityLaunchAnimator; -import com.android.systemui.animation.LaunchAnimator; +import com.android.systemui.animation.TransitionAnimator; import com.android.systemui.biometrics.AuthController; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants; @@ -1348,7 +1348,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } updateClockAppearance(); mQsController.updateQsState(); - if (!KeyguardShadeMigrationNssl.isEnabled()) { + if (!KeyguardShadeMigrationNssl.isEnabled() && !FooterViewRefactor.isEnabled()) { mNotificationStackScrollLayoutController.updateFooter(); } } @@ -3231,7 +3231,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void applyLaunchAnimationProgress(float linearProgress) { - boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, + boolean hideIcons = TransitionAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f; if (hideIcons != mHideIconsDuringLaunchAnimation) { mHideIconsDuringLaunchAnimation = hideIcons; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt index 6e8507422fbe..ac510fee1991 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt @@ -156,6 +156,54 @@ data class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevea } } +data class LinearSideLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect { + + override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) { + scrim.interpolatedRevealAmount = amount + scrim.startColorAlpha = + getPercentPastThreshold(1 - amount, threshold = 1 - START_COLOR_REVEAL_PERCENTAGE) + scrim.revealGradientEndColorAlpha = + 1f - + getPercentPastThreshold( + amount, + threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE + ) + + val gradientBoundsAmount = lerp(GRADIENT_START_BOUNDS_PERCENTAGE, 1f, amount) + if (isVertical) { + scrim.setRevealGradientBounds( + left = -(scrim.viewWidth) * gradientBoundsAmount, + top = -(scrim.viewHeight) * gradientBoundsAmount, + right = (scrim.viewWidth) * gradientBoundsAmount, + bottom = (scrim.viewHeight) + (scrim.viewHeight) * gradientBoundsAmount + ) + } else { + scrim.setRevealGradientBounds( + left = -(scrim.viewWidth) * gradientBoundsAmount, + top = -(scrim.viewHeight) * gradientBoundsAmount, + right = (scrim.viewWidth) + (scrim.viewWidth) * gradientBoundsAmount, + bottom = (scrim.viewHeight) * gradientBoundsAmount + ) + } + } + + private companion object { + // From which percentage we should start the gradient reveal width + // E.g. if 0 - starts with 0px width, 0.6f - starts with 60% width + private const val GRADIENT_START_BOUNDS_PERCENTAGE: Float = 1f + + // When to start changing alpha color of the gradient scrim + // E.g. if 0.6f - starts fading the gradient away at 60% and becomes completely + // transparent at 100% + private const val REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE: Float = 1f + + // When to finish displaying start color fill that reveals the content + // E.g. if 0.6f - the content won't be visible at 0% and it will gradually + // reduce the alpha until 60% (at this point the color fill is invisible) + private const val START_COLOR_REVEAL_PERCENTAGE: Float = 1f + } +} + data class CircleReveal( /** X-value of the circle center of the reveal. */ val centerX: Int, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt index 785e65dd4026..6f4a7cdaff5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt @@ -4,7 +4,7 @@ import android.util.MathUtils import com.android.internal.annotations.VisibleForTesting import com.android.systemui.animation.ActivityLaunchAnimator import com.android.app.animation.Interpolators -import com.android.systemui.animation.LaunchAnimator +import com.android.systemui.animation.TransitionAnimator import kotlin.math.min /** Parameters for the notifications launch expanding animations. */ @@ -16,7 +16,7 @@ class LaunchAnimationParameters( topCornerRadius: Float = 0f, bottomCornerRadius: Float = 0f -) : LaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) { +) : TransitionAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) { @VisibleForTesting constructor() : this( top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f @@ -58,7 +58,7 @@ class LaunchAnimationParameters( } fun getProgress(delay: Long, duration: Long): Float { - return LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, delay, + return TransitionAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, delay, duration) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt index 5983fc17d4c3..8fc961936abd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt @@ -20,7 +20,7 @@ import android.util.Log import android.view.ViewGroup import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.ActivityLaunchAnimator -import com.android.systemui.animation.LaunchAnimator +import com.android.systemui.animation.TransitionAnimator import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.stack.NotificationListContainer @@ -75,13 +75,13 @@ class NotificationLaunchAnimatorController( private val notificationEntry = notification.entry private val notificationKey = notificationEntry.sbn.key - override var launchContainer: ViewGroup + override var transitionContainer: ViewGroup get() = notification.rootView as ViewGroup set(ignored) { // Do nothing. Notifications are always animated inside their rootView. } - override fun createAnimatorState(): LaunchAnimator.State { + override fun createAnimatorState(): TransitionAnimator.State { // If the notification panel is collapsed, the clip may be larger than the height. val height = max(0, notification.actualHeight - notification.clipBottomAmount) val location = notification.locationOnScreen @@ -186,14 +186,14 @@ class NotificationLaunchAnimatorController( onFinishAnimationCallback?.run() } - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { notification.isExpandAnimationRunning = true notificationListContainer.setExpandingNotification(notification) jankMonitor.begin(notification, InteractionJankMonitor.CUJ_NOTIFICATION_APP_START) } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { if (ActivityLaunchAnimator.DEBUG_LAUNCH_ANIMATION) { Log.d(TAG, "onLaunchAnimationEnd()") } @@ -213,8 +213,8 @@ class NotificationLaunchAnimatorController( notificationListContainer.applyLaunchAnimationParams(params) } - override fun onLaunchAnimationProgress( - state: LaunchAnimator.State, + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, progress: Float, linearProgress: Float ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index 6e2beb45f3f2..8b0b9735754b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -56,8 +56,8 @@ internal constructor( if (FooterViewRefactor.isEnabled) { activeNotificationsInteractor.setNotifStats(notifStats) } - // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the footer - // visibility is handled in the new stack. + // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the silent + // section clear action is handled in the new stack. controller.setNotifStats(notifStats) if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) { renderListInteractor.setRenderedList(entries) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt index 0a9e12a23b8e..ccf6f40d8575 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt index 5111c11ac584..b23ef356c1c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt @@ -52,6 +52,9 @@ class FooterViewModel( isVisible = activeNotificationsInteractor.hasClearableNotifications .sample( + // TODO(b/322167853): This check is currently duplicated in + // NotificationListViewModel, but instead it should be a field in + // ShadeAnimationInteractor. combine( shadeInteractor.isShadeFullyExpanded, shadeInteractor.isShadeTouchable, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt index d903f066cde9..8768ea267b3f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt @@ -29,6 +29,8 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -56,6 +58,8 @@ constructor( panelTouchesEnabled && isKeyguardVisible } .flowOn(bgContext) + .conflate() + .distinctUntilChanged() /** Amount of a "white" tint to be applied to the icons. */ val tintAlpha: Flow<Float> = @@ -70,6 +74,8 @@ constructor( aodAmt + dozeAmt // If transitioning between them, they should sum to 1f } .flowOn(bgContext) + .conflate() + .distinctUntilChanged() /** Are notification icons animated (ex: animated gif)? */ val areIconAnimationsEnabled: Flow<Boolean> = @@ -78,8 +84,10 @@ constructor( // Don't animate icons when we're on AOD / dozing it != KeyguardState.AOD && it != KeyguardState.DOZING } - .flowOn(bgContext) .onStart { emit(true) } + .flowOn(bgContext) + .conflate() + .distinctUntilChanged() /** [NotificationIconsViewData] indicating which icons to display in the view. */ val icons: Flow<NotificationIconsViewData> = @@ -91,4 +99,6 @@ constructor( ) } .flowOn(bgContext) + .conflate() + .distinctUntilChanged() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt index 357482847700..260cccd7967f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt @@ -21,6 +21,8 @@ import com.android.systemui.statusbar.notification.icon.ui.viewmodel.Notificatio import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -56,4 +58,6 @@ constructor( ) } .flowOn(bgContext) + .conflate() + .distinctUntilChanged() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt index 38921c2f773b..a64f888cb238 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt @@ -35,6 +35,7 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOn @@ -64,10 +65,13 @@ constructor( panelTouchesEnabled && !isKeyguardShowing } .flowOn(bgContext) + .conflate() + .distinctUntilChanged() /** The colors with which to display the notification icons. */ val iconColors: Flow<NotificationIconColorLookup> = - combine(darkIconInteractor.tintAreas, darkIconInteractor.tintColor) { areas, tint -> + darkIconInteractor.darkState + .map { (areas: Collection<Rect>, tint: Int) -> NotificationIconColorLookup { viewBounds: Rect -> if (DarkIconDispatcher.isInAreas(areas, viewBounds)) { IconColorsImpl(tint, areas) @@ -77,6 +81,8 @@ constructor( } } .flowOn(bgContext) + .conflate() + .distinctUntilChanged() /** [NotificationIconsViewData] indicating which icons to display in the view. */ val icons: Flow<NotificationIconsViewData> = @@ -88,6 +94,8 @@ constructor( ) } .flowOn(bgContext) + .conflate() + .distinctUntilChanged() /** An Icon to show "isolated" in the IconContainer. */ val isolatedIcon: Flow<AnimatedValue<NotificationIconInfo?>> = @@ -99,6 +107,8 @@ constructor( } .distinctUntilChanged() .flowOn(bgContext) + .conflate() + .distinctUntilChanged() .pairwise(initialValue = null) .sample(shadeInteractor.shadeExpansion) { (prev, iconInfo), shadeExpansion -> val animate = @@ -113,7 +123,7 @@ constructor( /** Location to show an isolated icon, if there is one. */ val isolatedIconLocation: Flow<Rect> = - headsUpIconInteractor.isolatedIconLocation.filterNotNull() + headsUpIconInteractor.isolatedIconLocation.filterNotNull().conflate().distinctUntilChanged() private class IconColorsImpl( override val tint: Int, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 0a11eb26cc07..42b56e791f77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -752,6 +752,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } public void setIsRemoteInputActive(boolean isActive) { + FooterViewRefactor.assertInLegacyMode(); mIsRemoteInputActive = isActive; updateFooter(); } @@ -764,6 +765,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @VisibleForTesting public void updateFooter() { + FooterViewRefactor.assertInLegacyMode(); if (mFooterView == null || mController == null) { return; } @@ -776,10 +778,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } private boolean shouldShowDismissView() { + FooterViewRefactor.assertInLegacyMode(); return mController.hasActiveClearableNotifications(ROWS_ALL); } private boolean shouldShowFooterView(boolean showDismissView) { + FooterViewRefactor.assertInLegacyMode(); return (showDismissView || mController.getVisibleNotificationCount() > 0) && mIsCurrentUserSetup // see: b/193149550 && !onKeyguard() @@ -4359,6 +4363,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable layoutEnd -= mShelf.getIntrinsicHeight() + mPaddingBetweenElements; } if (endPosition > layoutEnd) { + // if Scene Container is active, send bottom notification expansion delta + // to it so that it can scroll the stack and scrim accordingly. + if (SceneContainerFlag.isEnabled()) { + float diff = endPosition - layoutEnd; + mController.sendSyntheticScrollToSceneFramework(diff); + } setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd)); mDisallowScrollingInThisMotion = true; } @@ -4723,9 +4733,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable footerView.setClearAllButtonVisible(false /* visible */, true /* animate */); }); } - if (FooterViewRefactor.isEnabled()) { - updateFooter(); - } } public void setEmptyShadeView(EmptyShadeView emptyShadeView) { @@ -4790,16 +4797,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } public void updateFooterView(boolean visible, boolean showDismissView, boolean showHistory) { + FooterViewRefactor.assertInLegacyMode(); if (mFooterView == null || mNotificationStackSizeCalculator == null) { return; } boolean animate = mIsExpanded && mAnimationsEnabled; mFooterView.setVisible(visible, animate); - if (!FooterViewRefactor.isEnabled()) { - mFooterView.showHistory(showHistory); - mFooterView.setClearAllButtonVisible(showDismissView, animate); - mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications); - } + mFooterView.showHistory(showHistory); + mFooterView.setClearAllButtonVisible(showDismissView, animate); + mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications); } @VisibleForTesting @@ -5070,7 +5076,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (mOwnScrollY > 0) { setOwnScrollY((int) MathUtils.lerp(mOwnScrollY, 0, mQsExpansionFraction)); } - if (footerAffected) { + if (!FooterViewRefactor.isEnabled() && footerAffected) { updateFooter(); } } @@ -5081,6 +5087,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) { + // If scene container is active, NSSL should not control its own scrolling. + if (SceneContainerFlag.isEnabled()) { + return; + } // Avoid Flicking during clear all // when the shade finishes closing, onExpansionStopped will call // resetScrollPosition to setOwnScrollY to 0 @@ -5176,6 +5186,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } void setUpcomingStatusBarState(int upcomingStatusBarState) { + FooterViewRefactor.assertInLegacyMode(); mUpcomingStatusBarState = upcomingStatusBarState; if (mUpcomingStatusBarState != mStatusBarState) { updateFooter(); @@ -5193,7 +5204,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable setDimmed(onKeyguard, fromShadeLocked); setExpandingEnabled(!onKeyguard); - updateFooter(); + if (!FooterViewRefactor.isEnabled()) { + updateFooter(); + } requestChildrenUpdate(); onUpdateRowStates(); updateVisibility(); @@ -5270,8 +5283,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable for (int i = 0; i < childCount; i++) { ExpandableView child = getChildAtIndex(i); child.dump(pw, args); - if (child instanceof FooterView) { - DumpUtilsKt.withIncreasedIndent(pw, () -> dumpFooterViewVisibility(pw)); + if (!FooterViewRefactor.isEnabled()) { + if (child instanceof FooterView) { + DumpUtilsKt.withIncreasedIndent(pw, + () -> dumpFooterViewVisibility(pw)); + } } pw.println(); } @@ -5290,6 +5306,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } private void dumpFooterViewVisibility(IndentingPrintWriter pw) { + FooterViewRefactor.assertInLegacyMode(); final boolean showDismissView = shouldShowDismissView(); pw.println("showFooterView: " + shouldShowFooterView(showDismissView)); @@ -5988,6 +6005,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * Sets whether the current user is set up, which is required to show the footer (b/193149550) */ public void setCurrentUserSetup(boolean isCurrentUserSetup) { + FooterViewRefactor.assertInLegacyMode(); if (mIsCurrentUserSetup != isCurrentUserSetup) { mIsCurrentUserSetup = isCurrentUserSetup; updateFooter(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index a2ff406b9599..5fa0624c96df 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -314,8 +314,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { // The bottom might change because we're using the final actual height of the view mView.setAnimateBottomOnLayout(true); } - // Let's update the footer once the notifications have been updated (in the next frame) - mView.post(this::updateFooter); + if (!FooterViewRefactor.isEnabled()) { + // Let's update the footer once the notifications have been updated (in the next frame) + mView.post(this::updateFooter); + } }; @VisibleForTesting @@ -342,8 +344,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.reinflateViews(); if (!FooterViewRefactor.isEnabled()) { updateShowEmptyShadeView(); + updateFooter(); } - updateFooter(); } @Override @@ -389,7 +391,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { @Override public void onUpcomingStateChanged(int newState) { - mView.setUpcomingStatusBarState(newState); + if (!FooterViewRefactor.isEnabled()) { + mView.setUpcomingStatusBarState(newState); + } } @Override @@ -407,7 +411,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void onUserChanged(int userId) { updateSensitivenessWithAnimation(false); mHistoryEnabled = null; - updateFooter(); + if (!FooterViewRefactor.isEnabled()) { + updateFooter(); + } } }; @@ -810,14 +816,14 @@ public class NotificationStackScrollLayoutController implements Dumpable { if (!FooterViewRefactor.isEnabled()) { mView.setFooterClearAllListener(() -> mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES)); + mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive()); + mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() { + @Override + public void onRemoteInputActive(boolean active) { + mView.setIsRemoteInputActive(active); + } + }); } - mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive()); - mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() { - @Override - public void onRemoteInputActive(boolean active) { - mView.setIsRemoteInputActive(active); - } - }); mView.setClearAllFinishedWhilePanelExpandedRunnable(()-> { final Runnable doCollapseRunnable = () -> mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE); @@ -871,7 +877,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { switch (key) { case Settings.Secure.NOTIFICATION_HISTORY_ENABLED: mHistoryEnabled = null; // invalidate - updateFooter(); + if (!FooterViewRefactor.isEnabled()) { + updateFooter(); + } break; case HIGH_PRIORITY: mView.setHighPriorityBeforeSpeedBump("1".equals(newValue)); @@ -893,9 +901,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { return kotlin.Unit.INSTANCE; }); - // attach callback, and then call it to update mView immediately - mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); - mDeviceProvisionedListener.onDeviceProvisionedChanged(); + if (!FooterViewRefactor.isEnabled()) { + // attach callback, and then call it to update mView immediately + mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); + mDeviceProvisionedListener.onDeviceProvisionedChanged(); + } if (screenshareNotificationHiding()) { mSensitiveNotificationProtectionController @@ -1106,8 +1116,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public int getVisibleNotificationCount() { - // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle footer - // visibility in the refactored code + FooterViewRefactor.assertInLegacyMode(); return mNotifStats.getNumActiveNotifs(); } @@ -1142,6 +1151,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { } } + /** Send internal notification expansion to the scene container framework. */ + public void sendSyntheticScrollToSceneFramework(Float delta) { + mStackAppearanceInteractor.setSyntheticScroll(delta); + } + /** Get the y-coordinate of the top bound of the stack. */ public float getPlaceholderTop() { return mStackAppearanceInteractor.getStackBounds().getValue().getTop(); @@ -1509,14 +1523,14 @@ public class NotificationStackScrollLayoutController implements Dumpable { * Return whether there are any clearable notifications */ public boolean hasActiveClearableNotifications(@SelectedRows int selection) { - // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer - // visibility in the refactored code + // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the silent + // section clear action in the new stack. return hasNotifications(selection, true /* clearable */); } public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) { - // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer - // visibility in the refactored code + // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the silent + // section clear action in the new stack. boolean hasAlertingMatchingClearable = isClearable ? mNotifStats.getHasClearableAlertingNotifs() : mNotifStats.getHasNonClearableAlertingNotifs(); @@ -1558,7 +1572,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { boolean remoteInputActive) { mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive); entry.notifyHeightChanged(true /* needsAnimation */); - updateFooter(); + if (!FooterViewRefactor.isEnabled()) { + updateFooter(); + } } public void lockScrollTo(NotificationEntry entry) { @@ -1573,6 +1589,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void updateFooter() { + FooterViewRefactor.assertInLegacyMode(); Trace.beginSection("NSSLC.updateFooter"); mView.updateFooter(); Trace.endSection(); @@ -2134,19 +2151,16 @@ public class NotificationStackScrollLayoutController implements Dumpable { private class NotifStackControllerImpl implements NotifStackController { @Override public void setNotifStats(@NonNull NotifStats notifStats) { - // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once footer visibility - // is handled in the refactored stack. + // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the silent + // section clear action in the new stack. mNotifStats = notifStats; if (!FooterViewRefactor.isEnabled()) { mView.setHasFilteredOutSeenNotifications( mSeenNotificationsInteractor .getHasFilteredOutSeenNotifications().getValue()); - } - updateFooter(); - - if (!FooterViewRefactor.isEnabled()) { + updateFooter(); updateShowEmptyShadeView(); updateImportantForAccessibility(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 664a6b6c54c7..15fde0ed49ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -33,6 +33,7 @@ import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.SourceType; +import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -137,7 +138,7 @@ public class StackScrollAlgorithm { } private void updateAlphaState(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { for (ExpandableView view : algorithmState.visibleChildren) { final ViewState viewState = view.getViewState(); final boolean isHunGoingToShade = ambientState.isShadeExpanded() @@ -295,7 +296,7 @@ public class StackScrollAlgorithm { } private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState, - int speedBumpIndex) { + int speedBumpIndex) { int childCount = algorithmState.visibleChildren.size(); int belowSpeedBump = speedBumpIndex; for (int i = 0; i < childCount; i++) { @@ -322,7 +323,7 @@ public class StackScrollAlgorithm { } private void updateClipping(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { float drawStart = ambientState.isOnKeyguard() ? 0 : ambientState.getStackY() - ambientState.getScrollY(); float clipStart = 0; @@ -454,7 +455,7 @@ public class StackScrollAlgorithm { } private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, - ExpandableView v) { + ExpandableView v) { ExpandableViewState viewState = v.getViewState(); viewState.notGoneIndex = notGoneIndex; state.visibleChildren.add(v); @@ -480,7 +481,7 @@ public class StackScrollAlgorithm { * @param ambientState The current ambient state */ protected void updatePositionsForState(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { if (!ambientState.isOnKeyguard() || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) { algorithmState.mCurrentYPosition += mNotificationScrimPadding; @@ -494,7 +495,7 @@ public class StackScrollAlgorithm { } private void setLocation(ExpandableViewState expandableViewState, float currentYPosition, - int i) { + int i) { expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; if (currentYPosition <= 0) { expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; @@ -598,18 +599,31 @@ public class StackScrollAlgorithm { viewEnd, /* hunMax */ ambientState.getMaxHeadsUpTranslation() ); if (view instanceof FooterView) { - final boolean shadeClosed = !ambientState.isShadeExpanded(); - final boolean isShelfShowing = algorithmState.firstViewInShelf != null; - if (shadeClosed) { - viewState.hidden = true; - } else { + if (FooterViewRefactor.isEnabled()) { final float footerEnd = algorithmState.mCurrentExpandedYPosition + view.getIntrinsicHeight(); final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight(); + // TODO(b/293167744): May be able to keep only noSpaceForFooter here if we add an + // emission when clearAllNotifications is called, and then use that in the footer + // visibility flow. ((FooterView.FooterViewState) viewState).hideContent = - isShelfShowing || noSpaceForFooter - || (ambientState.isClearAllInProgress() + noSpaceForFooter || (ambientState.isClearAllInProgress() && !hasNonClearableNotifs(algorithmState)); + + } else { + final boolean shadeClosed = !ambientState.isShadeExpanded(); + final boolean isShelfShowing = algorithmState.firstViewInShelf != null; + if (shadeClosed) { + viewState.hidden = true; + } else { + final float footerEnd = algorithmState.mCurrentExpandedYPosition + + view.getIntrinsicHeight(); + final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight(); + ((FooterView.FooterViewState) viewState).hideContent = + isShelfShowing || noSpaceForFooter + || (ambientState.isClearAllInProgress() + && !hasNonClearableNotifs(algorithmState)); + } } } else { if (view instanceof EmptyShadeView) { @@ -731,7 +745,7 @@ public class StackScrollAlgorithm { @VisibleForTesting void updatePulsingStates(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); ExpandableNotificationRow pulsingRow = null; for (int i = 0; i < childCount; i++) { @@ -761,7 +775,7 @@ public class StackScrollAlgorithm { } private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); // Move the tracked heads up into position during the appear animation, by interpolating @@ -870,18 +884,18 @@ public class StackScrollAlgorithm { boolean shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible, boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard) { return mustStayOnScreen && !headsUpIsVisible - && !showingPulsing - && (!isOnKeyguard || headsUpOnKeyguard); + && !showingPulsing + && (!isOnKeyguard || headsUpOnKeyguard); } - /** + /** * When shade is open and we are scrolled to the bottom of notifications, * clamp incoming HUN in its collapsed form, right below qs offset. * Transition pinned collapsed HUN to full height when scrolling back up. */ @VisibleForTesting void clampHunToTop(float clampInset, float stackTranslation, float collapsedHeight, - ExpandableViewState viewState) { + ExpandableViewState viewState) { final float newTranslation = Math.max(clampInset + stackTranslation, viewState.getYTranslation()); @@ -896,7 +910,7 @@ public class StackScrollAlgorithm { // Pin HUN to bottom of expanded QS // while the rest of notifications are scrolled offscreen. private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, - ExpandableViewState childState) { + ExpandableViewState childState) { float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation(); final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding() + ambientState.getStackTranslation(); @@ -919,7 +933,7 @@ public class StackScrollAlgorithm { @VisibleForTesting float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY, - float viewMaxHeight, float originalCornerRadius) { + float viewMaxHeight, float originalCornerRadius) { // Compute y where corner roundness should be in its original unpinned state. // We use view max height because the pinned collapsed HUN expands to max height @@ -948,7 +962,7 @@ public class StackScrollAlgorithm { * @param ambientState The ambient state of the algorithm */ private void updateZValuesForState(StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); float childrenOnTop = 0.0f; @@ -976,13 +990,13 @@ public class StackScrollAlgorithm { * vertically top of screen. Top HUNs should have drop shadows * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height - * that overlaps with QQS Panel. The integer part represents the count of - * previous HUNs whose Z positions are greater than 0. + * that overlaps with QQS Panel. The integer part represents the count of + * previous HUNs whose Z positions are greater than 0. */ protected float updateChildZValue(int i, float childrenOnTop, - StackScrollAlgorithmState algorithmState, - AmbientState ambientState, - boolean isTopHun) { + StackScrollAlgorithmState algorithmState, + AmbientState ambientState, + boolean isTopHun) { ExpandableView child = algorithmState.visibleChildren.get(i); ExpandableViewState childViewState = child.getViewState(); float baseZ = ambientState.getBaseZHeight(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt index 311ba83e85f1..9efe632f5dbb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt @@ -47,4 +47,11 @@ class NotificationStackAppearanceRepository @Inject constructor() { * further. */ val scrolledToTop = MutableStateFlow(true) + + /** + * The amount in px that the notification stack should scroll due to internal expansion. This + * should only happen when a notification expansion hits the bottom of the screen, so it is + * necessary to scroll up to keep expanding the notification. + */ + val syntheticScroll = MutableStateFlow(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 9984ba9c32ce..08df47388556 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository import javax.inject.Inject +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -50,6 +51,13 @@ constructor( */ val scrolledToTop: StateFlow<Boolean> = repository.scrolledToTop.asStateFlow() + /** + * The amount in px that the notification stack should scroll due to internal expansion. This + * should only happen when a notification expansion hits the bottom of the screen, so it is + * necessary to scroll up to keep expanding the notification. + */ + val syntheticScroll: Flow<Float> = repository.syntheticScroll.asStateFlow() + /** Sets the position of the notification stack in the current scene. */ fun setStackBounds(bounds: NotificationContainerBounds) { check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" } @@ -70,4 +78,9 @@ constructor( fun setScrolledToTop(scrolledToTop: Boolean) { repository.scrolledToTop.value = scrolledToTop } + + /** Sets the amount (px) that the notification stack should scroll due to internal expansion. */ + fun setSyntheticScroll(delta: Float) { + repository.syntheticScroll.value = delta + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 4d65b9d9c792..883aa9ba668b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -18,11 +18,10 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.view.LayoutInflater import androidx.lifecycle.lifecycleScope -import com.android.app.tracing.traceSection +import com.android.app.tracing.TraceUtils.traceAsync import com.android.internal.logging.MetricsLogger import com.android.internal.logging.nano.MetricsProto import com.android.systemui.common.ui.ConfigurationState -import com.android.systemui.common.ui.reinflateAndBindLatest import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.lifecycle.repeatWhenAttached @@ -33,6 +32,7 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder +import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder @@ -43,12 +43,18 @@ import com.android.systemui.statusbar.notification.stack.ui.view.NotificationSta import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel import com.android.systemui.statusbar.phone.NotificationIconAreaController +import com.android.systemui.util.kotlin.awaitCancellationThenDispose import com.android.systemui.util.kotlin.getOrNull +import com.android.systemui.util.ui.isAnimating +import com.android.systemui.util.ui.value import java.util.Optional import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch /** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */ @@ -83,7 +89,7 @@ constructor( bindHideList(viewController, viewModel, hiderTracker) if (FooterViewRefactor.isEnabled) { - launch { bindFooter(view) } + launch { reinflateAndBindFooter(view) } launch { bindEmptyShade(view) } launch { viewModel.isImportantForAccessibility.collect { isImportantForAccessibility @@ -108,42 +114,61 @@ constructor( ) } - private suspend fun bindFooter(parentView: NotificationStackScrollLayout) { + private suspend fun reinflateAndBindFooter(parentView: NotificationStackScrollLayout) { viewModel.footer.getOrNull()?.let { footerViewModel -> // The footer needs to be re-inflated every time the theme or the font size changes. - configuration.reinflateAndBindLatest( - R.layout.status_bar_notification_footer, - parentView, - attachToRoot = false, - backgroundDispatcher, - ) { footerView: FooterView -> - traceSection("bind FooterView") { - val disposableHandle = - FooterViewBinder.bindWhileAttached( - footerView, - footerViewModel, - clearAllNotifications = { - metricsLogger.action( - MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES - ) - parentView.clearAllNotifications() - }, - launchNotificationSettings = { view -> - notificationActivityStarter - .get() - .startHistoryIntent(view, /* showHistory = */ false) - }, - launchNotificationHistory = { view -> - notificationActivityStarter - .get() - .startHistoryIntent(view, /* showHistory = */ true) - }, - ) - parentView.setFooterView(footerView) - return@reinflateAndBindLatest disposableHandle + configuration + .inflateLayout<FooterView>( + R.layout.status_bar_notification_footer, + parentView, + attachToRoot = false, + ) + .flowOn(backgroundDispatcher) + .collectLatest { footerView: FooterView -> + traceAsync("bind FooterView") { + parentView.setFooterView(footerView) + bindFooter(footerView, footerViewModel, parentView) + } } + } + } + + /** + * Binds the footer (including its visibility) and dispose of the [DisposableHandle] when done. + */ + private suspend fun bindFooter( + footerView: FooterView, + footerViewModel: FooterViewModel, + parentView: NotificationStackScrollLayout + ): Unit = coroutineScope { + val disposableHandle = + FooterViewBinder.bindWhileAttached( + footerView, + footerViewModel, + clearAllNotifications = { + metricsLogger.action(MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES) + parentView.clearAllNotifications() + }, + launchNotificationSettings = { view -> + notificationActivityStarter + .get() + .startHistoryIntent(view, /* showHistory = */ false) + }, + launchNotificationHistory = { view -> + notificationActivityStarter + .get() + .startHistoryIntent(view, /* showHistory = */ true) + }, + ) + launch { + viewModel.shouldShowFooterView.collect { animatedVisibility -> + footerView.setVisible( + /* visible = */ animatedVisibility.value, + /* animate = */ animatedVisibility.isAnimating, + ) } } + disposableHandle.awaitCancellationThenDispose() } private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt index 814146c329a6..a1577854e732 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt @@ -69,6 +69,9 @@ object NotificationStackAppearanceViewBinder { controller.setMaxAlphaForExpansion( ((expandFraction - 0.5f) / 0.5f).coerceAtLeast(0f) ) + if (expandFraction == 0f || expandFraction == 1f) { + controller.onExpansionStopped() + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt index 86c0a67868e1..a6c658676687 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt @@ -16,21 +16,32 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel +import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.util.kotlin.combine +import com.android.systemui.util.kotlin.sample +import com.android.systemui.util.ui.AnimatableEvent +import com.android.systemui.util.ui.AnimatedValue +import com.android.systemui.util.ui.toAnimatedValueFlow import java.util.Optional import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart /** ViewModel for the list of notifications. */ @@ -42,9 +53,13 @@ constructor( val footer: Optional<FooterViewModel>, val logger: Optional<NotificationLoggerViewModel>, activeNotificationsInteractor: ActiveNotificationsInteractor, + keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, + powerInteractor: PowerInteractor, + remoteInputInteractor: RemoteInputInteractor, seenNotificationsInteractor: SeenNotificationsInteractor, shadeInteractor: ShadeInteractor, + userSetupInteractor: UserSetupInteractor, zenModeInteractor: ZenModeInteractor, ) { /** @@ -76,6 +91,10 @@ constructor( combine( activeNotificationsInteractor.areAnyNotificationsPresent, shadeInteractor.isQsFullscreen, + // TODO(b/293167744): It looks like we're essentially trying to check the same + // things for the empty shade visibility as we do for the footer, just in a + // slightly different way. We should change this so we also check + // statusBarState and isAwake instead of specific keyguard transitions. keyguardTransitionInteractor.isInTransitionToState(KeyguardState.AOD).onStart { emit(false) }, @@ -97,6 +116,80 @@ constructor( } } + val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy { + if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(AnimatedValue.NotAnimating(false)) + } else { + combine( + activeNotificationsInteractor.areAnyNotificationsPresent, + userSetupInteractor.isUserSetUp, + keyguardInteractor.statusBarState.map { it == StatusBarState.KEYGUARD }, + shadeInteractor.qsExpansion, + shadeInteractor.isQsFullscreen, + powerInteractor.isAsleep, + remoteInputInteractor.isRemoteInputActive, + shadeInteractor.shadeExpansion.map { it == 0f } + ) { + hasNotifications, + isUserSetUp, + isOnKeyguard, + qsExpansion, + qsFullScreen, + isAsleep, + isRemoteInputActive, + isShadeClosed -> + Pair( + // Should the footer be visible? + when { + !hasNotifications -> false + // Hide the footer until the user setup is complete, to prevent access + // to settings (b/193149550). + !isUserSetUp -> false + // Do not show the footer if the lockscreen is visible (incl. AOD), + // except if the shade is opened on top. See also b/219680200. + isOnKeyguard -> false + // Make sure we're not showing the footer in the transition to AOD while + // going to sleep (b/190227875). The StatusBarState is unfortunately not + // updated quickly enough when the power button is pressed, so this is + // necessary in addition to the isOnKeyguard check. + isAsleep -> false + // Do not show the footer if quick settings are fully expanded (except + // for the foldable split shade view). See b/201427195 && b/222699879. + qsExpansion == 1f && qsFullScreen -> false + // Hide the footer if remote input is active (i.e. user is replying to a + // notification). See b/75984847. + isRemoteInputActive -> false + // Never show the footer if the shade is collapsed (e.g. when HUNing). + isShadeClosed -> false + else -> true + }, + // This could in theory be in the .sample below, but it tends to be + // inconsistent, so we're passing it on to make sure we have the same state. + isOnKeyguard + ) + } + .distinctUntilChanged() + // Should we animate the visibility change? + .sample( + // TODO(b/322167853): This check is currently duplicated in FooterViewModel, + // but instead it should be a field in ShadeAnimationInteractor. + combine( + shadeInteractor.isShadeFullyExpanded, + shadeInteractor.isShadeTouchable, + ::Pair + ) + .onStart { emit(Pair(false, false)) } + ) { (visible, isOnKeyguard), (isShadeFullyExpanded, animationsEnabled) -> + // Animate if the shade is interactive, but NOT on the lockscreen. Having + // animations enabled while on the lockscreen makes the footer appear briefly + // when transitioning between the shade and keyguard. + val shouldAnimate = isShadeFullyExpanded && animationsEnabled && !isOnKeyguard + AnimatableEvent(visible, shouldAnimate) + } + .toAnimatedValueFlow() + } + } + // TODO(b/308591475): This should be tracked separately by the empty shade. val areNotificationsHiddenInShade: Flow<Boolean> by lazy { if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt index bdf1a6431549..3a0f03f70e1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt @@ -28,6 +28,7 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */ @SysUISingleton @@ -45,31 +46,32 @@ constructor( */ val expandFraction: Flow<Float> = combine( - shadeInteractor.shadeExpansion, - sceneInteractor.transitionState, - ) { shadeExpansion, transitionState -> - when (transitionState) { - is ObservableTransitionState.Idle -> { - if (transitionState.scene == SceneKey.Lockscreen) { - 1f - } else { - shadeExpansion + shadeInteractor.shadeExpansion, + sceneInteractor.transitionState, + ) { shadeExpansion, transitionState -> + when (transitionState) { + is ObservableTransitionState.Idle -> { + if (transitionState.scene == SceneKey.Lockscreen) { + 1f + } else { + shadeExpansion + } } - } - is ObservableTransitionState.Transition -> { - if ( - (transitionState.fromScene == SceneKey.Shade && - transitionState.toScene == SceneKey.QuickSettings) || - (transitionState.fromScene == SceneKey.QuickSettings && - transitionState.toScene == SceneKey.Shade) - ) { - 1f - } else { - shadeExpansion + is ObservableTransitionState.Transition -> { + if ( + (transitionState.fromScene == SceneKey.Shade && + transitionState.toScene == SceneKey.QuickSettings) || + (transitionState.fromScene == SceneKey.QuickSettings && + transitionState.toScene == SceneKey.Shade) + ) { + 1f + } else { + shadeExpansion + } } } } - } + .distinctUntilChanged() /** The bounds of the notification stack in the current scene. */ val stackBounds: Flow<NotificationContainerBounds> = stackAppearanceInteractor.stackBounds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 65d9c9f523f8..7ac5cd48f24a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -86,6 +86,13 @@ constructor( */ val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion + /** + * The amount in px that the notification stack should scroll due to internal expansion. This + * should only happen when a notification expansion hits the bottom of the screen, so it is + * necessary to scroll up to keep expanding the notification. + */ + val syntheticScroll: Flow<Float> = interactor.syntheticScroll + /** Sets the y-coord in px of the top of the contents of the notification stack. */ fun onContentTopChanged(padding: Float) { interactor.setContentTop(padding) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index 8a56da3dafab..b49af0e64772 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -837,7 +837,7 @@ constructor( if (animationController == null) { return null } - val rootView = animationController.launchContainer.rootView + val rootView = animationController.transitionContainer.rootView val controllerFromStatusBar: Optional<ActivityLaunchAnimator.Controller> = statusBarWindowController.wrapAnimationControllerIfInStatusBar( rootView, @@ -881,8 +881,8 @@ constructor( } } - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { - super.onLaunchAnimationStart(isExpandingFullyAbove) + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + super.onTransitionAnimationStart(isExpandingFullyAbove) // Double check that the keyguard is still showing and not going // away, but if so set the keyguard occluded. Typically, WM will let @@ -902,14 +902,14 @@ constructor( } } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { // Set mIsLaunchingActivityOverLockscreen to false before actually // finishing the animation so that we can assume that // mIsLaunchingActivityOverLockscreen being true means that we will // collapse the shade (or at least run the post collapse runnables) // later on. centralSurfaces?.setIsLaunchingActivityOverLockscreen(false) - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) } override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index be5c6b3ba069..8e3d678c152a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -349,8 +349,12 @@ public class NotificationIconContainer extends ViewGroup { } } if (child instanceof StatusBarIconView) { - ((StatusBarIconView) child).updateIconDimens(); - if (!NotificationIconContainerRefactor.isEnabled()) { + if (NotificationIconContainerRefactor.isEnabled()) { + if (!mChangingViewPositions) { + ((StatusBarIconView) child).updateIconDimens(); + } + } else { + ((StatusBarIconView) child).updateIconDimens(); ((StatusBarIconView) child).setDozing(mDozing, false, 0); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt index 8ca5bfc519fc..d43f4709a32d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt @@ -2,7 +2,7 @@ package com.android.systemui.statusbar.phone import android.view.View import com.android.systemui.animation.ActivityLaunchAnimator -import com.android.systemui.animation.LaunchAnimator +import com.android.systemui.animation.TransitionAnimator import com.android.systemui.shade.ShadeController import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor @@ -34,8 +34,8 @@ class StatusBarLaunchAnimatorController( } } - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { - delegate.onLaunchAnimationStart(isExpandingFullyAbove) + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationStart(isExpandingFullyAbove) shadeAnimationInteractor.setIsLaunchingActivity(true) if (!isExpandingFullyAbove) { shadeViewController.collapseWithDuration( @@ -43,18 +43,18 @@ class StatusBarLaunchAnimatorController( } } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { - delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) shadeAnimationInteractor.setIsLaunchingActivity(false) shadeController.onLaunchAnimationEnd(isExpandingFullyAbove) } - override fun onLaunchAnimationProgress( - state: LaunchAnimator.State, + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, progress: Float, linearProgress: Float ) { - delegate.onLaunchAnimationProgress(state, progress, linearProgress) + delegate.onTransitionAnimationProgress(state, progress, linearProgress) shadeViewController.applyLaunchAnimationProgress(linearProgress) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt index 246645ee0ead..72f45406b35e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt @@ -15,20 +15,14 @@ */ package com.android.systemui.statusbar.phone.domain.interactor -import android.graphics.Rect import com.android.systemui.statusbar.phone.data.repository.DarkIconRepository +import com.android.systemui.statusbar.phone.domain.model.DarkState import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map /** States pertaining to calculating colors for icons in dark mode. */ class DarkIconInteractor @Inject constructor(repository: DarkIconRepository) { - /** @see com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange.areas */ - val tintAreas: Flow<Collection<Rect>> = repository.darkState.map { it.areas } - /** - * @see com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange.darkIntensity - */ - val darkIntensity: Flow<Float> = repository.darkState.map { it.darkIntensity } - /** @see com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange.tint */ - val tintColor: Flow<Int> = repository.darkState.map { it.tint } + /** Dark-mode state for tinting icons. */ + val darkState: Flow<DarkState> = repository.darkState.map { DarkState(it.areas, it.tint) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt new file mode 100644 index 000000000000..3cab7cf859b4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone.domain.model + +import android.graphics.Rect + +/** Dark mode visual states. */ +data class DarkState( + /** Areas on screen that require a dark-mode adjustment. */ + val areas: Collection<Rect>, + /** Tint color to apply to UI elements that fall within [areas]. */ + val tint: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt index 63566ee814ab..e1798d3d1c0f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.satellite.ui.model +import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState @@ -36,7 +37,10 @@ object SatelliteIconModel { SatelliteConnectionState.On -> Icon.Resource( res = R.drawable.ic_satellite_not_connected, - contentDescription = null, + contentDescription = + ContentDescription.Resource( + R.string.accessibility_status_bar_satellite_available + ), ) SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength) } @@ -51,15 +55,36 @@ object SatelliteIconModel { // TODO(b/316634365): these need content descriptions when (signalStrength) { // No signal - 0 -> Icon.Resource(res = R.drawable.ic_satellite_connected_0, contentDescription = null) + 0 -> + Icon.Resource( + res = R.drawable.ic_satellite_connected_0, + contentDescription = + ContentDescription.Resource( + R.string.accessibility_status_bar_satellite_no_connection + ) + ) // Poor -> Moderate 1, - 2 -> Icon.Resource(res = R.drawable.ic_satellite_connected_1, contentDescription = null) + 2 -> + Icon.Resource( + res = R.drawable.ic_satellite_connected_1, + contentDescription = + ContentDescription.Resource( + R.string.accessibility_status_bar_satellite_poor_connection + ) + ) // Good -> Great 3, - 4 -> Icon.Resource(res = R.drawable.ic_satellite_connected_2, contentDescription = null) + 4 -> + Icon.Resource( + res = R.drawable.ic_satellite_connected_2, + contentDescription = + ContentDescription.Resource( + R.string.accessibility_status_bar_satellite_good_connection + ) + ) else -> null } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java index b59878253aa8..21d3fa47f51c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java @@ -47,13 +47,13 @@ import android.view.WindowInsets; import android.view.WindowManager; import com.android.internal.policy.SystemBarUtils; -import com.android.systemui.res.R; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DelegateLaunchAnimatorController; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener; @@ -194,17 +194,17 @@ public class StatusBarWindowController { return Optional.empty(); } - animationController.setLaunchContainer(mLaunchAnimationContainer); + animationController.setTransitionContainer(mLaunchAnimationContainer); return Optional.of(new DelegateLaunchAnimatorController(animationController) { @Override - public void onLaunchAnimationStart(boolean isExpandingFullyAbove) { - getDelegate().onLaunchAnimationStart(isExpandingFullyAbove); + public void onTransitionAnimationStart(boolean isExpandingFullyAbove) { + getDelegate().onTransitionAnimationStart(isExpandingFullyAbove); setLaunchAnimationRunning(true); } @Override - public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) { - getDelegate().onLaunchAnimationEnd(isExpandingFullyAbove); + public void onTransitionAnimationEnd(boolean isExpandingFullyAbove) { + getDelegate().onTransitionAnimationEnd(isExpandingFullyAbove); setLaunchAnimationRunning(false); } }); diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt new file mode 100644 index 000000000000..5c53ff98b777 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt @@ -0,0 +1,153 @@ +/* + * 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.unfold + +import android.animation.ValueAnimator +import android.annotation.BinderThread +import android.content.Context +import android.os.Handler +import android.os.SystemProperties +import android.util.Log +import android.view.animation.DecelerateInterpolator +import androidx.core.animation.addListener +import com.android.internal.foldables.FoldLockSettingAvailabilityProvider +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DeviceStateRepository +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState +import com.android.systemui.statusbar.LinearSideLightRevealEffect +import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_OPAQUE +import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_TRANSPARENT +import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation +import com.android.systemui.unfold.dagger.UnfoldBg +import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import javax.inject.Inject +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout + +class FoldLightRevealOverlayAnimation +@Inject +constructor( + private val context: Context, + @UnfoldBg private val bgHandler: Handler, + private val deviceStateRepository: DeviceStateRepository, + private val powerInteractor: PowerInteractor, + @Background private val applicationScope: CoroutineScope, + private val animationStatusRepository: AnimationStatusRepository, + private val controllerFactory: FullscreenLightRevealAnimationController.Factory +) : FullscreenLightRevealAnimation { + + private val revealProgressValueAnimator: ValueAnimator = + ValueAnimator.ofFloat(ALPHA_OPAQUE, ALPHA_TRANSPARENT) + private lateinit var controller: FullscreenLightRevealAnimationController + @Volatile private var readyCallback: CompletableDeferred<Runnable>? = null + + override fun init() { + // This method will be called only on devices where this animation is enabled, + // so normally this thread won't be created + if (!FoldLockSettingAvailabilityProvider(context.resources).isFoldLockBehaviorAvailable) { + return + } + + controller = + controllerFactory.create( + displaySelector = { minByOrNull { it.naturalWidth } }, + effectFactory = { LinearSideLightRevealEffect(it.isVerticalRotation()) }, + overlayContainerName = SURFACE_CONTAINER_NAME + ) + controller.init() + + applicationScope.launch(bgHandler.asCoroutineDispatcher()) { + powerInteractor.screenPowerState.collect { + if (it == ScreenPowerState.SCREEN_ON) { + readyCallback = null + } + } + } + + applicationScope.launch(bgHandler.asCoroutineDispatcher()) { + deviceStateRepository.state + .map { it != DeviceStateRepository.DeviceState.FOLDED } + .distinctUntilChanged() + .filter { isUnfolded -> isUnfolded } + .collect { controller.ensureOverlayRemoved() } + } + + applicationScope.launch(bgHandler.asCoroutineDispatcher()) { + deviceStateRepository.state + .filter { + animationStatusRepository.areAnimationsEnabled().first() && + it == DeviceStateRepository.DeviceState.FOLDED + } + .collect { + try { + withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) { + readyCallback = CompletableDeferred() + val onReady = readyCallback?.await() + readyCallback = null + controller.addOverlay(ALPHA_OPAQUE, onReady) + waitForScreenTurnedOn() + playFoldLightRevealOverlayAnimation() + } + } catch (e: TimeoutCancellationException) { + Log.e(TAG, "Fold light reveal animation timed out") + ensureOverlayRemovedInternal() + } + } + } + } + + @BinderThread + override fun onScreenTurningOn(onOverlayReady: Runnable) { + readyCallback?.complete(onOverlayReady) ?: onOverlayReady.run() + } + + private suspend fun waitForScreenTurnedOn() { + powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() + } + + private fun ensureOverlayRemovedInternal() { + revealProgressValueAnimator.cancel() + controller.ensureOverlayRemoved() + } + + private fun playFoldLightRevealOverlayAnimation() { + revealProgressValueAnimator.duration = ANIMATION_DURATION + revealProgressValueAnimator.interpolator = DecelerateInterpolator() + revealProgressValueAnimator.addUpdateListener { animation -> + controller.updateRevealAmount(animation.animatedFraction) + } + revealProgressValueAnimator.addListener(onEnd = { controller.ensureOverlayRemoved() }) + revealProgressValueAnimator.start() + } + + private companion object { + const val TAG = "FoldLightRevealOverlayAnimation" + const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L + const val SURFACE_CONTAINER_NAME = "fold-overlay-container" + val ANIMATION_DURATION: Long + get() = SystemProperties.getLong("persist.fold_animation_duration", 200L) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt new file mode 100644 index 000000000000..668b1439abab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt @@ -0,0 +1,271 @@ +/* + * 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.unfold + +import android.content.Context +import android.graphics.PixelFormat +import android.hardware.display.DisplayManager +import android.os.Handler +import android.os.Looper +import android.os.Trace +import android.view.Choreographer +import android.view.Display +import android.view.DisplayInfo +import android.view.Surface +import android.view.Surface.Rotation +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.SurfaceSession +import android.view.WindowManager +import android.view.WindowlessWindowManager +import com.android.app.tracing.traceSection +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.DisplayTracker +import com.android.systemui.statusbar.LightRevealEffect +import com.android.systemui.statusbar.LightRevealScrim +import com.android.systemui.unfold.dagger.UnfoldBg +import com.android.systemui.unfold.updates.RotationChangeProvider +import com.android.systemui.util.concurrency.ThreadFactory +import com.android.wm.shell.displayareahelper.DisplayAreaHelper +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.lang.IllegalArgumentException +import java.util.Optional +import java.util.concurrent.Executor +import java.util.function.Consumer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch + +interface FullscreenLightRevealAnimation { + fun init() + + fun onScreenTurningOn(onOverlayReady: Runnable) +} + +class FullscreenLightRevealAnimationController +@AssistedInject +constructor( + private val context: Context, + private val displayManager: DisplayManager, + private val threadFactory: ThreadFactory, + @UnfoldBg private val bgHandler: Handler, + @UnfoldBg private val rotationChangeProvider: RotationChangeProvider, + private val displayAreaHelper: Optional<DisplayAreaHelper>, + private val displayTracker: DisplayTracker, + @Background private val applicationScope: CoroutineScope, + @Main private val executor: Executor, + @Assisted private val displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?, + @Assisted private val lightRevealEffectFactory: (rotation: Int) -> LightRevealEffect, + @Assisted private val overlayContainerName: String +) { + + private lateinit var bgExecutor: Executor + private lateinit var wwm: WindowlessWindowManager + + private var currentRotation: Int = context.display.rotation + private var root: SurfaceControlViewHost? = null + private var scrimView: LightRevealScrim? = null + + private val rotationWatcher = RotationWatcher() + private val internalDisplayInfos: Sequence<DisplayInfo> + get() = + displayManager + .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) + .asSequence() + .map { DisplayInfo().apply { it.getDisplayInfo(this) } } + .filter { it.type == Display.TYPE_INTERNAL } + + var isTouchBlocked: Boolean = false + set(value) { + if (value != field) { + traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) } + field = value + } + } + + fun init() { + bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler) + rotationChangeProvider.addCallback(rotationWatcher) + + buildSurface { builder -> + applicationScope.launch(executor.asCoroutineDispatcher()) { + val overlayContainer = builder.build() + + SurfaceControl.Transaction() + .setLayer(overlayContainer, OVERLAY_LAYER_Z_INDEX) + .show(overlayContainer) + .apply() + + wwm = + WindowlessWindowManager(context.resources.configuration, overlayContainer, null) + } + } + } + + fun addOverlay( + initialAlpha: Float, + onOverlayReady: Runnable? = null, + ) { + if (!::wwm.isInitialized) { + // Surface overlay is not created yet on the first SysUI launch + onOverlayReady?.run() + return + } + ensureInBackground() + ensureOverlayRemoved() + prepareOverlay(onOverlayReady, wwm, bgExecutor, initialAlpha) + } + + fun ensureOverlayRemoved() { + ensureInBackground() + + traceSection("ensureOverlayRemoved") { + root?.release() + root = null + scrimView = null + } + } + + fun isOverlayVisible(): Boolean { + return scrimView == null + } + + fun updateRevealAmount(revealAmount: Float) { + scrimView?.revealAmount = revealAmount + } + + private fun buildSurface(onUpdated: Consumer<SurfaceControl.Builder>) { + val containerBuilder = + SurfaceControl.Builder(SurfaceSession()) + .setContainerLayer() + .setName(overlayContainerName) + + displayAreaHelper + .get() + .attachToRootDisplayArea(displayTracker.defaultDisplayId, containerBuilder, onUpdated) + } + + private fun prepareOverlay( + onOverlayReady: Runnable? = null, + wwm: WindowlessWindowManager, + bgExecutor: Executor, + initialAlpha: Float, + ) { + val newRoot = SurfaceControlViewHost(context, context.display, wwm, javaClass.simpleName) + + val params = getLayoutParams() + val newView = + LightRevealScrim( + context, + attrs = null, + initialWidth = params.width, + initialHeight = params.height + ) + .apply { + revealEffect = lightRevealEffectFactory(currentRotation) + revealAmount = initialAlpha + } + + newRoot.setView(newView, params) + + if (onOverlayReady != null) { + Trace.beginAsyncSection("$TAG#relayout", 0) + + newRoot.relayout(params) { transaction -> + val vsyncId = Choreographer.getSfInstance().vsyncId + transaction.setFrameTimelineVsync(vsyncId).apply() + + transaction + .setFrameTimelineVsync(vsyncId + 1) + .addTransactionCommittedListener(bgExecutor) { + Trace.endAsyncSection("$TAG#relayout", 0) + onOverlayReady.run() + } + .apply() + } + } + root = newRoot + scrimView = newView + } + + private fun ensureInBackground() { + check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" } + } + + private fun getLayoutParams(): WindowManager.LayoutParams { + val displayInfo = + internalDisplayInfos.displaySelector() + ?: throw IllegalArgumentException("No internal displays found!") + return WindowManager.LayoutParams().apply { + if (currentRotation.isVerticalRotation()) { + height = displayInfo.naturalHeight + width = displayInfo.naturalWidth + } else { + height = displayInfo.naturalWidth + width = displayInfo.naturalHeight + } + format = PixelFormat.TRANSLUCENT + type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY + title = javaClass.simpleName + layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + fitInsetsTypes = 0 + + flags = + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + setTrustedOverlay() + + packageName = context.opPackageName + } + } + + private inner class RotationWatcher : RotationChangeProvider.RotationListener { + override fun onRotationChanged(newRotation: Int) { + traceSection("$TAG#onRotationChanged") { + if (currentRotation != newRotation) { + currentRotation = newRotation + scrimView?.revealEffect = lightRevealEffectFactory(currentRotation) + root?.relayout(getLayoutParams()) + } + } + } + } + + @AssistedFactory + interface Factory { + fun create( + displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?, + effectFactory: (rotation: Int) -> LightRevealEffect, + overlayContainerName: String + ): FullscreenLightRevealAnimationController + } + + companion object { + private const val TAG = "FullscreenLightRevealAnimation" + private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE + private const val OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1 + const val ALPHA_TRANSPARENT = 1f + const val ALPHA_OPAQUE = 0f + + fun @receiver:Rotation Int.isVerticalRotation(): Boolean = + this == Surface.ROTATION_0 || this == Surface.ROTATION_180 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt index 0016d95dff80..139ac7e4f687 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.unfold import com.android.keyguard.KeyguardUnfoldTransition +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.shade.NotificationPanelUnfoldAnimationController import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController @@ -25,10 +26,14 @@ import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityManager import com.android.systemui.util.kotlin.getOrNull +import dagger.Binds import dagger.BindsInstance import dagger.Module import dagger.Provides import dagger.Subcomponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet import java.util.Optional import javax.inject.Named import javax.inject.Scope @@ -70,8 +75,33 @@ class SysUIUnfoldModule { } } +@Module +interface SysUIUnfoldStartableModule { + @Binds + @IntoMap + @ClassKey(UnfoldInitializationStartable::class) + fun bindsUnfoldInitializationStartable(impl: UnfoldInitializationStartable): CoreStartable +} + +@Module +abstract class SysUIUnfoldInternalModule { + @Binds + @IntoSet + @SysUIUnfoldScope + abstract fun bindsUnfoldLightRevealOverlayAnimation( + anim: UnfoldLightRevealOverlayAnimation + ): FullscreenLightRevealAnimation + + @Binds + @IntoSet + @SysUIUnfoldScope + abstract fun bindsFoldLightRevealOverlayAnimation( + anim: FoldLightRevealOverlayAnimation + ): FullscreenLightRevealAnimation +} + @SysUIUnfoldScope -@Subcomponent +@Subcomponent(modules = [SysUIUnfoldInternalModule::class]) interface SysUIUnfoldComponent { @Subcomponent.Factory @@ -92,12 +122,12 @@ interface SysUIUnfoldComponent { fun getFoldAodAnimationController(): FoldAodAnimationController + fun getFullScreenLightRevealAnimations(): Set<FullscreenLightRevealAnimation> + fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController fun getUnfoldHapticsPlayer(): UnfoldHapticsPlayer - fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation - fun getUnfoldKeyguardVisibilityManager(): UnfoldKeyguardVisibilityManager fun getUnfoldLatencyTracker(): UnfoldLatencyTracker diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldInitializationStartable.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldInitializationStartable.kt new file mode 100644 index 000000000000..75d8a58102ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldInitializationStartable.kt @@ -0,0 +1,67 @@ +/* + * 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.unfold + +import com.android.systemui.CoreStartable +import com.android.systemui.Flags +import com.android.systemui.unfold.dagger.UnfoldBg +import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder +import java.util.Optional +import javax.inject.Inject + +class UnfoldInitializationStartable +@Inject +constructor( + private val unfoldComponentOptional: Optional<SysUIUnfoldComponent>, + private val foldStateLoggingProviderOptional: Optional<FoldStateLoggingProvider>, + private val foldStateLoggerOptional: Optional<FoldStateLogger>, + @UnfoldBg + private val unfoldBgTransitionProgressProviderOptional: + Optional<UnfoldTransitionProgressProvider>, + private val unfoldTransitionProgressProviderOptional: + Optional<UnfoldTransitionProgressProvider>, + private val unfoldTransitionProgressForwarder: Optional<UnfoldTransitionProgressForwarder> +) : CoreStartable { + override fun start() { + unfoldComponentOptional.ifPresent { c: SysUIUnfoldComponent -> + c.getFullScreenLightRevealAnimations().forEach { it: FullscreenLightRevealAnimation -> + it.init() + } + c.getUnfoldTransitionWallpaperController().init() + c.getUnfoldHapticsPlayer() + c.getNaturalRotationUnfoldProgressProvider().init() + c.getUnfoldLatencyTracker().init() + } + + foldStateLoggingProviderOptional.ifPresent { obj: FoldStateLoggingProvider -> obj.init() } + foldStateLoggerOptional.ifPresent { obj: FoldStateLogger -> obj.init() } + + val unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider> = + if (Flags.unfoldAnimationBackgroundProgress()) { + unfoldBgTransitionProgressProviderOptional + } else { + unfoldTransitionProgressProviderOptional + } + unfoldTransitionProgressProvider.ifPresent { + progressProvider: UnfoldTransitionProgressProvider -> + unfoldTransitionProgressForwarder.ifPresent { + listener: UnfoldTransitionProgressForwarder -> + progressProvider.addCallback(listener) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt index b72c6f189e3e..f355dd84e3cb 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt @@ -18,42 +18,22 @@ package com.android.systemui.unfold import android.annotation.BinderThread import android.content.ContentResolver import android.content.Context -import android.graphics.PixelFormat import android.hardware.devicestate.DeviceStateManager -import android.hardware.devicestate.DeviceStateManager.FoldStateListener -import android.hardware.display.DisplayManager import android.hardware.input.InputManagerGlobal import android.os.Handler -import android.os.Looper import android.os.Trace -import android.view.Choreographer -import android.view.Display -import android.view.DisplayInfo -import android.view.Surface -import android.view.SurfaceControl -import android.view.SurfaceControlViewHost -import android.view.SurfaceSession -import android.view.WindowManager -import android.view.WindowlessWindowManager -import com.android.app.tracing.traceSection -import com.android.keyguard.logging.ScrimLogger import com.android.systemui.Flags.unfoldAnimationBackgroundProgress -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags -import com.android.systemui.settings.DisplayTracker -import com.android.systemui.statusbar.LightRevealEffect -import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.LinearLightRevealEffect +import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_OPAQUE +import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_TRANSPARENT +import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.FOLD import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.UNFOLD -import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.dagger.UnfoldBg -import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled import com.android.systemui.util.concurrency.ThreadFactory -import com.android.wm.shell.displayareahelper.DisplayAreaHelper -import java.util.Optional import java.util.concurrent.Executor import java.util.function.Consumer import javax.inject.Inject @@ -64,80 +44,43 @@ class UnfoldLightRevealOverlayAnimation @Inject constructor( private val context: Context, - private val featureFlags: FeatureFlags, - private val deviceStateManager: DeviceStateManager, + private val featureFlags: FeatureFlagsClassic, private val contentResolver: ContentResolver, - private val displayManager: DisplayManager, + @UnfoldBg private val unfoldProgressHandler: Handler, @UnfoldBg private val unfoldTransitionBgProgressProvider: Provider<UnfoldTransitionProgressProvider>, private val unfoldTransitionProgressProvider: Provider<UnfoldTransitionProgressProvider>, - private val displayAreaHelper: Optional<DisplayAreaHelper>, - @Main private val executor: Executor, + private val deviceStateManager: DeviceStateManager, private val threadFactory: ThreadFactory, - @UnfoldBg private val rotationChangeProvider: RotationChangeProvider, - @UnfoldBg private val unfoldProgressHandler: Handler, - private val displayTracker: DisplayTracker, - private val scrimLogger: ScrimLogger, -) { + private val fullscreenLightRevealAnimationControllerFactory: + FullscreenLightRevealAnimationController.Factory +) : FullscreenLightRevealAnimation { private val transitionListener = TransitionListener() - private val rotationWatcher = RotationWatcher() - - private lateinit var bgHandler: Handler - private lateinit var bgExecutor: Executor - - private lateinit var wwm: WindowlessWindowManager - private lateinit var unfoldedDisplayInfo: DisplayInfo - private lateinit var overlayContainer: SurfaceControl - - private var root: SurfaceControlViewHost? = null - private var scrimView: LightRevealScrim? = null private var isFolded: Boolean = false private var isUnfoldHandled: Boolean = true - private var overlayAddReason: AddOverlayReason? = null - private var isTouchBlocked: Boolean = true - - private var currentRotation: Int = context.display!!.rotation + private var overlayAddReason: AddOverlayReason = UNFOLD + private lateinit var controller: FullscreenLightRevealAnimationController + private lateinit var bgExecutor: Executor - fun init() { + override fun init() { // This method will be called only on devices where this animation is enabled, // so normally this thread won't be created - bgHandler = unfoldProgressHandler - bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler) + controller = + fullscreenLightRevealAnimationControllerFactory.create( + displaySelector = { maxByOrNull { it.naturalWidth } }, + effectFactory = { LinearLightRevealEffect(it.isVerticalRotation()) }, + overlayContainerName = SURFACE_CONTAINER_NAME, + ) + controller.init() + bgExecutor = threadFactory.buildDelayableExecutorOnHandler(unfoldProgressHandler) deviceStateManager.registerCallback(bgExecutor, FoldListener()) if (unfoldAnimationBackgroundProgress()) { unfoldTransitionBgProgressProvider.get().addCallback(transitionListener) } else { unfoldTransitionProgressProvider.get().addCallback(transitionListener) } - rotationChangeProvider.addCallback(rotationWatcher) - - val containerBuilder = - SurfaceControl.Builder(SurfaceSession()) - .setContainerLayer() - .setName("unfold-overlay-container") - - displayAreaHelper.get().attachToRootDisplayArea( - displayTracker.defaultDisplayId, - containerBuilder - ) { builder -> - executor.execute { - overlayContainer = builder.build() - - SurfaceControl.Transaction() - .setLayer(overlayContainer, UNFOLD_OVERLAY_LAYER_Z_INDEX) - .show(overlayContainer) - .apply() - - wwm = - WindowlessWindowManager(context.resources.configuration, overlayContainer, null) - } - } - - // Get unfolded display size immediately as 'current display info' might be - // not up-to-date during unfolding - unfoldedDisplayInfo = getUnfoldedDisplayInfo() } /** @@ -148,17 +91,18 @@ constructor( * @see [com.android.systemui.keyguard.KeyguardViewMediator] */ @BinderThread - fun onScreenTurningOn(onOverlayReady: Runnable) { + override fun onScreenTurningOn(onOverlayReady: Runnable) { executeInBackground { Trace.beginSection("$TAG#onScreenTurningOn") try { // Add the view only if we are unfolding and this is the first screen on if (!isFolded && !isUnfoldHandled && contentResolver.areAnimationsEnabled()) { - addOverlay(onOverlayReady, reason = UNFOLD) + overlayAddReason = UNFOLD + controller.addOverlay(calculateRevealAmount(), onOverlayReady) isUnfoldHandled = true } else { // No unfold transition, immediately report that overlay is ready - ensureOverlayRemoved() + controller.ensureOverlayRemoved() onOverlayReady.run() } } finally { @@ -167,78 +111,15 @@ constructor( } } - private fun addOverlay(onOverlayReady: Runnable? = null, reason: AddOverlayReason) { - if (!::wwm.isInitialized) { - // Surface overlay is not created yet on the first SysUI launch - onOverlayReady?.run() - return - } - - ensureInBackground() - ensureOverlayRemoved() - - overlayAddReason = reason - - val newRoot = - SurfaceControlViewHost( - context, - context.display, - wwm, - "UnfoldLightRevealOverlayAnimation" - ) - val params = getLayoutParams() - val newView = - LightRevealScrim( - context, - attrs = null, - initialWidth = params.width, - initialHeight = params.height - ) - .apply { - revealEffect = createLightRevealEffect() - revealAmount = calculateRevealAmount() - scrimLogger = this@UnfoldLightRevealOverlayAnimation.scrimLogger - } - - newRoot.setView(newView, params) - - if (onOverlayReady != null) { - Trace.beginAsyncSection("$TAG#relayout", 0) - - newRoot.relayout(params) { transaction -> - val vsyncId = Choreographer.getSfInstance().vsyncId - - // Apply the transaction that contains the first frame of the overlay and apply - // another empty transaction with 'vsyncId + 1' to make sure that it is actually - // displayed on the screen. The second transaction is necessary to remove the screen - // blocker (turn on the brightness) only when the content is actually visible as it - // might be presented only in the next frame. - // See b/197538198 - transaction.setFrameTimelineVsync(vsyncId).apply() - - transaction - .setFrameTimelineVsync(vsyncId + 1) - .addTransactionCommittedListener(bgExecutor) { - Trace.endAsyncSection("$TAG#relayout", 0) - onOverlayReady.run() - } - .apply() - } - } - - scrimView = newView - root = newRoot - } - private fun calculateRevealAmount(animationProgress: Float? = null): Float { - val overlayAddReason = overlayAddReason ?: UNFOLD + val overlayAddReason = overlayAddReason if (animationProgress == null) { - // Animation progress is unknown, calculate the initial value based on the overlay + // Animation progress unknown, calculate the initial value based on the overlay // add reason return when (overlayAddReason) { - FOLD -> TRANSPARENT - UNFOLD -> BLACK + FOLD -> ALPHA_TRANSPARENT + UNFOLD -> ALPHA_OPAQUE } } @@ -249,144 +130,57 @@ constructor( // Do not darken the content when SHOW_VIGNETTE_WHEN_FOLDING flag is off // and we are folding the device. We still add the overlay to block touches // while the animation is running but the overlay is transparent. - TRANSPARENT + ALPHA_TRANSPARENT } else { animationProgress } } - private fun getLayoutParams(): WindowManager.LayoutParams { - val params: WindowManager.LayoutParams = WindowManager.LayoutParams() + private inner class TransitionListener : + UnfoldTransitionProgressProvider.TransitionProgressListener { - val rotation = currentRotation - val isNatural = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 - - params.height = - if (isNatural) unfoldedDisplayInfo.naturalHeight else unfoldedDisplayInfo.naturalWidth - params.width = - if (isNatural) unfoldedDisplayInfo.naturalWidth else unfoldedDisplayInfo.naturalHeight - - params.format = PixelFormat.TRANSLUCENT - params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY - params.title = "Unfold Light Reveal Animation" - params.layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS - params.fitInsetsTypes = 0 - - val touchFlags = - if (isTouchBlocked) { - // Touchable by default, so it will block the touches - 0 - } else { - WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - } - params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or touchFlags - params.setTrustedOverlay() - - val packageName: String = context.opPackageName - params.packageName = packageName - - return params - } - - private fun updateTouchBlockIfNeeded(progress: Float) { - // When unfolding unblock touches a bit earlier than the animation end as the - // interpolation has a long tail of very slight movement at the end which should not - // affect much the usage of the device - val shouldBlockTouches = - if (overlayAddReason == UNFOLD) { - progress < UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS - } else { - true - } - - if (isTouchBlocked != shouldBlockTouches) { - isTouchBlocked = shouldBlockTouches - - traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) } + override fun onTransitionProgress(progress: Float) = executeInBackground { + controller.updateRevealAmount(calculateRevealAmount(progress)) + // When unfolding unblock touches a bit earlier than the animation end as the + // interpolation has a long tail of very slight movement at the end which should not + // affect much the usage of the device + controller.isTouchBlocked = + overlayAddReason == FOLD || progress < UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS } - } - - private fun createLightRevealEffect(): LightRevealEffect { - val isVerticalFold = - currentRotation == Surface.ROTATION_0 || currentRotation == Surface.ROTATION_180 - return LinearLightRevealEffect(isVertical = isVerticalFold) - } - private fun ensureOverlayRemoved() { - ensureInBackground() - traceSection("ensureOverlayRemoved") { - root?.release() - root = null - scrimView = null - } - } - - private fun getUnfoldedDisplayInfo(): DisplayInfo = - displayManager - .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) - .asSequence() - .map { DisplayInfo().apply { it.getDisplayInfo(this) } } - .filter { it.type == Display.TYPE_INTERNAL } - .maxByOrNull { it.naturalWidth }!! - - private inner class TransitionListener : TransitionProgressListener { - - override fun onTransitionProgress(progress: Float) { - executeInBackground { - scrimView?.revealAmount = calculateRevealAmount(progress) - updateTouchBlockIfNeeded(progress) - } - } - - override fun onTransitionFinished() { - executeInBackground { ensureOverlayRemoved() } + override fun onTransitionFinished() = executeInBackground { + controller.ensureOverlayRemoved() } override fun onTransitionStarted() { // Add view for folding case (when unfolding the view is added earlier) - if (scrimView == null) { - executeInBackground { addOverlay(reason = FOLD) } + if (controller.isOverlayVisible()) { + executeInBackground { + overlayAddReason = FOLD + controller.addOverlay(calculateRevealAmount()) + } } // Disable input dispatching during transition. InputManagerGlobal.getInstance().cancelCurrentTouch() } } - private inner class RotationWatcher : RotationChangeProvider.RotationListener { - override fun onRotationChanged(newRotation: Int) { - executeInBackground { - traceSection("$TAG#onRotationChanged") { - if (currentRotation != newRotation) { - currentRotation = newRotation - scrimView?.revealEffect = createLightRevealEffect() - root?.relayout(getLayoutParams()) - } - } - } - } - } - private fun executeInBackground(f: () -> Unit) { // This is needed to allow progresses to be received both from the main thread (that will // schedule a runnable on the bg thread), and from the bg thread directly (no reposting). - if (bgHandler.looper.isCurrentThread) { + if (unfoldProgressHandler.looper.isCurrentThread) { f() } else { - bgHandler.post(f) + unfoldProgressHandler.post(f) } } - private fun ensureInBackground() { - check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" } - } - private inner class FoldListener : - FoldStateListener( + DeviceStateManager.FoldStateListener( context, Consumer { isFolded -> if (isFolded) { - ensureOverlayRemoved() + controller.ensureOverlayRemoved() isUnfoldHandled = false } this.isFolded = isFolded @@ -400,16 +194,7 @@ constructor( private companion object { const val TAG = "UnfoldLightRevealOverlayAnimation" - const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE - - // Put the unfold overlay below the rotation animation screenshot to hide the moment - // when it is rotated but the rotation of the other windows hasn't happen yet - const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1 - - // constants for revealAmount. - const val TRANSPARENT = 1f - const val BLACK = 0f - - private const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f + const val SURFACE_CONTAINER_NAME = "unfold-overlay-container" + const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f } } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt index 0fb4b43865aa..38b381ac543e 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt @@ -1,6 +1,7 @@ package com.android.systemui.user.domain.interactor import android.annotation.UserIdInt +import android.content.pm.UserInfo import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Flags.refactorGetCurrentUser import com.android.systemui.dagger.SysUISingleton @@ -16,6 +17,9 @@ class SelectedUserInteractor @Inject constructor(private val repository: UserRep /** Flow providing the ID of the currently selected user. */ val selectedUser = repository.selectedUserInfo.map { it.id }.distinctUntilChanged() + /** Flow providing the [UserInfo] of the currently selected user. */ + val selectedUserInfo = repository.selectedUserInfo + /** * Returns the ID of the currently-selected user. * diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt index c170eb567344..a122311e3b34 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt @@ -27,7 +27,6 @@ import android.content.pm.UserInfo import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.Icon -import android.os.Process import android.os.RemoteException import android.os.UserHandle import android.os.UserManager @@ -50,6 +49,7 @@ import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.process.ProcessWrapper import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.res.R import com.android.systemui.telephony.domain.interactor.TelephonyInteractor @@ -108,6 +108,7 @@ constructor( private val guestUserInteractor: GuestUserInteractor, private val uiEventLogger: UiEventLogger, private val userRestrictionChecker: UserRestrictionChecker, + private val processWrapper: ProcessWrapper ) { /** * Defines interface for classes that can be notified when the state of users on the device is @@ -669,7 +670,7 @@ constructor( // Connect to the new secondary user's service (purely to ensure that a persistent // SystemUI application is created for that user) - if (userId != Process.myUserHandle().identifier) { + if (userId != processWrapper.myUserHandle().identifier && !processWrapper.isSystemUser) { applicationContext.startServiceAsUser( intent, UserHandle.of(userId), diff --git a/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt b/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt deleted file mode 100644 index d3653b4b7266..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.util.view - -import android.view.View -import com.android.systemui.util.kotlin.awaitCancellationThenDispose -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest - -/** - * Use the [bind] method to bind the view every time this flow emits, and suspend to await for more - * updates. New emissions lead to the previous binding call being cancelled if not completed. - * Dispose of the [DisposableHandle] returned by [bind] when done. - */ -suspend fun <T : View> Flow<T>.bindLatest(bind: (T) -> DisposableHandle?) { - this.collectLatest { view -> - val disposableHandle = bind(view) - disposableHandle?.awaitCancellationThenDispose() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index ce6d74092197..90c5c62718ad 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -116,9 +116,8 @@ import com.android.internal.view.RotationPolicy; import com.android.settingslib.Utils; import com.android.systemui.Dumpable; import com.android.systemui.Prefs; -import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.haptics.slider.HapticSliderViewBinder; import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin; import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig; import com.android.systemui.media.dialog.MediaOutputDialogFactory; @@ -145,9 +144,6 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; -import kotlinx.coroutines.CoroutineDispatcher; -import kotlinx.coroutines.CoroutineScope; - /** * Visual presentation of the volume dialog. * @@ -311,8 +307,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private int mOrientation; private final Lazy<SecureSettings> mSecureSettings; private int mDialogTimeoutMillis; - private final CoroutineDispatcher mMainDispatcher; - private final CoroutineScope mApplicationScope; private final VibratorHelper mVibratorHelper; private final com.android.systemui.util.time.SystemClock mSystemClock; @@ -333,14 +327,10 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, DumpManager dumpManager, Lazy<SecureSettings> secureSettings, VibratorHelper vibratorHelper, - @Main CoroutineDispatcher mainDispatcher, - @Application CoroutineScope applicationScope, com.android.systemui.util.time.SystemClock systemClock) { mContext = new ContextThemeWrapper(context, R.style.volume_dialog_theme); mHandler = new H(looper); - mMainDispatcher = mainDispatcher; - mApplicationScope = applicationScope; mVibratorHelper = vibratorHelper; mSystemClock = systemClock; mShouldListenForJank = shouldListenForJank; @@ -858,7 +848,10 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)}); } row.slider = row.view.findViewById(R.id.volume_row_slider); - row.createPlugin(mVibratorHelper, mSystemClock, mMainDispatcher, mApplicationScope); + if (hapticVolumeSlider()) { + row.createPlugin(mVibratorHelper, mSystemClock); + HapticSliderViewBinder.bind(row.slider, row.mHapticPlugin); + } row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); row.number = row.view.findViewById(R.id.volume_number); @@ -1498,7 +1491,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, for (int i = 0; i < mRows.size(); i++) { VolumeRow row = mRows.get(i); if (row.slider.getVisibility() == VISIBLE) { - row.addHaptics(); + row.addTouchListener(); } } Trace.endSection(); @@ -2620,17 +2613,13 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, void createPlugin( VibratorHelper vibratorHelper, - com.android.systemui.util.time.SystemClock systemClock, - CoroutineDispatcher mainDispatcher, - CoroutineScope applicationScope) { - if (!hapticVolumeSlider() || mHapticPlugin != null) return; + com.android.systemui.util.time.SystemClock systemClock) { + if (mHapticPlugin != null) return; mHapticPlugin = new SeekableSliderHapticPlugin( - vibratorHelper, - systemClock, - mainDispatcher, - applicationScope, - sSliderHapticFeedbackConfig); + vibratorHelper, + systemClock, + sSliderHapticFeedbackConfig); } @@ -2647,19 +2636,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, }); } - void addHaptics() { - if (mHapticPlugin != null) { - addTouchListener(); - mHapticPlugin.start(); - } - } - @SuppressLint("ClickableViewAccessibility") void removeHaptics() { slider.setOnTouchListener(null); - if (mHapticPlugin != null) { - mHapticPlugin.stop(); - } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt index ff1daea4816e..278ffc6b8412 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -21,6 +21,8 @@ import android.media.AudioManager import com.android.settingslib.volume.data.repository.AudioRepository import com.android.settingslib.volume.data.repository.AudioRepositoryImpl import com.android.settingslib.volume.domain.interactor.AudioModeInteractor +import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver +import com.android.settingslib.volume.shared.AudioManagerIntentsReceiverImpl import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import dagger.Module @@ -35,13 +37,19 @@ interface AudioModule { companion object { @Provides - fun provideAudioRepository( + fun provideAudioManagerIntentsReceiver( @Application context: Context, + @Application coroutineScope: CoroutineScope, + ): AudioManagerIntentsReceiver = AudioManagerIntentsReceiverImpl(context, coroutineScope) + + @Provides + fun provideAudioRepository( + intentsReceiver: AudioManagerIntentsReceiver, audioManager: AudioManager, @Background coroutineContext: CoroutineContext, @Application coroutineScope: CoroutineScope, ): AudioRepository = - AudioRepositoryImpl(context, audioManager, coroutineContext, coroutineScope) + AudioRepositoryImpl(intentsReceiver, audioManager, coroutineContext, coroutineScope) @Provides fun provideAudioModeInteractor(repository: AudioRepository): AudioModeInteractor = diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt index 2ff9af9ac3c0..ab76d450eb0a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt @@ -16,11 +16,11 @@ package com.android.systemui.volume.dagger -import android.content.Context import android.media.session.MediaSessionManager import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.volume.data.repository.MediaControllerRepository import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl +import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -37,14 +37,14 @@ interface MediaDevicesModule { @Provides @SysUISingleton fun provideMediaDeviceSessionRepository( - @Application context: Context, + intentsReceiver: AudioManagerIntentsReceiver, mediaSessionManager: MediaSessionManager, localBluetoothManager: LocalBluetoothManager?, @Application coroutineScope: CoroutineScope, @Background backgroundContext: CoroutineContext, ): MediaControllerRepository = MediaControllerRepositoryImpl( - context, + intentsReceiver, mediaSessionManager, localBluetoothManager, coroutineScope, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 2718839db651..32856373dbe9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -23,8 +23,6 @@ import android.os.Looper; import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.CoreStartable; -import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.VolumeDialog; @@ -55,9 +53,6 @@ import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; import dagger.multibindings.IntoSet; -import kotlinx.coroutines.CoroutineDispatcher; -import kotlinx.coroutines.CoroutineScope; - /** Dagger Module for code in the volume package. */ @Module( includes = { @@ -112,8 +107,6 @@ public interface VolumeModule { DumpManager dumpManager, Lazy<SecureSettings> secureSettings, VibratorHelper vibratorHelper, - @Main CoroutineDispatcher mainDispatcher, - @Application CoroutineScope applicationScope, SystemClock systemClock) { VolumeDialogImpl impl = new VolumeDialogImpl( context, @@ -132,8 +125,6 @@ public interface VolumeModule { dumpManager, secureSettings, vibratorHelper, - mainDispatcher, - applicationScope, systemClock); impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false); impl.setAutomute(true); diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt index 57ac4357b9b5..0a1ee249d6fb 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt @@ -15,8 +15,10 @@ */ package com.android.systemui.volume.panel.component.mediaoutput.data.repository +import android.media.MediaRouter2Manager import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl +import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.controls.pipeline.LocalMediaManagerFactory @@ -27,6 +29,8 @@ import kotlinx.coroutines.CoroutineScope class LocalMediaRepositoryFactory @Inject constructor( + private val intentsReceiver: AudioManagerIntentsReceiver, + private val mediaRouter2Manager: MediaRouter2Manager, private val localMediaManagerFactory: LocalMediaManagerFactory, @Application private val coroutineScope: CoroutineScope, @Background private val backgroundCoroutineContext: CoroutineContext, @@ -34,7 +38,9 @@ constructor( fun create(packageName: String?): LocalMediaRepository = LocalMediaRepositoryImpl( + intentsReceiver, localMediaManagerFactory.create(packageName), + mediaRouter2Manager, coroutineScope, backgroundCoroutineContext, ) diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index c2efc05132ba..d048cbe02fa8 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -28,6 +28,7 @@ import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockscreenCredential import com.android.keyguard.KeyguardPinViewController.PinBouncerUiEvent import com.android.keyguard.KeyguardSecurityModel.SecurityMode +import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake @@ -88,6 +89,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Mock private val mEmergencyButtonController: EmergencyButtonController? = null private val falsingCollector: FalsingCollector = FalsingCollectorFake() + private val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository()) @Mock lateinit var postureController: DevicePostureController @Mock lateinit var mSelectedUserInteractor: SelectedUserInteractor @@ -143,7 +145,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { featureFlags, mSelectedUserInteractor, uiEventLogger, - FakeKeyboardRepository() + keyguardKeyboardInteractor ) } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index 0959f1b2bcf6..4a2554eb5201 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.util.LatencyTracker import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector @@ -80,6 +81,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { LayoutInflater.from(context).inflate(R.layout.keyguard_sim_pin_view, null) as KeyguardSimPinView val fakeFeatureFlags = FakeFeatureFlags() + val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository()) mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES) underTest = @@ -97,7 +99,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { emergencyButtonController, fakeFeatureFlags, mSelectedUserInteractor, - FakeKeyboardRepository() + keyguardKeyboardInteractor ) underTest.init() underTest.onViewAttached() diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index 1281e4409a83..4f461845d905 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt @@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.util.LatencyTracker import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector @@ -75,6 +76,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { simPukView = LayoutInflater.from(context).inflate(R.layout.keyguard_sim_puk_view, null) as KeyguardSimPukView + val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository()) val fakeFeatureFlags = FakeFeatureFlags() mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES) underTest = @@ -92,7 +94,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { emergencyButtonController, fakeFeatureFlags, mSelectedUserInteractor, - FakeKeyboardRepository() + keyguardKeyboardInteractor ) underTest.init() } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt index b45c8948e763..fb649c8f3891 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt @@ -24,8 +24,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.FoldAodAnimationController +import com.android.systemui.unfold.FullscreenLightRevealAnimation import com.android.systemui.unfold.SysUIUnfoldComponent -import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation import com.android.systemui.util.mockito.capture import com.android.systemui.utils.os.FakeHandler import com.android.systemui.utils.os.FakeHandler.Mode.QUEUEING @@ -53,7 +53,9 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { @Mock private lateinit var foldAodAnimationController: FoldAodAnimationController @Mock - private lateinit var unfoldAnimation: UnfoldLightRevealOverlayAnimation + private lateinit var fullscreenLightRevealAnimation: FullscreenLightRevealAnimation + @Mock + private lateinit var fullScreenLightRevealAnimations: Set<FullscreenLightRevealAnimation> @Captor private lateinit var readyCaptor: ArgumentCaptor<Runnable> @@ -67,9 +69,9 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - - `when`(unfoldComponent.getUnfoldLightRevealOverlayAnimation()) - .thenReturn(unfoldAnimation) + fullScreenLightRevealAnimations = setOf(fullscreenLightRevealAnimation) + `when`(unfoldComponent.getFullScreenLightRevealAnimations()) + .thenReturn(fullScreenLightRevealAnimations) `when`(unfoldComponent.getFoldAodAnimationController()) .thenReturn(foldAodAnimationController) @@ -164,7 +166,7 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { } private fun onUnfoldOverlayReady() { - verify(unfoldAnimation).onScreenTurningOn(capture(readyCaptor)) + verify(fullscreenLightRevealAnimation).onScreenTurningOn(capture(readyCaptor)) readyCaptor.value.run() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt index 202d9ce27a34..e157fc508f87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt @@ -91,7 +91,7 @@ class SystemUIApplicationTest : SysuiTestCase() { whenever(sysuiComponent.startables) .thenReturn(mutableMapOf(StartableA::class.java to Provider { startableA })) app.onCreate() - app.startServicesIfNeeded() + app.startSystemUserServicesIfNeeded() assertThat(startableA.started).isTrue() } @@ -105,7 +105,7 @@ class SystemUIApplicationTest : SysuiTestCase() { ) ) app.onCreate() - app.startServicesIfNeeded() + app.startSystemUserServicesIfNeeded() assertThat(startableA.started).isTrue() assertThat(startableB.started).isTrue() } @@ -121,7 +121,7 @@ class SystemUIApplicationTest : SysuiTestCase() { ) ) app.onCreate() - app.startServicesIfNeeded() + app.startSystemUserServicesIfNeeded() assertThat(startableA.started).isTrue() assertThat(startableB.started).isTrue() assertThat(startableC.started).isTrue() @@ -141,7 +141,7 @@ class SystemUIApplicationTest : SysuiTestCase() { ) ) app.onCreate() - app.startServicesIfNeeded() + app.startSystemUserServicesIfNeeded() assertThat(startableA.started).isTrue() assertThat(startableB.started).isTrue() assertThat(startableC.started).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index 375ebe86ae29..8299acbc2d52 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -51,7 +51,6 @@ import android.view.WindowManagerGlobal; import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.view.animation.AccelerateInterpolator; -import androidx.test.filters.FlakyTest; import androidx.test.filters.LargeTest; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; @@ -80,7 +79,6 @@ import java.util.function.Supplier; @LargeTest @RunWith(AndroidTestingRunner.class) -@FlakyTest(bugId = 308501761) public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { @Rule diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt index 8faf715521f8..722107c7a1b5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt @@ -45,11 +45,11 @@ import org.mockito.junit.MockitoJUnit @RunWith(AndroidTestingRunner::class) @RunWithLooper class ActivityLaunchAnimatorTest : SysuiTestCase() { - private val launchContainer = LinearLayout(mContext) - private val testLaunchAnimator = fakeLaunchAnimator() + private val transitionContainer = LinearLayout(mContext) + private val testTransitionAnimator = fakeTransitionAnimator() @Mock lateinit var callback: ActivityLaunchAnimator.Callback @Mock lateinit var listener: ActivityLaunchAnimator.Listener - @Spy private val controller = TestLaunchAnimatorController(launchContainer) + @Spy private val controller = TestLaunchAnimatorController(transitionContainer) @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback private lateinit var activityLaunchAnimator: ActivityLaunchAnimator @@ -58,7 +58,11 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { @Before fun setup() { activityLaunchAnimator = - ActivityLaunchAnimator(testLaunchAnimator, testLaunchAnimator, disableWmTimeout = true) + ActivityLaunchAnimator( + testTransitionAnimator, + testTransitionAnimator, + disableWmTimeout = true + ) activityLaunchAnimator.callback = callback activityLaunchAnimator.addListener(listener) } @@ -165,7 +169,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { waitForIdleSync() verify(controller).onLaunchAnimationCancelled() - verify(controller, never()).onLaunchAnimationStart(anyBoolean()) + verify(controller, never()).onTransitionAnimationStart(anyBoolean()) verify(listener).onLaunchAnimationCancelled() verify(listener, never()).onLaunchAnimationStart() assertNull(runner.delegate) @@ -178,7 +182,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { waitForIdleSync() verify(controller).onLaunchAnimationCancelled() - verify(controller, never()).onLaunchAnimationStart(anyBoolean()) + verify(controller, never()).onTransitionAnimationStart(anyBoolean()) verify(listener).onLaunchAnimationCancelled() verify(listener, never()).onLaunchAnimationStart() assertNull(runner.delegate) @@ -190,7 +194,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { runner.onAnimationStart(0, arrayOf(fakeWindow()), emptyArray(), emptyArray(), iCallback) waitForIdleSync() verify(listener).onLaunchAnimationStart() - verify(controller).onLaunchAnimationStart(anyBoolean()) + verify(controller).onTransitionAnimationStart(anyBoolean()) } @Test @@ -240,10 +244,10 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { * A simple implementation of [ActivityLaunchAnimator.Controller] which throws if it is called * outside of the main thread. */ -private class TestLaunchAnimatorController(override var launchContainer: ViewGroup) : +private class TestLaunchAnimatorController(override var transitionContainer: ViewGroup) : ActivityLaunchAnimator.Controller { override fun createAnimatorState() = - LaunchAnimator.State( + TransitionAnimator.State( top = 100, bottom = 200, left = 300, @@ -262,19 +266,19 @@ private class TestLaunchAnimatorController(override var launchContainer: ViewGro assertOnMainThread() } - override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { assertOnMainThread() } - override fun onLaunchAnimationProgress( - state: LaunchAnimator.State, + override fun onTransitionAnimationProgress( + state: TransitionAnimator.State, progress: Float, linearProgress: Float ) { assertOnMainThread() } - override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { assertOnMainThread() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt index 2233e3226fd6..a58642192336 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt @@ -129,14 +129,14 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { // The dialog shouldn't be dismissable during the animation. runOnMainThreadAndWaitForIdleSync { - controller.onLaunchAnimationStart(isExpandingFullyAbove = true) + controller.onTransitionAnimationStart(isExpandingFullyAbove = true) secondDialog.dismiss() } assertTrue(secondDialog.isShowing) // Both dialogs should be dismissed at the end of the animation. runOnMainThreadAndWaitForIdleSync { - controller.onLaunchAnimationEnd(isExpandingFullyAbove = true) + controller.onTransitionAnimationEnd(isExpandingFullyAbove = true) } assertFalse(firstDialog.isShowing) assertFalse(secondDialog.isShowing) diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt index d1ac0e861d26..8442a62d7c40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt @@ -32,13 +32,13 @@ import org.junit.runner.RunWith class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() { @Test fun animatingOrphanViewDoesNotCrash() { - val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0) + val state = TransitionAnimator.State(top = 0, bottom = 0, left = 0, right = 0) val controller = GhostedViewLaunchAnimatorController(LaunchableFrameLayout(mContext)) controller.onIntentStarted(willAnimate = true) - controller.onLaunchAnimationStart(isExpandingFullyAbove = true) - controller.onLaunchAnimationProgress(state, progress = 0f, linearProgress = 0f) - controller.onLaunchAnimationEnd(isExpandingFullyAbove = true) + controller.onTransitionAnimationStart(isExpandingFullyAbove = true) + controller.onTransitionAnimationProgress(state, progress = 0f, linearProgress = 0f) + controller.onTransitionAnimationEnd(isExpandingFullyAbove = true) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt deleted file mode 100644 index 112cec25784c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - * - */ - -package com.android.systemui.common.ui - -import android.content.Context -import android.testing.AndroidTestingRunner -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.util.mockito.captureMany -import com.android.systemui.util.mockito.mock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidTestingRunner::class) -class ConfigurationStateTest : SysuiTestCase() { - - private val configurationController: ConfigurationController = mock() - private val layoutInflater = TestLayoutInflater() - private val backgroundDispatcher = StandardTestDispatcher() - private val testScope = TestScope(backgroundDispatcher) - - val underTest = ConfigurationState(configurationController, context, layoutInflater) - - @Test - fun reinflateAndBindLatest_inflatesWithoutEmission() = - testScope.runTest { - var callbackCount = 0 - backgroundScope.launch { - underTest.reinflateAndBindLatest<View>( - resource = 0, - root = null, - attachToRoot = false, - backgroundDispatcher, - ) { - callbackCount++ - null - } - } - - // Inflates without an emission - runCurrent() - assertThat(layoutInflater.inflationCount).isEqualTo(1) - assertThat(callbackCount).isEqualTo(1) - } - - @Test - fun reinflateAndBindLatest_reinflatesOnThemeChanged() = - testScope.runTest { - var callbackCount = 0 - backgroundScope.launch { - underTest.reinflateAndBindLatest<View>( - resource = 0, - root = null, - attachToRoot = false, - backgroundDispatcher, - ) { - callbackCount++ - null - } - } - runCurrent() - - val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany { - verify(configurationController, atLeastOnce()).addCallback(capture()) - } - - listOf(1, 2, 3).forEach { count -> - assertThat(layoutInflater.inflationCount).isEqualTo(count) - assertThat(callbackCount).isEqualTo(count) - configListeners.forEach { it.onThemeChanged() } - runCurrent() - } - } - - @Test - fun reinflateAndBindLatest_reinflatesOnDensityOrFontScaleChanged() = - testScope.runTest { - var callbackCount = 0 - backgroundScope.launch { - underTest.reinflateAndBindLatest<View>( - resource = 0, - root = null, - attachToRoot = false, - backgroundDispatcher, - ) { - callbackCount++ - null - } - } - runCurrent() - - val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany { - verify(configurationController, atLeastOnce()).addCallback(capture()) - } - - listOf(1, 2, 3).forEach { count -> - assertThat(layoutInflater.inflationCount).isEqualTo(count) - assertThat(callbackCount).isEqualTo(count) - configListeners.forEach { it.onDensityOrFontScaleChanged() } - runCurrent() - } - } - - @Test - fun testReinflateAndBindLatest_disposesOnCancel() = - testScope.runTest { - var callbackCount = 0 - var disposed = false - val job = launch { - underTest.reinflateAndBindLatest<View>( - resource = 0, - root = null, - attachToRoot = false, - backgroundDispatcher, - ) { - callbackCount++ - DisposableHandle { disposed = true } - } - } - - runCurrent() - job.cancelAndJoin() - assertThat(disposed).isTrue() - } - - inner class TestLayoutInflater : LayoutInflater(context) { - - var inflationCount = 0 - - override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View { - inflationCount++ - return View(context) - } - - override fun cloneInContext(p0: Context?): LayoutInflater { - // not needed for this test - return this - } - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index b28d0c802c18..e30dd35d74c1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -33,8 +33,7 @@ import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepositor import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig -import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig +import com.android.systemui.deviceentry.data.repository.fakeFaceWakeUpTriggersConfig import com.android.systemui.deviceentry.shared.FaceAuthUiEvent import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository @@ -56,10 +55,9 @@ import com.android.systemui.testKosmos import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -68,37 +66,33 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { - val kosmos = - testKosmos().apply { this.faceWakeUpTriggersConfig = mock<FaceWakeUpTriggersConfig>() } + private val kosmos = testKosmos() + private val testScope: TestScope = kosmos.testScope private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor - private val testScope = kosmos.testScope private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository private val fakeUserRepository = kosmos.fakeUserRepository private val facePropertyRepository = kosmos.facePropertyRepository - private val fakeDeviceEntryFingerprintAuthRepository = - kosmos.fakeDeviceEntryFingerprintAuthRepository + private val fakeDeviceEntryFingerprintAuthInteractor = + kosmos.deviceEntryFingerprintAuthInteractor private val powerInteractor = kosmos.powerInteractor private val fakeBiometricSettingsRepository = kosmos.fakeBiometricSettingsRepository private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor - private val faceWakeUpTriggersConfig = kosmos.faceWakeUpTriggersConfig + private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig private val trustManager = kosmos.trustManager @Before fun setup() { - MockitoAnnotations.initMocks(this) fakeUserRepository.setUserInfos(listOf(primaryUser, secondaryUser)) - underTest = SystemUIDeviceEntryFaceAuthInteractor( mContext, @@ -110,7 +104,7 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { keyguardTransitionInteractor, FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")), keyguardUpdateMonitor, - fakeDeviceEntryFingerprintAuthRepository, + fakeDeviceEntryFingerprintAuthInteractor, fakeUserRepository, facePropertyRepository, faceWakeUpTriggersConfig, @@ -126,10 +120,9 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { underTest.start() powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID) - whenever( - faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LID) - ) - .thenReturn(true) + faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom( + setOf(WakeSleepReason.LID.powerManagerWakeReason) + ) keyguardTransitionRepository.sendTransitionStep( TransitionStep( @@ -168,10 +161,9 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { underTest.start() powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID) - whenever( - faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LID) - ) - .thenReturn(true) + faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom( + setOf(WakeSleepReason.LID.powerManagerWakeReason) + ) keyguardTransitionRepository.sendTransitionStep( TransitionStep( @@ -194,10 +186,9 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { underTest.start() powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LIFT) - whenever( - faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LIFT) - ) - .thenReturn(false) + faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom( + setOf(WakeSleepReason.LID.powerManagerWakeReason) + ) keyguardTransitionRepository.sendTransitionStep( TransitionStep( @@ -217,10 +208,9 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { underTest.start() powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID) - whenever( - faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LID) - ) - .thenReturn(true) + faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom( + setOf(WakeSleepReason.LID.powerManagerWakeReason) + ) keyguardTransitionRepository.sendTransitionStep( TransitionStep( @@ -440,7 +430,45 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { underTest.start() fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(faceAuthRepository.isLockedOut.value).isTrue() + } + + @Test + fun faceLockoutStateIsResetWheneverFingerprintIsNotLockedOut() = + testScope.runTest { + underTest.start() + fakeUserRepository.setSelectedUserInfo(primaryUser, SelectionStatus.SELECTION_COMPLETE) + fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(faceAuthRepository.isLockedOut.value).isTrue() + facePropertyRepository.setLockoutMode(primaryUserId, LockoutMode.NONE) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(faceAuthRepository.isLockedOut.value).isFalse() + } + + @Test + fun faceLockoutStateIsSetToUsersLockoutStateWheneverFingerprintIsNotLockedOut() = + testScope.runTest { + underTest.start() + fakeUserRepository.setSelectedUserInfo(primaryUser, SelectionStatus.SELECTION_COMPLETE) + fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(faceAuthRepository.isLockedOut.value).isTrue() + facePropertyRepository.setLockoutMode(primaryUserId, LockoutMode.TIMED) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) runCurrent() assertThat(faceAuthRepository.isLockedOut.value).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt index db0496227a38..796d6d9c3359 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt @@ -20,8 +20,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest @@ -36,6 +36,7 @@ import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @SmallTest +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class SeekableSliderTrackerTest : SysuiTestCase() { @@ -51,7 +52,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun initializeSliderTracker_startsTracking() = runTest { // GIVEN Initialized tracker - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // THEN the tracker job is active assertThat(mSeekableSliderTracker.isTracking).isTrue() @@ -61,7 +62,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun stopTracking_onAnyState_resetsToIdle() = runTest { enumValues<SliderState>().forEach { // GIVEN Initialized tracker - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a state in the state machine mSeekableSliderTracker.setState(it) @@ -79,7 +80,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun initializeSliderTracker_isIdle() = runTest { // GIVEN Initialized tracker - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // THEN The state is idle and the listener is not called to play haptics assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) @@ -88,7 +89,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun startsTrackingTouch_onIdle_entersWaitState() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a start of tracking touch event val progress = 0f @@ -106,7 +107,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun waitCompletes_onWait_movesToHandleAcquired() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a start of tracking touch event that moves the tracker to WAIT val progress = 0f @@ -126,7 +127,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun impreciseTouch_onWait_movesToHandleAcquired() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the // slider @@ -151,7 +152,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun trackJump_onWait_movesToJumpTrackLocationSelected() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the // slider @@ -175,7 +176,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun upperBookendSelection_onWait_movesToBookendSelected() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the // slider @@ -197,7 +198,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun lowerBookendSelection_onWait_movesToBookendSelected() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the // slider @@ -219,7 +220,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun stopTracking_onWait_whenWaitingJobIsActive_resetsToIdle() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the // slider @@ -240,7 +241,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun progressChangeByUser_onJumpTrackLocationSelected_movesToDragHandleDragging() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a JUMP_TRACK_LOCATION_SELECTED state mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED) @@ -256,7 +257,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun touchRelease_onJumpTrackLocationSelected_movesToIdle() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a JUMP_TRACK_LOCATION_SELECTED state mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED) @@ -272,7 +273,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun progressChangeByUser_onJumpBookendSelected_movesToDragHandleDragging() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a JUMP_BOOKEND_SELECTED state mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED) @@ -288,7 +289,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun touchRelease_onJumpBookendSelected_movesToIdle() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a JUMP_BOOKEND_SELECTED state mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED) @@ -306,7 +307,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun progressChangeByUser_onHandleAcquired_movesToDragHandleDragging() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) @@ -325,7 +326,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun touchRelease_onHandleAcquired_movesToIdle() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) @@ -344,7 +345,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun progressChangeByUser_onHandleDragging_progressOutsideOfBookends_doesNotChangeState() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_DRAGGING state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) @@ -366,7 +367,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun progressChangeByUser_onHandleDragging_reachesLowerBookend_movesToHandleReachedBookend() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_DRAGGING state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) @@ -389,7 +390,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun progressChangeByUser_onHandleDragging_reachesUpperBookend_movesToHandleReachedBookend() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_DRAGGING state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) @@ -410,7 +411,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun touchRelease_onHandleDragging_movesToIdle() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_DRAGGING state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) @@ -430,7 +431,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun progressChangeByUser_outsideOfBookendRange_onLowerBookend_movesToDragHandleDragging() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) @@ -451,7 +452,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun progressChangeByUser_insideOfBookendRange_onLowerBookend_doesNotChangeState() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) @@ -473,7 +474,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun progressChangeByUser_outsideOfBookendRange_onUpperBookend_movesToDragHandleDragging() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) @@ -494,7 +495,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun progressChangeByUser_insideOfBookendRange_onUpperBookend_doesNotChangeState() = runTest { val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) @@ -514,7 +515,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun touchRelease_onHandleReachedBookend_movesToIdle() = runTest { - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) @@ -531,7 +532,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun onProgressChangeByProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest { // GIVEN an initialized tracker in the IDLE state - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a progress due to an external source that lands at the middle of the slider val progress = 0.5f @@ -550,7 +551,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun onProgressChangeByProgram_atUpperBookend_onIdle_movesToIdle() = runTest { // GIVEN an initialized tracker in the IDLE state val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a progress due to an external source that lands at the upper bookend val progress = config.upperBookendThreshold + 0.01f @@ -567,7 +568,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun onProgressChangeByProgram_atLowerBookend_onIdle_movesToIdle() = runTest { // GIVEN an initialized tracker in the IDLE state val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // WHEN a progress is recorded due to an external source that lands at the lower bookend val progress = config.lowerBookendThreshold - 0.01f @@ -583,7 +584,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun onArrowUp_onArrowMovedOnce_movesToIdle() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) // WHEN the external stimulus is released @@ -598,7 +599,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun onStartTrackingTouch_onArrowMovedOnce_movesToWait() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) // WHEN the slider starts tracking touch @@ -615,7 +616,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun onProgressChangeByProgram_onArrowMovedOnce_movesToArrowMovesContinuously() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) // WHEN the slider gets an external progress change @@ -634,7 +635,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun onArrowUp_onArrowMovesContinuously_movesToIdle() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the external stimulus is released @@ -649,7 +650,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun onStartTrackingTouch_onArrowMovesContinuously_movesToWait() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the slider starts tracking touch @@ -665,7 +666,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @Test fun onProgressChangeByProgram_onArrowMovesContinuously_preservesState() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state - initTracker(testScheduler) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the slider changes progress programmatically at the middle @@ -684,7 +685,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun onProgramProgress_atLowerBookend_onArrowMovesContinuously_movesToIdle() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the slider reaches the lower bookend programmatically @@ -702,7 +703,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun onProgramProgress_atUpperBookend_onArrowMovesContinuously_movesToIdle() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state val config = SeekableSliderTrackerConfig() - initTracker(testScheduler, config) + initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the slider reaches the lower bookend programmatically @@ -718,16 +719,11 @@ class SeekableSliderTrackerTest : SysuiTestCase() { @OptIn(ExperimentalCoroutinesApi::class) private fun initTracker( - scheduler: TestCoroutineScheduler, + scope: CoroutineScope, config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(), ) { mSeekableSliderTracker = - SeekableSliderTracker( - sliderStateListener, - sliderEventProducer, - UnconfinedTestDispatcher(scheduler), - config - ) + SeekableSliderTracker(sliderStateListener, sliderEventProducer, scope, config) mSeekableSliderTracker.startTracking() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 14cae0b89a49..24cf16479188 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -94,7 +94,6 @@ import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; @@ -754,7 +753,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test public void testUpdateIsKeyguardAfterOccludeAnimationEnds() { - mViewMediator.mOccludeAnimationController.onLaunchAnimationEnd( + mViewMediator.mOccludeAnimationController.onTransitionAnimationEnd( false /* isExpandingFullyAbove */); // Since the updateIsKeyguard call is delayed during the animation, ensure it's called once diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt index 2d9d5ed2b5e1..0e9197ef8ac1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt @@ -181,6 +181,31 @@ class KeyguardTransitionAnimationFlowTest : SysuiTestCase() { } @Test + fun usesOnStepToDoubleValueWithState() = + testScope.runTest { + val flow = + underTest.sharedFlowWithState( + duration = 1000.milliseconds, + onStep = { it * 2 }, + ) + val animationValues by collectLastValue(flow) + runCurrent() + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + assertThat(animationValues).isEqualTo(StateToValue(TransitionState.STARTED, 0f)) + repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING)) + assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 0.6f)) + repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING)) + assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.2f)) + repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING)) + assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.6f)) + repository.sendTransitionStep(step(1f, TransitionState.RUNNING)) + assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 2f)) + repository.sendTransitionStep(step(1f, TransitionState.FINISHED)) + assertThat(animationValues).isEqualTo(StateToValue(TransitionState.FINISHED, null)) + } + + @Test fun sameFloatValueWithTheSameTransitionStateDoesNotEmitTwice() = testScope.runTest { val flow = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt index 1c9c942eafc6..bfa84335d670 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.StateToValue import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.collect.Range @@ -57,17 +58,19 @@ class GoneToAodTransitionViewModelTest : SysuiTestCase() { // The animation should only start > .4f way through repository.sendTransitionStep(step(0f, TransitionState.STARTED)) - assertThat(enterFromTopTranslationY).isEqualTo(pixels) + assertThat(enterFromTopTranslationY) + .isEqualTo(StateToValue(TransitionState.STARTED, pixels)) - repository.sendTransitionStep(step(0.4f)) - assertThat(enterFromTopTranslationY).isEqualTo(pixels) + repository.sendTransitionStep(step(.55f)) + assertThat(enterFromTopTranslationY!!.value ?: -1f).isIn(Range.closed(pixels, 0f)) repository.sendTransitionStep(step(.85f)) - assertThat(enterFromTopTranslationY).isIn(Range.closed(pixels, 0f)) + assertThat(enterFromTopTranslationY!!.value ?: -1f).isIn(Range.closed(pixels, 0f)) // At the end, the translation should be complete and set to zero repository.sendTransitionStep(step(1f)) - assertThat(enterFromTopTranslationY).isEqualTo(0f) + assertThat(enterFromTopTranslationY) + .isEqualTo(StateToValue(TransitionState.RUNNING, 0f)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt index 301d887b7474..d9453d67848f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt @@ -33,6 +33,7 @@ class NearbyMediaDevicesManagerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) manager = NearbyMediaDevicesManager(commandQueue, logger) + manager.start() val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) verify(commandQueue).addCallback(callbackCaptor.capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt index d757d7144276..ab90b9be3c1e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt @@ -24,10 +24,13 @@ import com.android.internal.logging.testing.UiEventLoggerFake import com.android.settingslib.RestrictedLockUtils import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake +import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.BrightnessMirrorController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before @@ -35,6 +38,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.isNull @@ -61,7 +65,7 @@ class BrightnessSliderControllerTest : SysuiTestCase() { @Mock private lateinit var listener: ToggleSlider.Listener @Mock - private lateinit var mBrightnessSliderHapticPlugin: BrightnessSliderHapticPlugin + private lateinit var vibratorHelper: VibratorHelper @Captor private lateinit var seekBarChangeCaptor: ArgumentCaptor<SeekBar.OnSeekBarChangeListener> @@ -69,6 +73,7 @@ class BrightnessSliderControllerTest : SysuiTestCase() { private lateinit var seekBar: SeekBar private val uiEventLogger = UiEventLoggerFake() private var mFalsingManager: FalsingManagerFake = FalsingManagerFake() + private val systemClock = FakeSystemClock() private lateinit var mController: BrightnessSliderController @@ -78,13 +83,14 @@ class BrightnessSliderControllerTest : SysuiTestCase() { whenever(mirrorController.toggleSlider).thenReturn(mirror) whenever(motionEvent.copy()).thenReturn(motionEvent) + whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0)) mController = BrightnessSliderController( brightnessSliderView, mFalsingManager, uiEventLogger, - mBrightnessSliderHapticPlugin, + SeekableSliderHapticPlugin(vibratorHelper, systemClock), ) mController.init() mController.setOnChangedListener(listener) @@ -100,7 +106,6 @@ class BrightnessSliderControllerTest : SysuiTestCase() { mController.onViewAttached() verify(brightnessSliderView).setOnSeekBarChangeListener(notNull()) - verify(mBrightnessSliderHapticPlugin).start() } @Test @@ -110,7 +115,6 @@ class BrightnessSliderControllerTest : SysuiTestCase() { verify(brightnessSliderView).setOnSeekBarChangeListener(isNull()) verify(brightnessSliderView).setOnDispatchTouchEventListener(isNull()) - verify(mBrightnessSliderHapticPlugin).stop() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImplTest.kt deleted file mode 100644 index 51629b5c01e3..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImplTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.settings.brightness - -import android.view.VelocityTracker -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.haptics.slider.SeekableSliderEventProducer -import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -@SmallTest -@RunWith(AndroidJUnit4::class) -class BrightnessSliderHapticPluginImplTest : SysuiTestCase() { - - @Mock private lateinit var vibratorHelper: VibratorHelper - @Mock private lateinit var velocityTracker: VelocityTracker - @Mock private lateinit var mainDispatcher: CoroutineDispatcher - - private val systemClock = FakeSystemClock() - private val sliderEventProducer = SeekableSliderEventProducer() - - private lateinit var plugin: BrightnessSliderHapticPluginImpl - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0)) - } - - @Test - fun start_beginsTrackingSlider() = runTest { - createPlugin(UnconfinedTestDispatcher(testScheduler)) - plugin.start() - - assertThat(plugin.isTracking).isTrue() - } - - @Test - fun stop_stopsTrackingSlider() = runTest { - createPlugin(UnconfinedTestDispatcher(testScheduler)) - // GIVEN that the plugin started the tracking component - plugin.start() - - // WHEN called to stop - plugin.stop() - - // THEN the tracking component stops - assertThat(plugin.isTracking).isFalse() - } - - @Test - fun start_afterStop_startsTheTrackingAgain() = runTest { - createPlugin(UnconfinedTestDispatcher(testScheduler)) - // GIVEN that the plugin started the tracking component - plugin.start() - - // WHEN the plugin is restarted - plugin.stop() - plugin.start() - - // THEN the tracking begins again - assertThat(plugin.isTracking).isTrue() - } - - private fun createPlugin(dispatcher: CoroutineDispatcher) { - plugin = - BrightnessSliderHapticPluginImpl( - vibratorHelper, - systemClock, - dispatcher, - velocityTracker, - sliderEventProducer, - ) - } -} 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 9517f823cf10..1dc5f7dbf6fe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -103,8 +103,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ) testableLooper = TestableLooper.get(this) - communalRepository.setIsCommunalEnabled(true) - whenever(keyguardTransitionInteractor.isFinishedInStateWhere(any())) .thenReturn(bouncerShowingFlow) whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(shadeShowingFlow) @@ -125,36 +123,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test - fun isEnabled_communalEnabled_returnsTrue() { - communalRepository.setIsCommunalEnabled(true) - - assertThat(underTest.isEnabled()).isTrue() - } - - @Test - fun isEnabled_communalDisabled_returnsFalse() { - communalRepository.setIsCommunalEnabled(false) - - assertThat(underTest.isEnabled()).isFalse() - } - - @Test - fun initView_notEnabled_throwsException() { - communalRepository.setIsCommunalEnabled(false) - - underTest = - GlanceableHubContainerController( - communalInteractor, - communalViewModel, - keyguardTransitionInteractor, - shadeInteractor, - powerManager, - ) - - assertThrows(RuntimeException::class.java) { underTest.initView(context) } - } - - @Test fun initView_calledTwice_throwsException() { underTest = GlanceableHubContainerController( diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 22b05be507c6..248ed249c213 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -488,7 +488,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { return } - whenever(mGlanceableHubContainerController.isEnabled()).thenReturn(true) + whenever(mGlanceableHubContainerController.communalAvailable()) + .thenReturn(MutableStateFlow(true)) val mockCommunalView = mock(View::class.java) whenever(mGlanceableHubContainerController.initView(any<Context>())) @@ -513,7 +514,6 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { return } - whenever(mGlanceableHubContainerController.isEnabled()).thenReturn(false) whenever(mGlanceableHubContainerController.communalAvailable()) .thenReturn(MutableStateFlow(false)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt index cd744100770e..f58ff0adb2b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt @@ -115,7 +115,7 @@ class NotificationLaunchAnimatorControllerTest : SysuiTestCase() { @Test fun testHunIsRemovedAndCallbackIsInvokedWhenAnimationEnds() { flagNotificationAsHun() - controller.onLaunchAnimationEnd(isExpandingFullyAbove = true) + controller.onTransitionAnimationEnd(isExpandingFullyAbove = true) assertFalse(HeadsUpUtil.isClickedHeadsUpNotification(notification)) assertFalse(notification.entry.isExpandAnimationRunning) @@ -157,7 +157,7 @@ class NotificationLaunchAnimatorControllerTest : SysuiTestCase() { assertNotSame(GROUP_ALERT_SUMMARY, summary.sbn.notification.groupAlertBehavior) assertNotSame(GROUP_ALERT_SUMMARY, notification.entry.sbn.notification.groupAlertBehavior) - controller.onLaunchAnimationEnd(isExpandingFullyAbove = true) + controller.onTransitionAnimationEnd(isExpandingFullyAbove = true) verify(headsUpManager) .removeNotification(summary.key, true /* releaseImmediately */, false /* animate */) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 7589a49e1963..354f3f679ffb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -797,6 +797,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void testUpdateFooter_remoteInput() { ArgumentCaptor<RemoteInputController.Callback> callbackCaptor = ArgumentCaptor.forClass(RemoteInputController.Callback.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 4afcc8c9da43..04f3216c8e73 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -440,6 +440,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void testUpdateFooter_noNotifications() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -451,6 +452,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void testUpdateFooter_remoteInput() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -467,6 +469,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void testUpdateFooter_withoutNotifications() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -482,6 +485,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void testUpdateFooter_oneClearableNotification() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -497,6 +501,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void testUpdateFooter_withoutHistory() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -513,6 +518,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void testUpdateFooter_oneClearableNotification_beforeUserSetup() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(false); @@ -528,6 +534,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @DisableFlags(FooterViewRefactor.FLAG_NAME) public void testUpdateFooter_oneNonClearableNotification() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -544,9 +551,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - public void testUpdateFooter_atEnd() { - mStackScroller.setCurrentUserSetup(true); - + public void testFooterPosition_atEnd() { // add footer FooterView view = mock(FooterView.class); mStackScroller.setFooterView(view); @@ -559,8 +564,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { // Expecting the footer to be the last child int expected = mStackScroller.getChildCount() - 1; - - // move footer to end verify(mStackScroller).changeViewPosition(any(FooterView.class), eq(expected)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index 88e4f5ab8d55..3a7659dd00e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -27,19 +27,27 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.res.R import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor +import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository import com.android.systemui.statusbar.policy.data.repository.zenModeRepository import com.android.systemui.statusbar.policy.fakeConfigurationController import com.android.systemui.testKosmos +import com.android.systemui.util.ui.isAnimating +import com.android.systemui.util.ui.value import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent @@ -58,11 +66,16 @@ class NotificationListViewModelTest : SysuiTestCase() { fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } } private val testScope = kosmos.testScope + private val activeNotificationListRepository = kosmos.activeNotificationListRepository + private val fakeConfigurationController = kosmos.fakeConfigurationController + private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository private val fakeKeyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val fakePowerRepository = kosmos.fakePowerRepository + private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository private val fakeShadeRepository = kosmos.fakeShadeRepository + private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository private val zenModeRepository = kosmos.zenModeRepository - private val fakeConfigurationController = kosmos.fakeConfigurationController val underTest = kosmos.notificationListViewModel @@ -273,4 +286,193 @@ class NotificationListViewModelTest : SysuiTestCase() { assertThat(hasFilteredNotifs).isFalse() } + + @Test + fun testShouldShowFooterView_trueWhenShade() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND shade is open + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeShadeRepository.setLegacyShadeExpansion(1f) + runCurrent() + + // THEN footer is visible + assertThat(shouldShow?.value).isTrue() + } + + @Test + fun testShouldShowFooterView_trueWhenLockedShade() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND shade is open on lockscreen + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED) + fakeShadeRepository.setLegacyShadeExpansion(1f) + runCurrent() + + // THEN footer is visible + assertThat(shouldShow?.value).isTrue() + } + + @Test + fun testShouldShowFooterView_falseWhenKeyguard() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND is on keyguard + fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) + runCurrent() + + // THEN footer is not visible + assertThat(shouldShow?.value).isFalse() + } + + @Test + fun testShouldShowFooterView_falseWhenUserNotSetUp() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND shade is open + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeShadeRepository.setLegacyShadeExpansion(1f) + // AND user is not set up + fakeUserSetupRepository.setUserSetUp(false) + runCurrent() + + // THEN footer is not visible + assertThat(shouldShow?.value).isFalse() + } + + @Test + fun testShouldShowFooterView_falseWhenStartingToSleep() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND shade is open + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeShadeRepository.setLegacyShadeExpansion(1f) + // AND device is starting to go to sleep + fakePowerRepository.updateWakefulness(WakefulnessState.STARTING_TO_SLEEP) + runCurrent() + + // THEN footer is not visible + assertThat(shouldShow?.value).isFalse() + } + + @Test + fun testShouldShowFooterView_falseWhenQsExpandedDefault() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND shade is open + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeShadeRepository.setLegacyShadeExpansion(1f) + // AND quick settings are expanded + fakeShadeRepository.setQsExpansion(1f) + fakeShadeRepository.legacyQsFullscreen.value = true + runCurrent() + + // THEN footer is not visible + assertThat(shouldShow?.value).isFalse() + } + + @Test + fun testShouldShowFooterView_trueWhenQsExpandedSplitShade() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND shade is open + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeShadeRepository.setLegacyShadeExpansion(1f) + // AND quick settings are expanded + fakeShadeRepository.setQsExpansion(1f) + // AND split shade is enabled + overrideResource(R.bool.config_use_split_notification_shade, true) + fakeConfigurationController.notifyConfigurationChanged() + runCurrent() + + // THEN footer is visible + assertThat(shouldShow?.value).isTrue() + } + + @Test + fun testShouldShowFooterView_falseWhenRemoteInputActive() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND shade is open + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeShadeRepository.setLegacyShadeExpansion(1f) + // AND remote input is active + fakeRemoteInputRepository.isRemoteInputActive.value = true + runCurrent() + + // THEN footer is not visible + assertThat(shouldShow?.value).isFalse() + } + + @Test + fun testShouldShowFooterView_falseWhenShadeIsClosed() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND shade is closed + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeShadeRepository.setLegacyShadeExpansion(0f) + runCurrent() + + // THEN footer is not visible + assertThat(shouldShow?.value).isFalse() + } + + @Test + fun testShouldShowFooterView_animatesWhenShade() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND shade is open and fully expanded + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeShadeRepository.setLegacyShadeExpansion(1f) + runCurrent() + + // THEN footer visibility animates + assertThat(shouldShow?.isAnimating).isTrue() + } + + @Test + fun testShouldShowFooterView_notAnimatingOnKeyguard() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN has notifs + activeNotificationListRepository.setActiveNotifs(count = 2) + // AND we are on the keyguard + fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) + fakeShadeRepository.setLegacyShadeExpansion(1f) + runCurrent() + + // THEN footer visibility does not animate + assertThat(shouldShow?.isAnimating).isFalse() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt index b6a033a7c5f6..1b4385148f88 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt @@ -46,6 +46,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.process.processWrapper import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.res.R import com.android.systemui.statusbar.policy.DeviceProvisionedController @@ -1147,6 +1148,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { uiEventLogger = uiEventLogger, featureFlags = kosmos.fakeFeatureFlagsClassic, userRestrictionChecker = mock(), + processWrapper = kosmos.processWrapper, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt index 21d4549c904d..661837bdb1e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.process.ProcessWrapperFake import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor @@ -264,6 +265,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { guestUserInteractor = guestUserInteractor, uiEventLogger = uiEventLogger, userRestrictionChecker = mock(), + processWrapper = ProcessWrapperFake() ) ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index d0804be81072..5661e202d134 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.process.ProcessWrapperFake import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor @@ -176,6 +177,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { guestUserInteractor = guestUserInteractor, uiEventLogger = uiEventLogger, userRestrictionChecker = mock(), + processWrapper = ProcessWrapperFake() ), guestUserInteractor = guestUserInteractor, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 7a8dce8fcd90..8a33778f320a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -64,7 +64,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.testing.UiEventLoggerFake; -import com.android.keyguard.TestScopeProvider; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; @@ -102,8 +101,6 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; import java.util.function.Predicate; -import kotlinx.coroutines.Dispatchers; - @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -208,8 +205,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { mDumpManager, mLazySecureSettings, mVibratorHelper, - Dispatchers.getUnconfined(), - TestScopeProvider.getTestScope(), new FakeSystemClock()); mDialog.init(0, null); State state = createShellState(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt index f723a9e57e72..5b84a418181d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt @@ -36,7 +36,7 @@ fun fakeDialogLaunchAnimator( object : AnimationFeatureFlags { override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim }, - launchAnimator = fakeLaunchAnimator(), + transitionAnimator = fakeTransitionAnimator(), isForTesting = true, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeLaunchAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt index 09830413bdc8..bc7ec3f3b6d0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeLaunchAnimator.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt @@ -16,19 +16,19 @@ package com.android.systemui.animation import com.android.app.animation.Interpolators -/** A [LaunchAnimator] to be used in tests. */ -fun fakeLaunchAnimator(): LaunchAnimator { - return LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS) +/** A [TransitionAnimator] to be used in tests. */ +fun fakeTransitionAnimator(): TransitionAnimator { + return TransitionAnimator(TEST_TIMINGS, TEST_INTERPOLATORS) } /** - * A [LaunchAnimator.Timings] to be used in tests. + * A [TransitionAnimator.Timings] to be used in tests. * * Note that all timings except the total duration are non-zero to avoid divide-by-zero exceptions * when computing the progress of a sub-animation (the contents fade in/out). */ private val TEST_TIMINGS = - LaunchAnimator.Timings( + TransitionAnimator.Timings( totalDuration = 0L, contentBeforeFadeOutDelay = 1L, contentBeforeFadeOutDuration = 1L, @@ -36,9 +36,9 @@ private val TEST_TIMINGS = contentAfterFadeInDuration = 1L ) -/** A [LaunchAnimator.Interpolators] to be used in tests. */ +/** A [TransitionAnimator.Interpolators] to be used in tests. */ private val TEST_INTERPOLATORS = - LaunchAnimator.Interpolators( + TransitionAnimator.Interpolators( positionInterpolator = Interpolators.STANDARD, positionXInterpolator = Interpolators.STANDARD, contentBeforeFadeOutInterpolator = Interpolators.STANDARD, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryKosmos.kt new file mode 100644 index 000000000000..5485f79629e8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.app.admin.devicePolicyManager +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.util.settings.fakeSettings + +val Kosmos.communalSettingsRepository: CommunalSettingsRepository by + Kosmos.Fixture { + CommunalSettingsRepositoryImpl( + bgDispatcher = testDispatcher, + featureFlagsClassic = featureFlagsClassic, + secureSettings = fakeSettings, + broadcastDispatcher = broadcastDispatcher, + devicePolicyManager = devicePolicyManager, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt index cccd90832326..ae7d87783b7c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalCoroutinesApi::class) class FakeCommunalRepository( applicationScope: CoroutineScope, - override var isCommunalEnabled: Boolean = true, override val desiredScene: MutableStateFlow<CommunalSceneKey> = MutableStateFlow(CommunalSceneKey.DEFAULT), ) : CommunalRepository { @@ -40,21 +39,10 @@ class FakeCommunalRepository( _transitionState.value = transitionState } - fun setIsCommunalEnabled(value: Boolean) { - isCommunalEnabled = value - } - private val _isCommunalHubShowing: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isCommunalHubShowing: Flow<Boolean> = _isCommunalHubShowing fun setIsCommunalHubShowing(isCommunalHubShowing: Boolean) { _isCommunalHubShowing.value = isCommunalHubShowing } - - private val _communalEnabledState: MutableStateFlow<Boolean> = MutableStateFlow(false) - override val communalEnabledState: StateFlow<Boolean> = _communalEnabledState - - fun setCommunalEnabledState(enabled: Boolean) { - _communalEnabledState.value = enabled - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index c47f020a3b83..f7e9a117aa77 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -27,7 +27,6 @@ import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.smartspace.data.repository.smartspaceRepository -import com.android.systemui.user.data.repository.userRepository import com.android.systemui.util.mockito.mock val Kosmos.communalInteractor by Fixture { @@ -38,12 +37,12 @@ val Kosmos.communalInteractor by Fixture { mediaRepository = communalMediaRepository, communalPrefsRepository = communalPrefsRepository, smartspaceRepository = smartspaceRepository, - userRepository = userRepository, appWidgetHost = mock(), keyguardInteractor = keyguardInteractor, editWidgetsActivityStarter = editWidgetsActivityStarter, logBuffer = logcatLogBuffer("CommunalInteractor"), tableLogBuffer = mock(), + communalSettingsInteractor = communalSettingsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt new file mode 100644 index 000000000000..b4773f69f1c5 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import com.android.systemui.communal.data.repository.communalSettingsRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.user.domain.interactor.selectedUserInteractor +import com.android.systemui.util.mockito.mock + +val Kosmos.communalSettingsInteractor by Fixture { + CommunalSettingsInteractor( + bgScope = applicationCoroutineScope, + repository = communalSettingsRepository, + userInteractor = selectedUserInteractor, + tableLogBuffer = mock(), + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt index 9776b436555d..00fdceda01d1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt @@ -31,6 +31,7 @@ val Kosmos.communalTutorialInteractor by keyguardInteractor = keyguardInteractor, communalRepository = communalRepository, communalInteractor = communalInteractor, + communalSettingsInteractor = communalSettingsInteractor, tableLogBuffer = mock(), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt index 21cff0dbde2d..3b3e23e524c1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt @@ -18,5 +18,7 @@ package com.android.systemui.deviceentry.data.repository import com.android.systemui.kosmos.Kosmos +var Kosmos.fakeFaceWakeUpTriggersConfig by Kosmos.Fixture { FakeFaceWakeUpTriggersConfig() } + var Kosmos.faceWakeUpTriggersConfig: FaceWakeUpTriggersConfig by - Kosmos.Fixture { FakeFaceWakeUpTriggersConfig() } + Kosmos.Fixture { fakeFaceWakeUpTriggersConfig } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt index 5575b05b3874..a8fc27a7da4e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt @@ -27,7 +27,6 @@ import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteract import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -51,7 +50,7 @@ val Kosmos.deviceEntryFaceAuthInteractor by keyguardTransitionInteractor = keyguardTransitionInteractor, faceAuthenticationLogger = faceAuthLogger, keyguardUpdateMonitor = keyguardUpdateMonitor, - deviceEntryFingerprintAuthRepository = deviceEntryFingerprintAuthRepository, + deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, userRepository = userRepository, facePropertyRepository = facePropertyRepository, faceWakeUpTriggersConfig = faceWakeUpTriggersConfig, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt index a8f45b0974c4..6f168d47038d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt @@ -33,6 +33,7 @@ var Kosmos.aodBurnInViewModel by Fixture { keyguardInteractor = keyguardInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, goneToAodTransitionViewModel = goneToAodTransitionViewModel, + aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, keyguardClockViewModel = keyguardClockViewModel, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt index 9841778f835b..dee3644e95bd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt @@ -16,11 +16,17 @@ package com.android.systemui.process +import android.os.UserHandle + class ProcessWrapperFake : ProcessWrapper() { var systemUser: Boolean = false + var userHandle: UserHandle = UserHandle.getUserHandleForUid(0) + override fun isSystemUser(): Boolean { return systemUser } + + override fun myUserHandle() = userHandle } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt index e67df9dde871..8e430dbcf828 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt @@ -26,6 +26,8 @@ import kotlinx.coroutines.flow.filterNotNull class FakeQSSceneAdapter( private val inflateDelegate: suspend (Context) -> View, + override val qqsHeight: Int = 0, + override val qsHeight: Int = 0, ) : QSSceneAdapter { private val _customizing = MutableStateFlow(false) override val isCustomizing: StateFlow<Boolean> = _customizing.asStateFlow() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt index 998e579b14fc..37b2b765c9bd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt @@ -16,10 +16,14 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.shade.domain.interactor.shadeAnimationInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel @@ -30,14 +34,18 @@ import java.util.Optional val Kosmos.notificationListViewModel by Fixture { NotificationListViewModel( - shelf = notificationShelfViewModel, - hideListViewModel = hideListViewModel, - footer = Optional.of(footerViewModel), - logger = Optional.of(notificationListLoggerViewModel), - activeNotificationsInteractor = activeNotificationsInteractor, - keyguardTransitionInteractor = keyguardTransitionInteractor, - seenNotificationsInteractor = seenNotificationsInteractor, - shadeInteractor = shadeInteractor, - zenModeInteractor = zenModeInteractor, + notificationShelfViewModel, + hideListViewModel, + Optional.of(footerViewModel), + Optional.of(notificationListLoggerViewModel), + activeNotificationsInteractor, + keyguardInteractor, + keyguardTransitionInteractor, + powerInteractor, + remoteInputInteractor, + seenNotificationsInteractor, + shadeInteractor, + userSetupInteractor, + zenModeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt index 4e2dc7af8cb4..1504df4ef6d0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt @@ -28,6 +28,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.plugins.activityStarter +import com.android.systemui.process.processWrapper import com.android.systemui.telephony.domain.interactor.telephonyInteractor import com.android.systemui.user.data.repository.userRepository import com.android.systemui.utils.userRestrictionChecker @@ -53,5 +54,6 @@ val Kosmos.userSwitcherInteractor by guestUserInteractor = guestUserInteractor, uiEventLogger = uiEventLogger, userRestrictionChecker = userRestrictionChecker, + processWrapper = processWrapper, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt new file mode 100644 index 000000000000..bcb584858e32 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.settings + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.fakeSettings: FakeSettings by Fixture { FakeSettings() } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index fd8ab9683831..e1291e5f75ec 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -451,7 +451,7 @@ final class AutofillManagerServiceImpl final Session.SaveResult saveResult = session.showSaveLocked(); - session.logContextCommitted(saveResult.getNoSaveUiReason(), commitReason); + session.logContextCommittedLocked(saveResult.getNoSaveUiReason(), commitReason); if (saveResult.isLogSaveShown()) { session.logSaveUiShown(); diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 83d9cdbd2843..b89e0d8c72df 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -16,6 +16,8 @@ package com.android.server.autofill; +import static android.credentials.Constants.FAILURE_CREDMAN_SELECTOR; +import static android.credentials.Constants.SUCCESS_CREDMAN_SELECTOR; import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES; import static android.service.autofill.AutofillService.EXTRA_FILL_RESPONSE; import static android.service.autofill.AutofillService.WEBVIEW_REQUESTED_CREDENTIAL_KEY; @@ -113,6 +115,8 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.pm.ServiceInfo; +import android.credentials.GetCredentialException; +import android.credentials.GetCredentialResponse; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -123,10 +127,12 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.IBinder.DeathRecipient; +import android.os.Parcel; import android.os.Parcelable; import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.SystemClock; import android.service.assist.classification.FieldClassificationRequest; import android.service.assist.classification.FieldClassificationResponse; @@ -153,6 +159,7 @@ import android.service.autofill.SaveInfo; import android.service.autofill.SaveRequest; import android.service.autofill.UserData; import android.service.autofill.ValueFinder; +import android.service.credentials.CredentialProviderService; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -2507,7 +2514,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + id + " destroyed"); return; } - fillInIntent = createAuthFillInIntentLocked(requestId, extras, /* authExtras= */ null); + fillInIntent = createAuthFillInIntentLocked(requestId, extras); if (fillInIntent == null) { forceRemoveFromServiceLocked(); return; @@ -2808,6 +2815,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mSessionFlags.mExpiredResponse = false; final Parcelable result = data.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT); + final Bundle newClientState = data.getBundle(AutofillManager.EXTRA_CLIENT_STATE); if (sDebug) { Slog.d(TAG, "setAuthenticationResultLocked(): result=" + result @@ -2818,6 +2826,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mPresentationStatsEventLogger.maybeSetAuthenticationResult( AUTHENTICATION_RESULT_SUCCESS); replaceResponseLocked(authenticatedResponse, (FillResponse) result, newClientState); + } else if (result instanceof GetCredentialResponse) { + Slog.d(TAG, "Received GetCredentialResponse from authentication flow"); + Dataset dataset = getDatasetFromCredentialResponse((GetCredentialResponse) result); + if (dataset != null) { + autoFill(requestId, datasetIdx, dataset, false, UI_TYPE_UNKNOWN); + } } else if (result instanceof Dataset) { if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) { logAuthenticationStatusLocked(requestId, @@ -2854,6 +2868,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + private Dataset getDatasetFromCredentialResponse(GetCredentialResponse result) { + if (result == null) { + return null; + } + Bundle bundle = result.getCredential().getData(); + if (bundle == null) { + return null; + } + return bundle.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT, Dataset.class); + } + Dataset getEffectiveDatasetForAuthentication(Dataset authenticatedDataset) { FillResponse response = new FillResponse.Builder().addDataset(authenticatedDataset).build(); response = getEffectiveFillResponse(response); @@ -3061,6 +3086,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState * when necessary. */ public void logContextCommitted() { + if (sVerbose) { + Slog.v(TAG, "logContextCommitted (" + id + "): commit_reason:" + COMMIT_REASON_UNKNOWN + + " no_save_reason:" + Event.NO_SAVE_UI_REASON_NONE); + } mHandler.sendMessage(obtainMessage(Session::handleLogContextCommitted, this, Event.NO_SAVE_UI_REASON_NONE, COMMIT_REASON_UNKNOWN)); @@ -3069,16 +3098,26 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState /** * Generates a {@link android.service.autofill.FillEventHistory.Event#TYPE_CONTEXT_COMMITTED} - * when necessary. + * when necessary. Note that it could be called before save UI is shown and the session is + * committed. * * @param saveDialogNotShowReason The reason why a save dialog was not shown. * @param commitReason The reason why context is committed. */ - public void logContextCommitted(@NoSaveReason int saveDialogNotShowReason, + + @GuardedBy("mLock") + public void logContextCommittedLocked(@NoSaveReason int saveDialogNotShowReason, @AutofillCommitReason int commitReason) { + if (sVerbose) { + Slog.v(TAG, "logContextCommittedLocked (" + id + "): commit_reason:" + commitReason + + " no_save_reason:" + saveDialogNotShowReason); + } mHandler.sendMessage(obtainMessage(Session::handleLogContextCommitted, this, saveDialogNotShowReason, commitReason)); - logAllEvents(commitReason); + + mSessionCommittedEventLogger.maybeSetCommitReason(commitReason); + mSessionCommittedEventLogger.maybeSetRequestCount(mRequestCount); + mSaveEventLogger.maybeSetSaveUiNotShownReason(NO_SAVE_REASON_NONE); } private void handleLogContextCommitted(@NoSaveReason int saveDialogNotShowReason, @@ -3134,6 +3173,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @Nullable ArrayList<FieldClassification> detectedFieldClassifications, @NoSaveReason int saveDialogNotShowReason, @AutofillCommitReason int commitReason) { + if (sVerbose) { + Slog.v(TAG, "logContextCommittedLocked (" + id + "): commit_reason:" + commitReason + + " no_save_reason:" + saveDialogNotShowReason); + } final FillResponse lastResponse = getLastResponseLocked("logContextCommited(%s)"); if (lastResponse == null) return; @@ -3310,7 +3353,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState changedFieldIds, changedDatasetIds, manuallyFilledFieldIds, manuallyFilledDatasetIds, detectedFieldIds, detectedFieldClassifications, mComponentName, mCompatMode, saveDialogNotShowReason); - logAllEvents(commitReason); + mSessionCommittedEventLogger.maybeSetCommitReason(commitReason); + mSessionCommittedEventLogger.maybeSetRequestCount(mRequestCount); + mSaveEventLogger.maybeSetSaveUiNotShownReason(saveDialogNotShowReason); } /** @@ -3751,11 +3796,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } - if (sDebug) { - Slog.d(TAG, "Good news, everyone! All checks passed, show save UI for " - + id + "!"); - } - final IAutoFillManagerClient client = getClient(); mPendingSaveUi = new PendingUi(new Binder(), id, client); @@ -3787,6 +3827,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } mSessionFlags.mShowingSaveUi = true; + if (sDebug) { + Slog.d(TAG, "Good news, everyone! All checks passed, show save UI for " + + id + "!"); + } return new SaveResult(/* logSaveShown= */ true, /* removeSession= */ false, Event.NO_SAVE_UI_REASON_NONE); } @@ -4690,6 +4734,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } + if (isCredmanIntegrationActive(response)) { + Slog.d(TAG, "Attempting to add Credential Manager callback to pinned entries"); + addCredentialManagerCallback(response); + } + if (response.supportsInlineSuggestions()) { synchronized (mLock) { if (requestShowInlineSuggestionsLocked(response, filterText)) { @@ -4749,6 +4798,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + private boolean isCredmanIntegrationActive(FillResponse response) { + return Flags.autofillCredmanIntegration() + && (response.getFlags() & FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE) != 0; + } + @GuardedBy("mLock") private void updateFillDialogTriggerIdsLocked() { final FillResponse response = getLastResponseLocked(null); @@ -4964,6 +5018,69 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return mInlineSessionController.setInlineFillUiLocked(inlineFillUi); } + private void addCredentialManagerCallback(FillResponse response) { + if (response.getDatasets() == null) { + return; + } + for (Dataset dataset: response.getDatasets()) { + if (isPinnedDataset(dataset)) { + Slog.d(TAG, "Adding Credential Manager callback to a pinned entry"); + addCredentialManagerCallbackForDataset(dataset, response.getRequestId()); + } + } + } + + private void addCredentialManagerCallbackForDataset(Dataset dataset, int requestId) { + final ResultReceiver resultReceiver = new ResultReceiver(mHandler) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == SUCCESS_CREDMAN_SELECTOR) { + Slog.d(TAG, "onReceiveResult from Credential Manager bottom sheet"); + GetCredentialResponse getCredentialResponse = + resultData.getParcelable( + CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE, + GetCredentialResponse.class); + Dataset datasetFromCredential = getDatasetFromCredentialResponse( + getCredentialResponse); + if (datasetFromCredential != null) { + autoFill(requestId, /*datasetIndex=*/-1, + datasetFromCredential, false, + UI_TYPE_CREDMAN_BOTTOM_SHEET); + } + } else if (resultCode == FAILURE_CREDMAN_SELECTOR) { + GetCredentialException exception = resultData.getParcelable( + CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION, + GetCredentialException.class); + Slog.d(TAG, "Credman bottom sheet from pinned " + + "entry failed with: + " + exception.getType() + " , " + + exception.getMessage()); + } else { + Slog.d(TAG, "Unknown resultCode from credential " + + "manager bottom sheet: " + resultCode); + } + } + }; + ResultReceiver ipcFriendlyResultReceiver = + toIpcFriendlyResultReceiver(resultReceiver); + + Intent metadataIntent = dataset.getCredentialFillInIntent(); + metadataIntent.putExtra( + android.credentials.selection.Constants.EXTRA_FINAL_RESPONSE_RECEIVER, + ipcFriendlyResultReceiver); + dataset.setCredentialFillInIntent(metadataIntent); + } + + private ResultReceiver toIpcFriendlyResultReceiver(ResultReceiver resultReceiver) { + final Parcel parcel = Parcel.obtain(); + resultReceiver.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel); + parcel.recycle(); + + return ipcFriendly; + } + boolean isDestroyed() { synchronized (mLock) { return mDestroyed; @@ -5669,8 +5786,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // does not matter the value of isPrimary because null response won't be overridden. setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH, /* clearResponse= */ false, /* isPrimary= */ true); - final Intent fillInIntent = createAuthFillInIntentLocked(requestId, mClientState, - dataset.getAuthenticationExtras()); + final Intent fillInIntent; + if (dataset.getCredentialFillInIntent() != null && Flags.autofillCredmanIntegration()) { + Slog.d(TAG, "Setting credential fill intent"); + fillInIntent = dataset.getCredentialFillInIntent(); + } else { + fillInIntent = createAuthFillInIntentLocked(requestId, mClientState); + } + if (fillInIntent == null) { forceRemoveFromServiceLocked(); return; @@ -5686,8 +5809,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // TODO: this should never be null, but we got at least one occurrence, probably due to a race. @GuardedBy("mLock") @Nullable - private Intent createAuthFillInIntentLocked(int requestId, Bundle extras, - @Nullable Bundle authExtras) { + private Intent createAuthFillInIntentLocked(int requestId, Bundle extras) { final Intent fillInIntent = new Intent(); final FillContext context = getFillContextByRequestIdLocked(requestId); @@ -5704,9 +5826,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, context.getStructure()); fillInIntent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, extras); - if (authExtras != null) { - fillInIntent.putExtra(AutofillManager.EXTRA_AUTH_STATE, authExtras); - } return fillInIntent; } @@ -6286,6 +6405,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") private void logAllEvents(@AutofillCommitReason int val) { + if (sVerbose) { + Slog.v(TAG, "logAllEvents(" + id + "): commitReason: " + val); + } mSessionCommittedEventLogger.maybeSetCommitReason(val); mSessionCommittedEventLogger.maybeSetRequestCount(mRequestCount); mSessionCommittedEventLogger.maybeSetSessionDurationMillis( @@ -6311,6 +6433,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") RemoteFillService destroyLocked() { // Log unlogged events. + if (sVerbose) { + Slog.v(TAG, "destroyLocked for session: " + id); + } logAllEvents(COMMIT_REASON_SESSION_DESTROYED); if (mDestroyed) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 0054bc87af77..b43f1a93f183 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -1305,6 +1305,8 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore.dump(out); mDevicePresenceMonitor.dump(out); mCompanionAppController.dump(out); + mTransportManager.dump(out); + mSystemDataTransferRequestStore.dump(out); } } diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java index 51c5fd69cdf2..c4c80f907b3a 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java @@ -48,6 +48,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -303,6 +304,32 @@ public class SystemDataTransferRequestStore { } } + + + /** + * Dumps current system data transfer request states. + */ + public void dump(@NonNull PrintWriter out) { + synchronized (mLock) { + out.append("System Data Transfer Requests (Cached): "); + if (mCachedPerUser.size() == 0) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int i = 0; i < mCachedPerUser.size(); i++) { + final int userId = mCachedPerUser.keyAt(i); + for (SystemDataTransferRequest request : mCachedPerUser.get(userId)) { + out.append(" u") + .append(String.valueOf(userId)) + .append(" -> ") + .append(request.toString()) + .append('\n'); + } + } + } + } + } + private void writeRequestsToXml(@NonNull TypedXmlSerializer serializer, @Nullable Collection<SystemDataTransferRequest> requests) throws IOException { serializer.startTag(null, XML_TAG_REQUESTS); diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java index 3e45626d9799..3861f99eb03c 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -36,6 +36,7 @@ import com.android.server.companion.AssociationStore; import java.io.FileDescriptor; import java.io.IOException; +import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -225,6 +226,25 @@ public class CompanionTransportManager { } /** + * Dumps current list of active transports. + */ + public void dump(@NonNull PrintWriter out) { + synchronized (mTransports) { + out.append("System Data Transports: "); + if (mTransports.size() == 0) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int i = 0; i < mTransports.size(); i++) { + final int associationId = mTransports.keyAt(i); + final Transport transport = mTransports.get(associationId); + out.append(" ").append(transport.toString()).append('\n'); + } + } + } + } + + /** * @hide */ public void enableSecureTransport(boolean enabled) { diff --git a/services/companion/java/com/android/server/companion/transport/RawTransport.java b/services/companion/java/com/android/server/companion/transport/RawTransport.java index ca169aac6a37..05703ce21ca4 100644 --- a/services/companion/java/com/android/server/companion/transport/RawTransport.java +++ b/services/companion/java/com/android/server/companion/transport/RawTransport.java @@ -94,6 +94,13 @@ class RawTransport extends Transport { } } + @Override + public String toString() { + return "RawTransport{" + + "mAssociationId=" + mAssociationId + + '}'; + } + private void receiveMessage() throws IOException { synchronized (mRemoteIn) { final byte[] headerBytes = new byte[HEADER_LENGTH]; diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java index 6e906ebe887a..1e95e65848a5 100644 --- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java +++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java @@ -152,4 +152,12 @@ class SecureTransport extends Transport implements SecureChannel.Callback { close(); } } + + @Override + public String toString() { + return "SecureTransport{" + + "mAssociationId=" + mAssociationId + + ", mSecureChannel=" + mSecureChannel + + '}'; + } } diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index 8962bf02ff2e..1b49f18e60cf 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -692,32 +692,37 @@ class InputController { private int mInputDeviceId = IInputConstants.INVALID_INPUT_DEVICE_ID; - WaitForDevice(String deviceName, int vendorId, int productId) { + WaitForDevice(String deviceName, int vendorId, int productId, int associatedDisplayId) { mListener = new InputManager.InputDeviceListener() { @Override public void onInputDeviceAdded(int deviceId) { - final InputDevice device = InputManagerGlobal.getInstance().getInputDevice( - deviceId); - Objects.requireNonNull(device, "Newly added input device was null."); - if (!device.getName().equals(deviceName)) { - return; - } - final InputDeviceIdentifier id = device.getIdentifier(); - if (id.getVendorId() != vendorId || id.getProductId() != productId) { - return; - } - mInputDeviceId = deviceId; - mDeviceAddedLatch.countDown(); + onInputDeviceChanged(deviceId); } @Override public void onInputDeviceRemoved(int deviceId) { - } @Override public void onInputDeviceChanged(int deviceId) { + if (isMatchingDevice(deviceId)) { + mInputDeviceId = deviceId; + mDeviceAddedLatch.countDown(); + } + } + private boolean isMatchingDevice(int deviceId) { + final InputDevice device = InputManagerGlobal.getInstance().getInputDevice( + deviceId); + Objects.requireNonNull(device, "Newly added input device was null."); + if (!device.getName().equals(deviceName)) { + return false; + } + final InputDeviceIdentifier id = device.getIdentifier(); + if (id.getVendorId() != vendorId || id.getProductId() != productId) { + return false; + } + return device.getAssociatedDisplayId() == associatedDisplayId; } }; InputManagerGlobal.getInstance().registerInputDeviceListener(mListener, mHandler); @@ -799,7 +804,7 @@ class InputController { final int inputDeviceId; setUniqueIdAssociation(displayId, phys); - try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) { + try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId, displayId)) { ptr = deviceOpener.get(); // See INVALID_PTR in libs/input/VirtualInputDevice.cpp. if (ptr == 0) { diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java index 9554e63b882b..fb527c104946 100644 --- a/services/core/java/com/android/server/DockObserver.java +++ b/services/core/java/com/android/server/DockObserver.java @@ -20,8 +20,9 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; -import android.media.AudioAttributes; +import android.media.AudioManager; import android.media.Ringtone; +import android.media.RingtoneManager; import android.net.Uri; import android.os.Binder; import android.os.Handler; @@ -305,16 +306,11 @@ final class DockObserver extends SystemService { if (soundPath != null) { final Uri soundUri = Uri.parse("file://" + soundPath); if (soundUri != null) { - AudioAttributes audioAttributes = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build(); - final Ringtone sfx = new Ringtone.Builder(getContext(), - Ringtone.MEDIA_SOUND, audioAttributes) - .setUri(soundUri) - .setPreferBuiltinDevice() - .build(); + final Ringtone sfx = RingtoneManager.getRingtone( + getContext(), soundUri); if (sfx != null) { + sfx.setStreamType(AudioManager.STREAM_SYSTEM); + sfx.preferBuiltinDevice(true); sfx.play(); } } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index adc0255743c2..cd45b03ba7ad 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -581,7 +581,8 @@ public final class ActiveServices { if (DEBUG_FOREGROUND_SERVICE) { Slog.i(TAG, " Stopping fg for service " + r); } - setServiceForegroundInnerLocked(r, 0, null, 0, 0); + setServiceForegroundInnerLocked(r, 0, null, 0, 0, + 0); } } @@ -989,7 +990,7 @@ public final class ActiveServices { if (fgRequired) { logFgsBackgroundStart(r); - if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabled(r)) { + if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabled(r, callingUid)) { String msg = "startForegroundService() not allowed due to " + "mAllowStartForeground false: service " + r.shortInstanceName; @@ -1787,11 +1788,13 @@ public final class ActiveServices { public void setServiceForegroundLocked(ComponentName className, IBinder token, int id, Notification notification, int flags, int foregroundServiceType) { final int userId = UserHandle.getCallingUserId(); + final int callingUid = mAm.mInjector.getCallingUid(); final long origId = mAm.mInjector.clearCallingIdentity(); try { ServiceRecord r = findServiceLocked(className, token, userId); if (r != null) { - setServiceForegroundInnerLocked(r, id, notification, flags, foregroundServiceType); + setServiceForegroundInnerLocked(r, id, notification, flags, foregroundServiceType, + callingUid); } } finally { mAm.mInjector.restoreCallingIdentity(origId); @@ -2106,7 +2109,8 @@ public final class ActiveServices { */ @GuardedBy("mAm") private void setServiceForegroundInnerLocked(final ServiceRecord r, int id, - Notification notification, int flags, int foregroundServiceType) { + Notification notification, int flags, int foregroundServiceType, + int callingUidIfStart) { if (id != 0) { if (notification == null) { throw new IllegalArgumentException("null notification"); @@ -2234,7 +2238,8 @@ public final class ActiveServices { } // Whether FGS-BG-start restriction is enabled for this service. - final boolean isBgFgsRestrictionEnabledForService = isBgFgsRestrictionEnabled(r); + final boolean isBgFgsRestrictionEnabledForService = isBgFgsRestrictionEnabled(r, + callingUidIfStart); // Whether to extend the SHORT_SERVICE time out. boolean extendShortServiceTimeout = false; @@ -8486,14 +8491,43 @@ public final class ActiveServices { NOTE_FOREGROUND_SERVICE_BG_LAUNCH, n.build(), UserHandle.ALL); } - private boolean isBgFgsRestrictionEnabled(ServiceRecord r) { - return mAm.mConstants.mFlagFgsStartRestrictionEnabled - // Checking service's targetSdkVersion. - && CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, r.appInfo.uid) - && (!mAm.mConstants.mFgsStartRestrictionCheckCallerTargetSdk - // Checking callingUid's targetSdkVersion. - || CompatChanges.isChangeEnabled( - FGS_BG_START_RESTRICTION_CHANGE_ID, r.mRecentCallingUid)); + private boolean isBgFgsRestrictionEnabled(ServiceRecord r, int actualCallingUid) { + // mFlagFgsStartRestrictionEnabled controls whether to enable the BG FGS restrictions: + // - If true (default), BG-FGS restrictions are enabled if the service targets >= S. + // - If false, BG-FGS restrictions are disabled for all apps. + if (!mAm.mConstants.mFlagFgsStartRestrictionEnabled) { + return false; + } + + // If the service target below S, then don't enable the restrictions. + if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, r.appInfo.uid)) { + return false; + } + + // mFgsStartRestrictionCheckCallerTargetSdk controls whether we take the caller's target + // SDK level into account or not: + // - If true (default), BG-FGS restrictions only happens if the caller _also_ targets >= S. + // - If false, BG-FGS restrictions do _not_ use the caller SDK levels. + if (!mAm.mConstants.mFgsStartRestrictionCheckCallerTargetSdk) { + return true; // In this case, we only check the service's target SDK level. + } + final int callingUid; + if (Flags.newFgsRestrictionLogic()) { + // We always consider SYSTEM_UID to target S+, so just enable the restrictions. + if (actualCallingUid == Process.SYSTEM_UID) { + return true; + } + callingUid = actualCallingUid; + } else { + // Legacy logic used mRecentCallingUid. + callingUid = r.mRecentCallingUid; + } + if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, callingUid)) { + return false; // If the caller targets < S, then we still disable the restrictions. + } + + // Both the service and the caller target S+, so enable the check. + return true; } private void logFgsBackgroundStart(ServiceRecord r) { diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java index fbd32a67fe6c..d061e2d21811 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java @@ -31,6 +31,7 @@ import android.util.Slog; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.fingerprint.aidl.AidlSession; import java.util.function.Supplier; @@ -202,6 +203,16 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement } } + // TODO(b/317414324): Deprecate setIgnoreDisplayTouches + protected final void resetIgnoreDisplayTouches() { + final AidlSession session = (AidlSession) getFreshDaemon(); + try { + session.getSession().setIgnoreDisplayTouches(false); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when resetting setIgnoreDisplayTouches"); + } + } + @Override public boolean isInterruptable() { return true; diff --git a/services/core/java/com/android/server/biometrics/sensors/LockoutResetDispatcher.java b/services/core/java/com/android/server/biometrics/sensors/LockoutResetDispatcher.java index 92218b1023c4..199db8c48c7c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/LockoutResetDispatcher.java +++ b/services/core/java/com/android/server/biometrics/sensors/LockoutResetDispatcher.java @@ -27,9 +27,8 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; -import java.util.ArrayList; import java.util.Iterator; -import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; /** * Allows clients (such as keyguard) to register for notifications on when biometric lockout @@ -42,7 +41,7 @@ public class LockoutResetDispatcher implements IBinder.DeathRecipient { private final Context mContext; @VisibleForTesting - final List<ClientCallback> mClientCallbacks = new ArrayList<>(); + final ConcurrentLinkedQueue<ClientCallback> mClientCallbacks = new ConcurrentLinkedQueue<>(); private static class ClientCallback { private static final long WAKELOCK_TIMEOUT_MS = 2000; diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 8121a639ab0a..93d1b6e079ca 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -232,6 +232,7 @@ public class FingerprintAuthenticationClient handleLockout(authenticated); if (authenticated) { mState = STATE_STOPPED; + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); @@ -268,6 +269,7 @@ public class FingerprintAuthenticationClient // Send the error, but do not invoke the FinishCallback yet. Since lockout is not // controlled by the HAL, the framework must stop the sensor before finishing the // client. + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); @@ -298,6 +300,7 @@ public class FingerprintAuthenticationClient BiometricNotificationUtils.showBadCalibrationNotification(getContext()); } + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); @@ -306,6 +309,7 @@ public class FingerprintAuthenticationClient @Override protected void startHalOperation() { + resetIgnoreDisplayTouches(); mSensorOverlays.show(getSensorId(), getRequestReason(), this); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStarted(getRequestReason()); @@ -419,6 +423,7 @@ public class FingerprintAuthenticationClient @Override protected void stopHalOperation() { + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); @@ -518,6 +523,7 @@ public class FingerprintAuthenticationClient Slog.e(TAG, "Remote exception", e); } + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); @@ -548,6 +554,7 @@ public class FingerprintAuthenticationClient Slog.e(TAG, "Remote exception", e); } + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java index cb220b9e1c34..8d2b46fe743d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java @@ -87,6 +87,7 @@ public class FingerprintDetectClient extends AcquisitionClient<AidlSession> @Override protected void stopHalOperation() { + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); unsubscribeBiometricContext(); @@ -102,6 +103,7 @@ public class FingerprintDetectClient extends AcquisitionClient<AidlSession> @Override protected void startHalOperation() { + resetIgnoreDisplayTouches(); mSensorOverlays.show(getSensorId(), BiometricRequestConstants.REASON_AUTH_KEYGUARD, this); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java index 225bd594adc6..79975e515c70 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java @@ -144,6 +144,7 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement controller -> controller.onEnrollmentProgress(getSensorId(), remaining)); if (remaining == 0) { + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); @@ -178,6 +179,7 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement @Override public void onError(int errorCode, int vendorCode) { super.onError(errorCode, vendorCode); + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); @@ -192,6 +194,7 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement @Override protected void startHalOperation() { + resetIgnoreDisplayTouches(); mSensorOverlays.show(getSensorId(), getRequestReasonFromEnrollReason(mEnrollReason), this); if (sidefpsControllerRefactor()) { @@ -273,6 +276,7 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement @Override protected void stopHalOperation() { + resetIgnoreDisplayTouches(); mSensorOverlays.hide(getSensorId()); if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index 458fd82d9a65..05e681edb05e 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -1020,6 +1020,10 @@ public class CameraServiceProxy extends SystemService } } + private boolean isAutomotive() { + return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); + } + private Set<Integer> getEnabledUserHandles(int currentUserHandle) { int[] userProfiles = mUserManager.getEnabledProfileIds(currentUserHandle); Set<Integer> handles = new ArraySet<>(userProfiles.length); @@ -1030,8 +1034,8 @@ public class CameraServiceProxy extends SystemService if (Flags.cameraHsumPermission()) { // If the device is running in headless system user mode then allow - // User 0 to access camera. - if (UserManager.isHeadlessSystemUserMode()) { + // User 0 to access camera only for automotive form factor. + if (UserManager.isHeadlessSystemUserMode() && isAutomotive()) { handles.add(UserHandle.USER_SYSTEM); } } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 7726609e7075..574be34e56e5 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -621,10 +621,6 @@ public class InputManagerService extends IInputManager.Stub mBatteryController.systemRunning(); mKeyboardBacklightController.systemRunning(); mKeyRemapper.systemRunning(); - - mNative.setStylusPointerIconEnabled( - Objects.requireNonNull(mContext.getSystemService(InputManager.class)) - .isStylusPointerIconEnabled()); } private void reloadDeviceAliases() { diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java index 5ffc3809ec98..c02d5249fdce 100644 --- a/services/core/java/com/android/server/input/InputSettingsObserver.java +++ b/services/core/java/com/android/server/input/InputSettingsObserver.java @@ -94,7 +94,9 @@ class InputSettingsObserver extends ContentObserver { Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SLOW_KEYS), (reason) -> updateAccessibilitySlowKeys()), Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_STICKY_KEYS), - (reason) -> updateAccessibilityStickyKeys())); + (reason) -> updateAccessibilityStickyKeys()), + Map.entry(Settings.Secure.getUriFor(Settings.Secure.STYLUS_POINTER_ICON_ENABLED), + (reason) -> updateStylusPointerIconEnabled())); } /** @@ -254,4 +256,8 @@ class InputSettingsObserver extends ContentObserver { mNative.setMinTimeBetweenUserActivityPokes(intervalMillis); } } + + private void updateStylusPointerIconEnabled() { + mNative.setStylusPointerIconEnabled(InputSettings.isStylusPointerIconEnabled(mContext)); + } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 91706cf46fc9..7dbe880fea8e 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -727,7 +727,7 @@ public class NotificationManagerService extends SystemService { private static final int MY_UID = Process.myUid(); private static final int MY_PID = Process.myPid(); - private static final IBinder ALLOWLIST_TOKEN = new Binder(); + static final IBinder ALLOWLIST_TOKEN = new Binder(); protected RankingHandler mRankingHandler; private long mLastOverRateLogTime; private float mMaxPackageEnqueueRate = DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE; @@ -4759,7 +4759,7 @@ public class NotificationManagerService extends SystemService { // Remove background token before returning notification to untrusted app, this // ensures the app isn't able to perform background operations that are // associated with notification interactions. - notification.clearAllowlistToken(); + notification.overrideAllowlistToken(null); return new StatusBarNotification( sbn.getPackageName(), sbn.getOpPkg(), @@ -7623,6 +7623,8 @@ public class NotificationManagerService extends SystemService { } } + notification.overrideAllowlistToken(ALLOWLIST_TOKEN); + // Remote views? Are they too big? checkRemoteViews(pkg, tag, id, notification); } diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java new file mode 100644 index 000000000000..155361837071 --- /dev/null +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static android.os.Process.THREAD_PRIORITY_BACKGROUND; + +import android.annotation.NonNull; +import android.app.BackgroundInstallControlManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IRemoteCallback; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.ServiceThread; + +public class BackgroundInstallControlCallbackHelper { + + @VisibleForTesting static final String FLAGGED_PACKAGE_NAME_KEY = "packageName"; + @VisibleForTesting static final String FLAGGED_USER_ID_KEY = "userId"; + private static final String TAG = "BackgroundInstallControlCallbackHelper"; + + private final Handler mHandler; + + BackgroundInstallControlCallbackHelper() { + HandlerThread backgroundThread = + new ServiceThread( + "BackgroundInstallControlCallbackHelperBg", + THREAD_PRIORITY_BACKGROUND, + true); + backgroundThread.start(); + mHandler = new Handler(backgroundThread.getLooper()); + } + + @NonNull @VisibleForTesting + final RemoteCallbackList<IRemoteCallback> mCallbacks = new RemoteCallbackList<>(); + + /** Registers callback that gets invoked upon detection of an MBA + * + * NOTE: The callback is user context agnostic and currently broadcasts to all users of other + * users app installs. This is fine because the API is for SystemServer use only. + */ + public void registerBackgroundInstallCallback(IRemoteCallback callback) { + synchronized (mCallbacks) { + mCallbacks.register(callback, null); + } + } + + /** Unregisters callback */ + public void unregisterBackgroundInstallCallback(IRemoteCallback callback) { + synchronized (mCallbacks) { + mCallbacks.unregister(callback); + } + } + + /** + * Invokes all registered callbacks Callbacks are processed through user provided-threads and + * parameters are passed in via {@link BackgroundInstallControlManager} InstallEvent + */ + public void notifyAllCallbacks(int userId, String packageName) { + Bundle extras = new Bundle(); + extras.putCharSequence(FLAGGED_PACKAGE_NAME_KEY, packageName); + extras.putInt(FLAGGED_USER_ID_KEY, userId); + synchronized (mCallbacks) { + mHandler.post( + () -> + mCallbacks.broadcast( + callback -> { + try { + callback.sendResult(extras); + } catch (RemoteException e) { + Slog.e( + TAG, + "error detected: " + e.getLocalizedMessage(), + e); + } + })); + } + } +} diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index 200b17bc2f97..3468081088a3 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -36,6 +36,7 @@ import android.os.Binder; import android.os.Build; import android.os.Environment; import android.os.Handler; +import android.os.IRemoteCallback; import android.os.Looper; import android.os.Message; import android.os.SystemClock; @@ -93,6 +94,8 @@ public class BackgroundInstallControlService extends SystemService { private final File mDiskFile; private final Context mContext; + private final BackgroundInstallControlCallbackHelper mCallbackHelper; + private SparseSetArray<String> mBackgroundInstalledPackages = null; // User ID -> package name -> set of foreground time frame @@ -112,6 +115,7 @@ public class BackgroundInstallControlService extends SystemService { mHandler = new EventHandler(injector.getLooper(), this); mDiskFile = injector.getDiskFile(); mContext = injector.getContext(); + mCallbackHelper = injector.getBackgroundInstallControlCallbackHelper(); UsageStatsManagerInternal usageStatsManagerInternal = injector.getUsageStatsManagerInternal(); usageStatsManagerInternal.registerListener( @@ -150,6 +154,16 @@ public class BackgroundInstallControlService extends SystemService { } } + @Override + public void registerBackgroundInstallCallback(IRemoteCallback callback) { + mService.mCallbackHelper.registerBackgroundInstallCallback(callback); + } + + @Override + public void unregisterBackgroundInstallCallback(IRemoteCallback callback) { + mService.mCallbackHelper.unregisterBackgroundInstallCallback(callback); + } + } @RequiresPermission(GET_BACKGROUND_INSTALLED_PACKAGES) @@ -274,6 +288,7 @@ public class BackgroundInstallControlService extends SystemService { initBackgroundInstalledPackages(); mBackgroundInstalledPackages.add(userId, packageName); + mCallbackHelper.notifyAllCallbacks(userId, packageName); writeBackgroundInstalledPackagesToDisk(); } @@ -568,6 +583,8 @@ public class BackgroundInstallControlService extends SystemService { File getDiskFile(); + BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper(); + } private static final class InjectorImpl implements Injector { @@ -617,5 +634,10 @@ public class BackgroundInstallControlService extends SystemService { File file = new File(dir, DISK_FILE_NAME); return file; } + + @Override + public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() { + return new BackgroundInstallControlCallbackHelper(); + } } } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 33f481cb2e2b..db5acc2eda09 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -592,9 +592,11 @@ final class InstallPackageHelper { mPm.addAllPackageProperties(pkg); if (oldPkgSetting == null || oldPkgSetting.getPkg() == null) { - mPm.mDomainVerificationManager.addPackage(pkgSetting); + mPm.mDomainVerificationManager.addPackage(pkgSetting, + request.getPreVerifiedDomains()); } else { - mPm.mDomainVerificationManager.migrateState(oldPkgSetting, pkgSetting); + mPm.mDomainVerificationManager.migrateState(oldPkgSetting, pkgSetting, + request.getPreVerifiedDomains()); } int collectionSize = ArrayUtils.size(pkg.getInstrumentations()); diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index b8960da15389..afd4fb17dff5 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -569,7 +569,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService * target sdk apps as malware can target older sdk versions to avoid * the enforcement of new API behavior. */ - public static final int MIN_INSTALLABLE_TARGET_SDK = Build.VERSION_CODES.M; + public static final int MIN_INSTALLABLE_TARGET_SDK = + Flags.minTargetSdk24() ? Build.VERSION_CODES.N : Build.VERSION_CODES.M; // Compilation reasons. // TODO(b/260124949): Clean this up with the legacy dexopt code. @@ -6483,6 +6484,17 @@ public class PackageManagerService implements PackageSender, TestUtilityService } @Override + @Nullable + public ComponentName getDomainVerificationAgent() { + final int callerUid = Binder.getCallingUid(); + if (!PackageManagerServiceUtils.isRootOrShell(callerUid)) { + throw new SecurityException("Not allowed to query domain verification agent"); + } + final Computer snapshot = snapshotComputer(); + return getDomainVerificationAgentComponentNameLPr(snapshot); + } + + @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { try { diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index e329f09b0c32..89589ed30f5a 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -396,6 +396,8 @@ class PackageManagerShellCommand extends ShellCommand { return runArchive(); case "request-unarchive": return runUnarchive(); + case "get-domain-verification-agent": + return runGetDomainVerificationAgent(); default: { if (ART_SERVICE_COMMANDS.contains(cmd)) { if (DexOptHelper.useArtService()) { @@ -4794,6 +4796,19 @@ class PackageManagerShellCommand extends ShellCommand { return 0; } + private int runGetDomainVerificationAgent() throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); + try { + final ComponentName domainVerificationAgent = mInterface.getDomainVerificationAgent(); + pw.println(domainVerificationAgent == null + ? "No Domain Verifier available!" : domainVerificationAgent.toString()); + } catch (Exception e) { + pw.println("Failure [" + e.getMessage() + "]"); + return 1; + } + return 0; + } + @Override public void onHelp() { final PrintWriter pw = getOutPrintWriter(); @@ -5194,6 +5209,9 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" to unarchive an app to the responsible installer. Options are:"); pw.println(" --user: request unarchival of the app from the given user."); pw.println(""); + pw.println(" get-domain-verification-agent"); + pw.println(" Displays the component name of the domain verification agent on device."); + pw.println(""); if (DexOptHelper.useArtService()) { printArtServiceHelp(); } else { diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java index b7203045543e..067a012ed373 100644 --- a/services/core/java/com/android/server/pm/UserTypeFactory.java +++ b/services/core/java/com/android/server/pm/UserTypeFactory.java @@ -288,29 +288,6 @@ public final class UserTypeFactory { * configuration. */ private static UserTypeDetails.Builder getDefaultTypeProfilePrivate() { - UserProperties.Builder userPropertiesBuilder = new UserProperties.Builder() - .setStartWithParent(true) - .setCredentialShareableWithParent(true) - .setAuthAlwaysRequiredToDisableQuietMode(true) - .setAllowStoppingUserWithDelayedLocking(true) - .setMediaSharedWithParent(false) - .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE) - .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE) - .setShowInQuietMode( - UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) - .setShowInSharingSurfaces( - UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) - .setCrossProfileIntentFilterAccessControl( - UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM) - .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT) - .setCrossProfileContentSharingStrategy( - UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) - .setItemsRestrictedOnHomeScreen(true); - if (android.multiuser.Flags.supportHidingProfiles()) { - userPropertiesBuilder.setProfileApiVisibility( - UserProperties.PROFILE_API_VISIBILITY_HIDDEN); - } - return new UserTypeDetails.Builder() .setName(USER_TYPE_PROFILE_PRIVATE) .setBaseType(FLAG_PROFILE) @@ -329,7 +306,26 @@ public final class UserTypeFactory { .setDarkThemeBadgeColors( R.color.white) .setDefaultRestrictions(getDefaultProfileRestrictions()) - .setDefaultUserProperties(userPropertiesBuilder); + .setDefaultUserProperties(new UserProperties.Builder() + .setStartWithParent(true) + .setCredentialShareableWithParent(true) + .setAuthAlwaysRequiredToDisableQuietMode(true) + .setAllowStoppingUserWithDelayedLocking(true) + .setMediaSharedWithParent(false) + .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE) + .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE) + .setShowInQuietMode( + UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) + .setShowInSharingSurfaces( + UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) + .setCrossProfileIntentFilterAccessControl( + UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM) + .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT) + .setCrossProfileContentSharingStrategy( + UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) + .setProfileApiVisibility( + UserProperties.PROFILE_API_VISIBILITY_HIDDEN) + .setItemsRestrictedOnHomeScreen(true)); } /** diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java index 53ee18947f50..7ca449a61d6d 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java @@ -26,6 +26,7 @@ import android.content.pm.IntentFilterVerificationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; +import android.content.pm.verify.domain.DomainSet; import android.content.pm.verify.domain.DomainVerificationInfo; import android.content.pm.verify.domain.DomainVerificationManager; import android.content.pm.verify.domain.DomainVerificationState; @@ -230,13 +231,20 @@ public interface DomainVerificationManagerInternal { * broadcast will be sent to the domain verification agent so it may re-run any verification * logic for the newly associated domains. * <p> - * This will mutate internal {@link DomainVerificationPkgState} and so will hold the internal - * lock. This should never be called from within the domain verification classes themselves. + * Optionally, the caller can specify a set of domains that are already pre-verified by the + * installer. These domains, if specified with autoVerify in the manifest, will be regarded as + * verified as soon as the app is installed, until the domain verification agent sends back the + * real verification results. + * <p> + * This method will mutate internal {@link DomainVerificationPkgState} and so will hold the + * internal lock. This should never be called from within the domain verification classes + * themselves. * <p> * This will NOT call {@link #writeSettings(Computer, TypedXmlSerializer, boolean, int)}. That must be * handled by the caller. */ - void addPackage(@NonNull PackageStateInternal newPkgSetting); + void addPackage(@NonNull PackageStateInternal newPkgSetting, + @Nullable DomainSet preVerifiedDomains); /** * Migrates verification state from a previous install to a new one. It is expected that the @@ -245,14 +253,20 @@ public interface DomainVerificationManagerInternal { * domains under the assumption that the new package will pass the same server side config as * the previous package, as they have matching signatures. * <p> - * This will mutate internal {@link DomainVerificationPkgState} and so will hold the internal - * lock. This should never be called from within the domain verification classes themselves. + * Optionally, the caller can specify a set of domains that are already pre-verified by the + * installer. These domains, if specified with autoVerify in the manifest, will be regarded as + * verified as soon as the app is updated, until the domain verification agent sends back the + * real verification results. + * <p> + * This method will mutate internal {@link DomainVerificationPkgState} and so will hold the + * internal lock. This should never be called from within the domain verification classes + * themselves. * <p> * This will NOT call {@link #writeSettings(Computer, TypedXmlSerializer, boolean, int)}. That must be * handled by the caller. */ void migrateState(@NonNull PackageStateInternal oldPkgSetting, - @NonNull PackageStateInternal newPkgSetting); + @NonNull PackageStateInternal newPkgSetting, @Nullable DomainSet preVerifiedDomains); /** * Serializes the entire internal state. This is equivalent to a full backup of the existing diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java index 6150099b2945..c796b40f11bf 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java @@ -32,6 +32,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.verify.domain.DomainOwner; +import android.content.pm.verify.domain.DomainSet; import android.content.pm.verify.domain.DomainVerificationInfo; import android.content.pm.verify.domain.DomainVerificationManager; import android.content.pm.verify.domain.DomainVerificationState; @@ -859,7 +860,7 @@ public class DomainVerificationService extends SystemService @Override public void migrateState(@NonNull PackageStateInternal oldPkgSetting, - @NonNull PackageStateInternal newPkgSetting) { + @NonNull PackageStateInternal newPkgSetting, @Nullable DomainSet preVerifiedDomains) { String pkgName = newPkgSetting.getPackageName(); boolean sendBroadcast; @@ -935,6 +936,9 @@ public class DomainVerificationService extends SystemService sendBroadcast = hasAutoVerifyDomains && needsBroadcast; + // Apply pre-verified states as the last step of migration + applyPreVerifiedState(newStateMap, newAutoVerifyDomains, preVerifiedDomains); + mAttachedPkgStates.put(pkgName, newDomainSetId, new DomainVerificationPkgState( pkgName, newDomainSetId, hasAutoVerifyDomains, newStateMap, newUserStates, null /* signature */)); @@ -947,7 +951,8 @@ public class DomainVerificationService extends SystemService // TODO(b/159952358): Handle valid domainSetIds for PackageStateInternals with no AndroidPackage @Override - public void addPackage(@NonNull PackageStateInternal newPkgSetting) { + public void addPackage(@NonNull PackageStateInternal newPkgSetting, + @Nullable DomainSet preVerifiedDomains) { // TODO(b/159952358): Optimize packages without any domains. Those wouldn't have to be in // the state map, but it would require handling the "migration" case where an app either // gains or loses all domains. @@ -1029,6 +1034,9 @@ public class DomainVerificationService extends SystemService DomainVerificationState.STATE_MIGRATED); } } + + // Apply pre-verified states before sending out broadcast + applyPreVerifiedState(pkgState.getStateMap(), autoVerifyDomains, preVerifiedDomains); } synchronized (mLock) { @@ -1040,6 +1048,27 @@ public class DomainVerificationService extends SystemService } } + private void applyPreVerifiedState(ArrayMap<String, Integer> stateMap, + ArraySet<String> autoVerifyDomains, + DomainSet preVerifiedDomains) { + // If any pre-verified domains are provided, treating them as verified as well. This + // allows the app to be opened immediately by the corresponding app links, but the + // pre-verified state can still be overwritten by the domain verification agent in the + // future. + if (preVerifiedDomains != null && !autoVerifyDomains.isEmpty()) { + for (String preVerifiedDomain : preVerifiedDomains.getDomains()) { + if (autoVerifyDomains.contains(preVerifiedDomain) + && !stateMap.containsKey(preVerifiedDomain)) { + // Only set the pre-verified state if there's no existing state + stateMap.put(preVerifiedDomain, DomainVerificationState.STATE_PRE_VERIFIED); + if (DEBUG_APPROVAL) { + Slog.d(TAG, "Inserted pre-verified domain: " + preVerifiedDomain); + } + } + } + } + } + /** * Applies any immutable state as the final step when adding or migrating state. Currently only * applies {@link SystemConfig#getLinkedApps()}, which approves all domains for a system app. diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java index 466c4c9a31d6..13b072bc220f 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java @@ -62,6 +62,7 @@ public class DomainVerificationShell { pw.println(" - restored: preserved verification from a user data restore"); pw.println(" - legacy_failure: rejected by a legacy verifier, unknown reason"); pw.println(" - system_configured: automatically approved by the device config"); + pw.println(" - pre_verified: the domain was pre-verified by the installer"); pw.println(" - >= 1024: Custom error code which is specific to the device verifier"); pw.println(" --user <USER_ID>: include user selections (includes all domains, not"); pw.println(" just autoVerify ones)"); diff --git a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java index b2bbcda862ec..e1abae808f10 100644 --- a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java +++ b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java @@ -32,6 +32,8 @@ import android.util.Slog; import com.android.internal.infra.ServiceConnector; +import java.io.IOException; + /** Manages the connection to the remote wearable sensing service. */ final class RemoteWearableSensingService extends ServiceConnector.Impl<IWearableSensingService> { private static final String TAG = @@ -56,6 +58,29 @@ final class RemoteWearableSensingService extends ServiceConnector.Impl<IWearable } /** + * Provides a secure connection to the wearable. + * + * @param secureWearableConnection The secure connection to the wearable + * @param callback The callback for service status + */ + public void provideSecureWearableConnection( + ParcelFileDescriptor secureWearableConnection, RemoteCallback callback) { + if (DEBUG) { + Slog.i(TAG, "Providing secure wearable connection."); + } + var unused = post( + service -> { + service.provideSecureWearableConnection(secureWearableConnection, callback); + try { + // close the local fd after it has been sent to the WSS process + secureWearableConnection.close(); + } catch (IOException ex) { + Slog.w(TAG, "Unable to close the local parcelFileDescriptor.", ex); + } + }); + } + + /** * Provides the implementation a data stream to the wearable. * * @param parcelFileDescriptor The data stream to the wearable @@ -66,7 +91,16 @@ final class RemoteWearableSensingService extends ServiceConnector.Impl<IWearable if (DEBUG) { Slog.i(TAG, "Providing data stream."); } - post(service -> service.provideDataStream(parcelFileDescriptor, callback)); + var unused = post( + service -> { + service.provideDataStream(parcelFileDescriptor, callback); + try { + // close the local fd after it has been sent to the WSS process + parcelFileDescriptor.close(); + } catch (IOException ex) { + Slog.w(TAG, "Unable to close the local parcelFileDescriptor.", ex); + } + }); } /** diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java index e73fd0f400ac..a8d63228775f 100644 --- a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java +++ b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java @@ -22,17 +22,19 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.AppGlobals; import android.app.ambientcontext.AmbientContextEvent; +import android.app.wearable.Flags; import android.app.wearable.WearableSensingManager; +import android.companion.CompanionDeviceManager; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.os.Bundle; -import android.system.OsConstants; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SharedMemory; +import android.system.OsConstants; import android.util.IndentingPrintWriter; import android.util.Slog; @@ -40,6 +42,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.infra.AbstractPerUserSystemService; +import java.io.IOException; import java.io.PrintWriter; /** @@ -55,6 +58,10 @@ final class WearableSensingManagerPerUserService extends RemoteWearableSensingService mRemoteService; private ComponentName mComponentName; + private final Object mSecureChannelLock = new Object(); + + @GuardedBy("mSecureChannelLock") + private WearableSensingSecureChannel mSecureChannel; WearableSensingManagerPerUserService( @NonNull WearableSensingManagerService master, Object lock, @UserIdInt int userId) { @@ -76,6 +83,11 @@ final class WearableSensingManagerPerUserService extends mRemoteService = null; } } + synchronized (mSecureChannelLock) { + if (mSecureChannel != null) { + mSecureChannel.close(); + } + } } @GuardedBy("mLock") @@ -156,6 +168,63 @@ final class WearableSensingManagerPerUserService extends } /** + * Creates a CompanionDeviceManager secure channel and sends a proxy to the wearable sensing + * service. + */ + public void onProvideWearableConnection( + ParcelFileDescriptor wearableConnection, RemoteCallback callback) { + Slog.i(TAG, "onProvideWearableConnection in per user service."); + synchronized (mLock) { + if (!setUpServiceIfNeeded()) { + Slog.w(TAG, "Detection service is not available at this moment."); + notifyStatusCallback(callback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE); + return; + } + } + synchronized (mSecureChannelLock) { + if (mSecureChannel != null) { + // TODO(b/321012559): Kill the WearableSensingService process if it has not been + // killed from onError + mSecureChannel.close(); + } + try { + mSecureChannel = + WearableSensingSecureChannel.create( + getContext().getSystemService(CompanionDeviceManager.class), + wearableConnection, + new WearableSensingSecureChannel.SecureTransportListener() { + @Override + public void onSecureTransportAvailable( + ParcelFileDescriptor secureTransport) { + Slog.i(TAG, "calling over to remote service."); + synchronized (mLock) { + ensureRemoteServiceInitiated(); + mRemoteService.provideSecureWearableConnection( + secureTransport, callback); + } + } + + @Override + public void onError() { + // TODO(b/321012559): Kill the WearableSensingService + // process if mSecureChannel has not been reassigned + if (Flags.enableProvideWearableConnectionApi()) { + notifyStatusCallback( + callback, + WearableSensingManager.STATUS_CHANNEL_ERROR); + } + } + }); + } catch (IOException ex) { + Slog.e(TAG, "Unable to create the secure channel.", ex); + if (Flags.enableProvideWearableConnectionApi()) { + notifyStatusCallback(callback, WearableSensingManager.STATUS_CHANNEL_ERROR); + } + } + } + } + + /** * Handles sending the provided data stream for the wearable to the wearable sensing service. */ public void onProvideDataStream( diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java index 4cc2c025575e..28c8f8769e15 100644 --- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java +++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java @@ -211,9 +211,27 @@ public class WearableSensingManagerService extends private final class WearableSensingManagerInternal extends IWearableSensingManager.Stub { @Override + public void provideWearableConnection( + ParcelFileDescriptor wearableConnection, RemoteCallback callback) { + Slog.i(TAG, "WearableSensingManagerInternal provideWearableConnection."); + Objects.requireNonNull(wearableConnection); + Objects.requireNonNull(callback); + mContext.enforceCallingOrSelfPermission( + Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG); + if (!mIsServiceEnabled) { + Slog.w(TAG, "Service not available."); + WearableSensingManagerPerUserService.notifyStatusCallback( + callback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE); + return; + } + callPerUserServiceIfExist( + service -> service.onProvideWearableConnection(wearableConnection, callback), + callback); + } + + @Override public void provideDataStream( - ParcelFileDescriptor parcelFileDescriptor, - RemoteCallback callback) { + ParcelFileDescriptor parcelFileDescriptor, RemoteCallback callback) { Slog.i(TAG, "WearableSensingManagerInternal provideDataStream."); Objects.requireNonNull(parcelFileDescriptor); Objects.requireNonNull(callback); diff --git a/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java new file mode 100644 index 000000000000..a16ff51e2d20 --- /dev/null +++ b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wearable; + +import android.annotation.NonNull; +import android.companion.AssociationInfo; +import android.companion.AssociationRequest; +import android.companion.CompanionDeviceManager; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.os.ParcelFileDescriptor.AutoCloseInputStream; +import android.os.ParcelFileDescriptor.AutoCloseOutputStream; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * A wrapper that manages a CompanionDeviceManager secure channel for wearable sensing. + * + * <p>This wrapper accepts a connection to a wearable from the caller. It then attaches the + * connection to the CompanionDeviceManager via {@link + * CompanionDeviceManager#attachSystemDataTransport(int, InputStream, OutputStream)}, which will + * create an encrypted channel using the provided connection as the raw underlying connection. The + * wearable device is expected to attach its side of the raw connection to its + * CompanionDeviceManager via the same method so that the two CompanionDeviceManagers on the two + * devices can perform attestation and set up the encrypted channel. Attestation requirements are + * listed in {@link com.android.server.security.AttestationVerificationPeerDeviceVerifier}. + * + * <p>When the encrypted channel is available, it will be provided to the caller via the + * SecureTransportListener. + */ +final class WearableSensingSecureChannel { + + /** A listener for secure transport and its error signal. */ + interface SecureTransportListener { + + /** Called when the secure transport is available. */ + void onSecureTransportAvailable(ParcelFileDescriptor secureTransport); + + /** + * Called when there is a non-recoverable error. The secure channel will be automatically + * closed. + */ + void onError(); + } + + private static final String TAG = WearableSensingSecureChannel.class.getSimpleName(); + private static final String CDM_ASSOCIATION_DISPLAY_NAME = "PlaceholderDisplayNameFromWSM"; + // The batch size of reading from the ParcelFileDescriptor returned to mSecureTransportListener + private static final int READ_BUFFER_SIZE = 8192; + + private final Object mLock = new Object(); + // CompanionDeviceManager (CDM) can continue to call these ExecutorServices even after the + // corresponding cleanup methods in CDM have been called (e.g. + // removeOnTransportsChangedListener). Since we shut down these ExecutorServices after + // clean up, we use SoftShutdownExecutor to suppress RejectedExecutionExceptions. + private final SoftShutdownExecutor mMessageFromWearableExecutor = + new SoftShutdownExecutor(Executors.newSingleThreadExecutor()); + private final SoftShutdownExecutor mMessageToWearableExecutor = + new SoftShutdownExecutor(Executors.newSingleThreadExecutor()); + private final SoftShutdownExecutor mLightWeightExecutor = + new SoftShutdownExecutor(Executors.newSingleThreadExecutor()); + private final CompanionDeviceManager mCompanionDeviceManager; + private final ParcelFileDescriptor mUnderlyingTransport; + private final SecureTransportListener mSecureTransportListener; + private final AtomicBoolean mTransportAvailable = new AtomicBoolean(false); + private final Consumer<List<AssociationInfo>> mOnTransportsChangedListener = + this::onTransportsChanged; + private final BiConsumer<Integer, byte[]> mOnMessageReceivedListener = this::onMessageReceived; + private final ParcelFileDescriptor mRemoteFd; // To be returned to mSecureTransportListener + // read input received from the ParcelFileDescriptor returned to mSecureTransportListener + private final InputStream mLocalIn; + // send output to the ParcelFileDescriptor returned to mSecureTransportListener + private final OutputStream mLocalOut; + + @GuardedBy("mLock") + private boolean mClosed = false; + + private Integer mAssociationId = null; + + /** + * Creates a WearableSensingSecureChannel. When the secure transport is ready, + * secureTransportListener will be notified. + * + * @param companionDeviceManager The CompanionDeviceManager system service. + * @param underlyingTransport The underlying transport to create the secure channel on. + * @param secureTransportListener The listener to receive the secure transport when it is ready. + * @throws IOException if it cannot create a {@link ParcelFileDescriptor} socket pair. + */ + static WearableSensingSecureChannel create( + @NonNull CompanionDeviceManager companionDeviceManager, + @NonNull ParcelFileDescriptor underlyingTransport, + @NonNull SecureTransportListener secureTransportListener) + throws IOException { + Objects.requireNonNull(companionDeviceManager); + Objects.requireNonNull(underlyingTransport); + Objects.requireNonNull(secureTransportListener); + ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(); + WearableSensingSecureChannel channel = + new WearableSensingSecureChannel( + companionDeviceManager, + underlyingTransport, + secureTransportListener, + pair[0], + pair[1]); + channel.initialize(); + return channel; + } + + private WearableSensingSecureChannel( + CompanionDeviceManager companionDeviceManager, + ParcelFileDescriptor underlyingTransport, + SecureTransportListener secureTransportListener, + ParcelFileDescriptor remoteFd, + ParcelFileDescriptor localFd) { + mCompanionDeviceManager = companionDeviceManager; + mUnderlyingTransport = underlyingTransport; + mSecureTransportListener = secureTransportListener; + mRemoteFd = remoteFd; + mLocalIn = new AutoCloseInputStream(localFd); + mLocalOut = new AutoCloseOutputStream(localFd); + } + + private void initialize() { + final long originalCallingIdentity = Binder.clearCallingIdentity(); + try { + Slog.d(TAG, "Requesting CDM association."); + mCompanionDeviceManager.associate( + new AssociationRequest.Builder() + .setDisplayName(CDM_ASSOCIATION_DISPLAY_NAME) + .setSelfManaged(true) + .build(), + mLightWeightExecutor, + new CompanionDeviceManager.Callback() { + @Override + public void onAssociationCreated(AssociationInfo associationInfo) { + WearableSensingSecureChannel.this.onAssociationCreated( + associationInfo.getId()); + } + + @Override + public void onFailure(CharSequence error) { + Slog.e( + TAG, + "Failed to create CompanionDeviceManager association: " + + error); + onError(); + } + }); + } finally { + Binder.restoreCallingIdentity(originalCallingIdentity); + } + } + + private void onAssociationCreated(int associationId) { + Slog.i(TAG, "CDM association created."); + synchronized (mLock) { + if (mClosed) { + return; + } + mAssociationId = associationId; + mCompanionDeviceManager.addOnMessageReceivedListener( + mMessageFromWearableExecutor, + CompanionDeviceManager.MESSAGE_ONEWAY_FROM_WEARABLE, + mOnMessageReceivedListener); + mCompanionDeviceManager.addOnTransportsChangedListener( + mLightWeightExecutor, mOnTransportsChangedListener); + mCompanionDeviceManager.attachSystemDataTransport( + associationId, + new AutoCloseInputStream(mUnderlyingTransport), + new AutoCloseOutputStream(mUnderlyingTransport)); + } + } + + private void onTransportsChanged(List<AssociationInfo> associationInfos) { + synchronized (mLock) { + if (mClosed) { + return; + } + if (mAssociationId == null) { + Slog.e(TAG, "mAssociationId is null when transport changed"); + return; + } + } + // Do not call onTransportAvailable() or onError() when holding the lock because it can + // cause a deadlock if the callback holds another lock. + boolean transportAvailable = + associationInfos.stream().anyMatch(info -> info.getId() == mAssociationId); + if (transportAvailable && mTransportAvailable.compareAndSet(false, true)) { + onTransportAvailable(); + } else if (!transportAvailable && mTransportAvailable.compareAndSet(true, false)) { + Slog.i(TAG, "CDM transport is detached. This is not recoverable."); + onError(); + } + } + + private void onTransportAvailable() { + // Start sending data received from the remote stream to the wearable. + Slog.i(TAG, "Transport available"); + mMessageToWearableExecutor.execute( + () -> { + int[] associationIdsToSendMessageTo = new int[] {mAssociationId}; + byte[] buffer = new byte[READ_BUFFER_SIZE]; + int readLen; + try { + while ((readLen = mLocalIn.read(buffer)) != -1) { + byte[] data = new byte[readLen]; + System.arraycopy(buffer, 0, data, 0, readLen); + Slog.v(TAG, "Sending message to wearable"); + mCompanionDeviceManager.sendMessage( + CompanionDeviceManager.MESSAGE_ONEWAY_TO_WEARABLE, + data, + associationIdsToSendMessageTo); + } + } catch (IOException e) { + Slog.i(TAG, "IOException while reading from remote stream."); + onError(); + return; + } + Slog.i( + TAG, + "Reached EOF when reading from remote stream. Reporting this as an" + + " error."); + onError(); + }); + mSecureTransportListener.onSecureTransportAvailable(mRemoteFd); + } + + private void onMessageReceived(int associationIdForMessage, byte[] data) { + if (associationIdForMessage == mAssociationId) { + Slog.v(TAG, "Received message from wearable."); + try { + mLocalOut.write(data); + mLocalOut.flush(); + } catch (IOException e) { + Slog.i( + TAG, + "IOException when writing to remote stream. Closing the secure channel."); + onError(); + } + } else { + Slog.v( + TAG, + "Received CDM message of type MESSAGE_ONEWAY_FROM_WEARABLE, but it is for" + + " another association. Ignoring the message."); + } + } + + private void onError() { + synchronized (mLock) { + if (mClosed) { + return; + } + } + mSecureTransportListener.onError(); + close(); + } + + /** Closes this secure channel and releases all resources. */ + void close() { + synchronized (mLock) { + if (mClosed) { + return; + } + Slog.i(TAG, "Closing WearableSensingSecureChannel."); + mClosed = true; + if (mAssociationId != null) { + final long originalCallingIdentity = Binder.clearCallingIdentity(); + try { + mCompanionDeviceManager.removeOnTransportsChangedListener( + mOnTransportsChangedListener); + mCompanionDeviceManager.removeOnMessageReceivedListener( + CompanionDeviceManager.MESSAGE_ONEWAY_FROM_WEARABLE, + mOnMessageReceivedListener); + mCompanionDeviceManager.detachSystemDataTransport(mAssociationId); + mCompanionDeviceManager.disassociate(mAssociationId); + } finally { + Binder.restoreCallingIdentity(originalCallingIdentity); + } + } + try { + mLocalIn.close(); + } catch (IOException ex) { + Slog.e(TAG, "Encountered IOException when closing local input stream.", ex); + } + try { + mLocalOut.close(); + } catch (IOException ex) { + Slog.e(TAG, "Encountered IOException when closing local output stream.", ex); + } + mMessageFromWearableExecutor.shutdown(); + mMessageToWearableExecutor.shutdown(); + mLightWeightExecutor.shutdown(); + } + } + + /** + * An executor that can be shutdown. Unlike an ExecutorService, it will not throw a + * RejectedExecutionException if {@link #execute(Runnable)} is called after shutdown. + */ + private static class SoftShutdownExecutor implements Executor { + + private final ExecutorService mExecutorService; + + SoftShutdownExecutor(ExecutorService executorService) { + mExecutorService = executorService; + } + + @Override + public void execute(Runnable runnable) { + try { + mExecutorService.execute(runnable); + } catch (RejectedExecutionException ex) { + Slog.d(TAG, "Received new runnable after shutdown. Ignoring."); + } + } + + /** Shutdown the underlying ExecutorService. */ + void shutdown() { + mExecutorService.shutdown(); + } + } +} diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 19ea9f907306..ee865d3a588a 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -1610,7 +1610,7 @@ final class AccessibilityController { Slog.i(LOG_TAG, "computeChangedWindows()"); } - final List<WindowInfo> windows = new ArrayList<>(); + final List<WindowInfo> windows; final List<AccessibilityWindow> visibleWindows = new ArrayList<>(); final int topFocusedDisplayId; IBinder topFocusedWindowToken = null; @@ -1640,69 +1640,11 @@ final class AccessibilityController { } final Display display = dc.getDisplay(); display.getRealSize(mTempPoint); - final int screenWidth = mTempPoint.x; - final int screenHeight = mTempPoint.y; - - Region unaccountedSpace = mTempRegion; - unaccountedSpace.set(0, 0, screenWidth, screenHeight); mA11yWindowsPopulator.populateVisibleWindowsOnScreenLocked( mDisplayId, visibleWindows); - Set<IBinder> addedWindows = mTempBinderSet; - addedWindows.clear(); - - boolean focusedWindowAdded = false; - final int visibleWindowCount = visibleWindows.size(); - - // Iterate until we figure out what is touchable for the entire screen. - for (int i = 0; i < visibleWindowCount; i++) { - final AccessibilityWindow a11yWindow = visibleWindows.get(i); - final Region regionInWindow = new Region(); - a11yWindow.getTouchableRegionInWindow(regionInWindow); - if (windowMattersToAccessibility(a11yWindow, regionInWindow, - unaccountedSpace)) { - addPopulatedWindowInfo(a11yWindow, regionInWindow, windows, addedWindows); - if (windowMattersToUnaccountedSpaceComputation(a11yWindow)) { - updateUnaccountedSpace(a11yWindow, unaccountedSpace); - } - focusedWindowAdded |= a11yWindow.isFocused(); - } else if (a11yWindow.isUntouchableNavigationBar()) { - // If this widow is navigation bar without touchable region, accounting the - // region of navigation bar inset because all touch events from this region - // would be received by launcher, i.e. this region is a un-touchable one - // for the application. - unaccountedSpace.op( - getSystemBarInsetsFrame( - mService.mWindowMap.get(a11yWindow.getWindowInfo().token)), - unaccountedSpace, - Region.Op.REVERSE_DIFFERENCE); - } - - if (unaccountedSpace.isEmpty() && focusedWindowAdded) { - break; - } - } - - // Remove child/parent references to windows that were not added. - final int windowCount = windows.size(); - for (int i = 0; i < windowCount; i++) { - WindowInfo window = windows.get(i); - if (!addedWindows.contains(window.parentToken)) { - window.parentToken = null; - } - if (window.childTokens != null) { - final int childTokenCount = window.childTokens.size(); - for (int j = childTokenCount - 1; j >= 0; j--) { - if (!addedWindows.contains(window.childTokens.get(j))) { - window.childTokens.remove(j); - } - } - // Leave the child token list if empty. - } - } - - addedWindows.clear(); + windows = buildWindowInfoListLocked(visibleWindows, mTempPoint); // Gets the top focused display Id and window token for supporting multi-display. topFocusedDisplayId = mService.mRoot.getTopFocusedDisplayContent().getDisplayId(); @@ -1718,6 +1660,74 @@ final class AccessibilityController { mInitialized = true; } + /** + * From a list of windows, decides windows to be exposed to accessibility based on touchable + * region in the screen. + */ + private List<WindowInfo> buildWindowInfoListLocked(List<AccessibilityWindow> visibleWindows, + Point screenSize) { + final List<WindowInfo> windows = new ArrayList<>(); + final Set<IBinder> addedWindows = mTempBinderSet; + addedWindows.clear(); + + boolean focusedWindowAdded = false; + + final int visibleWindowCount = visibleWindows.size(); + + Region unaccountedSpace = mTempRegion; + unaccountedSpace.set(0, 0, screenSize.x, screenSize.y); + + // Iterate until we figure out what is touchable for the entire screen. + for (int i = 0; i < visibleWindowCount; i++) { + final AccessibilityWindow a11yWindow = visibleWindows.get(i); + final Region regionInWindow = new Region(); + a11yWindow.getTouchableRegionInWindow(regionInWindow); + if (windowMattersToAccessibility(a11yWindow, regionInWindow, unaccountedSpace)) { + addPopulatedWindowInfo(a11yWindow, regionInWindow, windows, addedWindows); + if (windowMattersToUnaccountedSpaceComputation(a11yWindow)) { + updateUnaccountedSpace(a11yWindow, unaccountedSpace); + } + focusedWindowAdded |= a11yWindow.isFocused(); + } else if (a11yWindow.isUntouchableNavigationBar()) { + // If this widow is navigation bar without touchable region, accounting the + // region of navigation bar inset because all touch events from this region + // would be received by launcher, i.e. this region is a un-touchable one + // for the application. + unaccountedSpace.op( + getSystemBarInsetsFrame( + mService.mWindowMap.get(a11yWindow.getWindowInfo().token)), + unaccountedSpace, + Region.Op.REVERSE_DIFFERENCE); + } + + if (unaccountedSpace.isEmpty() && focusedWindowAdded) { + break; + } + } + + // Remove child/parent references to windows that were not added. + final int windowCount = windows.size(); + for (int i = 0; i < windowCount; i++) { + WindowInfo window = windows.get(i); + if (!addedWindows.contains(window.parentToken)) { + window.parentToken = null; + } + if (window.childTokens != null) { + final int childTokenCount = window.childTokens.size(); + for (int j = childTokenCount - 1; j >= 0; j--) { + if (!addedWindows.contains(window.childTokens.get(j))) { + window.childTokens.remove(j); + } + } + // Leave the child token list if empty. + } + } + + addedWindows.clear(); + + return windows; + } + // Some windows should be excluded from unaccounted space computation, though they still // should be reported private boolean windowMattersToUnaccountedSpaceComputation(AccessibilityWindow a11yWindow) { diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java index 4ced5d524798..f2dc55f38bc3 100644 --- a/services/core/java/com/android/server/wm/SafeActivityOptions.java +++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java @@ -333,7 +333,9 @@ public class SafeActivityOptions { if (aInfo != null && overrideTaskTransition) { final int startTasksFromRecentsPerm = ActivityTaskManagerService.checkPermission( START_TASKS_FROM_RECENTS, callingPid, callingUid); - if (startTasksFromRecentsPerm != PERMISSION_GRANTED) { + // Allow if calling uid is from assistant, or start task from recents + if (startTasksFromRecentsPerm != PERMISSION_GRANTED + && !isAssistant(supervisor.mService, callingUid)) { final String msg = "Permission Denial: starting " + getIntentString(intent) + " from " + callerApp + " (pid=" + callingPid + ", uid=" + callingUid + ") with overrideTaskTransition=true"; diff --git a/services/core/java/com/android/server/wm/WindowList.java b/services/core/java/com/android/server/wm/WindowList.java index dfeba40fa45e..1e888f5823a1 100644 --- a/services/core/java/com/android/server/wm/WindowList.java +++ b/services/core/java/com/android/server/wm/WindowList.java @@ -24,7 +24,7 @@ import java.util.ArrayList; */ class WindowList<E> extends ArrayList<E> { - void addFirst(E e) { + public void addFirst(E e) { add(0, e); } diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java index b1349ea92a4f..f5ba50d7f079 100644 --- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -27,6 +27,7 @@ import android.credentials.selection.ProviderData; import android.credentials.selection.RequestInfo; import android.os.CancellationSignal; import android.os.RemoteException; +import android.os.ResultReceiver; import android.service.credentials.CallingAppInfo; import android.util.Slog; @@ -149,7 +150,7 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta } @Override - public void onUiCancellation(boolean isUserCancellation) { + public void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver) { // Not needed since UI is not involved } diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index b6f7eb39e29b..be4b9e1fc7b1 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -31,6 +31,7 @@ import android.credentials.selection.ProviderData; import android.credentials.selection.RequestInfo; import android.os.CancellationSignal; import android.os.RemoteException; +import android.os.ResultReceiver; import android.service.credentials.CallingAppInfo; import android.service.credentials.PermissionUtils; import android.util.Slog; @@ -163,7 +164,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR } @Override - public void onUiCancellation(boolean isUserCancellation) { + public void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver) { String exception = CreateCredentialException.TYPE_USER_CANCELED; String message = "User cancelled the selector"; if (!isUserCancellation) { diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index 84b5cb785a41..534c842f6b07 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -15,6 +15,8 @@ */ package com.android.server.credentials; +import static android.credentials.selection.Constants.EXTRA_FINAL_RESPONSE_RECEIVER; + import android.annotation.NonNull; import android.app.PendingIntent; import android.content.ComponentName; @@ -34,7 +36,6 @@ import android.os.Looper; import android.os.ResultReceiver; import android.os.UserHandle; import android.service.credentials.CredentialProviderInfoFactory; -import android.util.Slog; import java.util.ArrayList; import java.util.HashSet; @@ -79,20 +80,25 @@ public class CredentialManagerUi { UserSelectionDialogResult selection = UserSelectionDialogResult .fromResultData(resultData); if (selection != null) { - mCallbacks.onUiSelection(selection); - } else { - Slog.i(TAG, "No selection found in UI result"); + ResultReceiver resultReceiver = resultData.getParcelable( + EXTRA_FINAL_RESPONSE_RECEIVER, + ResultReceiver.class); + mCallbacks.onUiSelection(selection, resultReceiver); } break; case UserSelectionDialogResult.RESULT_CODE_DIALOG_USER_CANCELED: mStatus = UiStatus.TERMINATED; - mCallbacks.onUiCancellation(/* isUserCancellation= */ true); + mCallbacks.onUiCancellation(/* isUserCancellation= */ true, + resultData.getParcelable(EXTRA_FINAL_RESPONSE_RECEIVER, + ResultReceiver.class)); break; case UserSelectionDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS: mStatus = UiStatus.TERMINATED; - mCallbacks.onUiCancellation(/* isUserCancellation= */ false); + mCallbacks.onUiCancellation(/* isUserCancellation= */ false, + resultData.getParcelable(EXTRA_FINAL_RESPONSE_RECEIVER, + ResultReceiver.class)); break; case UserSelectionDialogResult.RESULT_CODE_DATA_PARSING_FAILURE: mStatus = UiStatus.TERMINATED; @@ -116,10 +122,10 @@ public class CredentialManagerUi { */ public interface CredentialManagerUiCallback { /** Called when the user makes a selection. */ - void onUiSelection(UserSelectionDialogResult selection); + void onUiSelection(UserSelectionDialogResult selection, ResultReceiver resultReceiver); /** Called when the UI is canceled without a successful provider result. */ - void onUiCancellation(boolean isUserCancellation); + void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver); /** Called when the selector UI fails to come up (mostly due to parsing issue today). */ void onUiSelectorInvocationFailure(); diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java index 9e362b3e4784..adb1b72cf3ee 100644 --- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java @@ -20,6 +20,7 @@ import android.Manifest; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; +import android.credentials.Constants; import android.credentials.CredentialProviderInfo; import android.credentials.GetCandidateCredentialsException; import android.credentials.GetCandidateCredentialsResponse; @@ -29,10 +30,13 @@ import android.credentials.IGetCandidateCredentialsCallback; import android.credentials.selection.GetCredentialProviderData; import android.credentials.selection.ProviderData; import android.credentials.selection.RequestInfo; +import android.os.Bundle; import android.os.CancellationSignal; import android.os.IBinder; import android.os.RemoteException; +import android.os.ResultReceiver; import android.service.credentials.CallingAppInfo; +import android.service.credentials.CredentialProviderService; import android.service.credentials.PermissionUtils; import android.util.Slog; @@ -153,7 +157,8 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ } @Override - public void onUiCancellation(boolean isUserCancellation) { + public void onUiCancellation(boolean isUserCancellation, + @Nullable ResultReceiver finalResponseReceiver) { String exception = GetCandidateCredentialsException.TYPE_USER_CANCELED; String message = "User cancelled the selector"; if (!isUserCancellation) { @@ -161,7 +166,12 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ message = "The UI was interrupted - please try again."; } mRequestSessionMetric.collectFrameworkException(exception); - respondToClientWithErrorAndFinish(exception, message); + if (finalResponseReceiver != null) { + Bundle resultData = new Bundle(); + finalResponseReceiver.send(Constants.FAILURE_CREDMAN_SELECTOR, resultData); + } else { + respondToClientWithErrorAndFinish(exception, message); + } } @Override @@ -197,7 +207,16 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ public void onFinalResponseReceived(ComponentName componentName, GetCredentialResponse response) { Slog.d(TAG, "onFinalResponseReceived"); - respondToClientWithResponseAndFinish(new GetCandidateCredentialsResponse(response)); + if (this.mFinalResponseReceiver != null) { + Slog.d(TAG, "onFinalResponseReceived sending through final receiver"); + Bundle resultData = new Bundle(); + resultData.putParcelable( + CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE, response); + mFinalResponseReceiver.send(Constants.SUCCESS_CREDMAN_SELECTOR, resultData); + finishSession(/*propagateCancellation=*/ false); + } else { + Slog.w(TAG, "onFinalResponseReceived result receiver not found for pinned entry"); + } } /** @@ -212,6 +231,5 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ */ public int getAutofillRequestId() { return mAutofillRequestId; - } } diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 4068d7b5c95a..a279337698f2 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -31,6 +31,7 @@ import android.credentials.selection.RequestInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.RemoteException; +import android.os.ResultReceiver; import android.service.credentials.CallingAppInfo; import android.service.credentials.PermissionUtils; import android.util.Slog; @@ -165,7 +166,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, } @Override - public void onUiCancellation(boolean isUserCancellation) { + public void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver) { String exception = GetCredentialException.TYPE_USER_CANCELED; String message = "User cancelled the selector"; if (!isUserCancellation) { diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index bf7df86890de..633c9c4cb8e1 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -17,6 +17,7 @@ package com.android.server.credentials; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.content.ComponentName; @@ -33,6 +34,7 @@ import android.os.IBinder; import android.os.IInterface; import android.os.Looper; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.UserHandle; import android.service.credentials.CallingAppInfo; import android.util.Slog; @@ -101,6 +103,9 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential protected PendingIntent mPendingIntent; + @Nullable + protected ResultReceiver mFinalResponseReceiver; + @NonNull protected RequestSessionStatus mRequestSessionStatus = RequestSessionStatus.IN_PROGRESS; @@ -219,7 +224,8 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential // UI callbacks @Override // from CredentialManagerUiCallbacks - public void onUiSelection(UserSelectionDialogResult selection) { + public void onUiSelection(UserSelectionDialogResult selection, + ResultReceiver finalResponseReceiver) { if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { Slog.w(TAG, "Request has already been completed. This is strange."); return; @@ -234,6 +240,7 @@ abstract class RequestSession<T, U, V> implements CredentialManagerUi.Credential Slog.w(TAG, "providerSession not found in onUiSelection. This is strange."); return; } + mFinalResponseReceiver = finalResponseReceiver; ProviderSessionMetric providerSessionMetric = providerSession.mProviderSessionMetric; int initialAuthMetricsProvider = providerSessionMetric.getBrowsedAuthenticationMetric() .size(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 74d544fcbf8a..6f0985aecebd 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -34,6 +34,7 @@ import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CALLS; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CAMERA; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CERTIFICATES; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE; +import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS; import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DISPLAY; @@ -110,6 +111,8 @@ import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_DEV import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE; import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_USER; import static android.app.admin.DevicePolicyManager.ACTION_SYSTEM_UPDATE_POLICY_CHANGED; +import static android.app.admin.DevicePolicyManager.CONTENT_PROTECTION_DISABLED; +import static android.app.admin.DevicePolicyManager.ContentProtectionPolicy; import static android.app.admin.DevicePolicyManager.DELEGATION_APP_RESTRICTIONS; import static android.app.admin.DevicePolicyManager.DELEGATION_BLOCK_UNINSTALL; import static android.app.admin.DevicePolicyManager.DELEGATION_CERT_INSTALL; @@ -220,6 +223,7 @@ import static android.app.admin.ProvisioningException.ERROR_REMOVE_NON_REQUIRED_ import static android.app.admin.ProvisioningException.ERROR_SETTING_PROFILE_OWNER_FAILED; import static android.app.admin.ProvisioningException.ERROR_SET_DEVICE_OWNER_FAILED; import static android.app.admin.ProvisioningException.ERROR_STARTING_PROFILE_FAILED; +import static android.app.admin.flags.Flags.backupServiceSecurityLogEventEnabled; import static android.app.admin.flags.Flags.dumpsysPolicyEngineMigrationEnabled; import static android.app.admin.flags.Flags.policyEngineMigrationV2Enabled; import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE; @@ -17926,6 +17930,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { || isProfileOwner(caller) || isFinancedDeviceOwner(caller)); toggleBackupServiceActive(caller.getUserId(), enabled); + + if (backupServiceSecurityLogEventEnabled()) { + if (SecurityLog.isLoggingEnabled()) { + SecurityLog.writeEvent(SecurityLog.TAG_BACKUP_SERVICE_TOGGLED, + caller.getPackageName(), caller.getUserId(), enabled ? 1 : 0); + } + } } @Override @@ -23165,6 +23176,90 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + private EnforcingAdmin enforceCanCallContentProtectionLocked( + ComponentName who, String callerPackageName) { + CallerIdentity caller = getCallerIdentity(who, callerPackageName); + final int userId = caller.getUserId(); + + EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( + who, + MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, + caller.getPackageName(), + userId + ); + if ((isDeviceOwner(caller) || isProfileOwner(caller)) + && !canDPCManagedUserUseLockTaskLocked(userId)) { + throw new SecurityException( + "User " + userId + " is not allowed to use content protection"); + } + return enforcingAdmin; + } + + private void enforceCanQueryContentProtectionLocked( + ComponentName who, String callerPackageName) { + CallerIdentity caller = getCallerIdentity(who, callerPackageName); + final int userId = caller.getUserId(); + + enforceCanQuery(MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, caller.getPackageName(), userId); + if ((isDeviceOwner(caller) || isProfileOwner(caller)) + && !canDPCManagedUserUseLockTaskLocked(userId)) { + throw new SecurityException( + "User " + userId + " is not allowed to use content protection"); + } + } + + @Override + public void setContentProtectionPolicy( + ComponentName who, String callerPackageName, @ContentProtectionPolicy int policy) + throws SecurityException { + if (!android.view.contentprotection.flags.Flags.manageDevicePolicyEnabled()) { + return; + } + + CallerIdentity caller = getCallerIdentity(who, callerPackageName); + checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_CONTENT_PROTECTION_POLICY); + + EnforcingAdmin enforcingAdmin; + synchronized (getLockObject()) { + enforcingAdmin = enforceCanCallContentProtectionLocked(who, caller.getPackageName()); + } + + if (policy == CONTENT_PROTECTION_DISABLED) { + mDevicePolicyEngine.removeLocalPolicy( + PolicyDefinition.CONTENT_PROTECTION, + enforcingAdmin, + caller.getUserId()); + } else { + mDevicePolicyEngine.setLocalPolicy( + PolicyDefinition.CONTENT_PROTECTION, + enforcingAdmin, + new IntegerPolicyValue(policy), + caller.getUserId()); + } + } + + @Override + public @ContentProtectionPolicy int getContentProtectionPolicy( + ComponentName who, String callerPackageName) { + if (!android.view.contentprotection.flags.Flags.manageDevicePolicyEnabled()) { + return CONTENT_PROTECTION_DISABLED; + } + + CallerIdentity caller = getCallerIdentity(who, callerPackageName); + final int userHandle = caller.getUserId(); + + synchronized (getLockObject()) { + enforceCanQueryContentProtectionLocked(who, caller.getPackageName()); + } + Integer policy = mDevicePolicyEngine.getResolvedPolicy( + PolicyDefinition.CONTENT_PROTECTION, userHandle); + if (policy == null) { + return CONTENT_PROTECTION_DISABLED; + } else { + return policy; + } + } + @Override public ManagedSubscriptionsPolicy getManagedSubscriptionsPolicy() { synchronized (getLockObject()) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 0fc8c5e7a46a..27f183445cfa 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -341,6 +341,13 @@ final class PolicyDefinition<V> { PolicyEnforcerCallbacks.setUsbDataSignalingEnabled(value, context), new BooleanPolicySerializer()); + static PolicyDefinition<Integer> CONTENT_PROTECTION = new PolicyDefinition<>( + new NoArgsPolicyKey(DevicePolicyIdentifiers.CONTENT_PROTECTION_POLICY), + new MostRecent<>(), + POLICY_FLAG_LOCAL_ONLY_POLICY, + (Integer value, Context context, Integer userId, PolicyKey policyKey) -> true, + new IntegerPolicySerializer()); + private static final Map<String, PolicyDefinition<?>> POLICY_DEFINITIONS = new HashMap<>(); private static Map<String, Integer> USER_RESTRICTION_FLAGS = new HashMap<>(); @@ -374,6 +381,8 @@ final class PolicyDefinition<V> { PERSONAL_APPS_SUSPENDED); POLICY_DEFINITIONS.put(DevicePolicyIdentifiers.USB_DATA_SIGNALING_POLICY, USB_DATA_SIGNALING); + POLICY_DEFINITIONS.put(DevicePolicyIdentifiers.CONTENT_PROTECTION_POLICY, + CONTENT_PROTECTION); // User Restriction Policies USER_RESTRICTION_FLAGS.put(UserManager.DISALLOW_MODIFY_ACCOUNTS, /* flags= */ 0); diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig new file mode 100644 index 000000000000..4b578afddad2 --- /dev/null +++ b/services/java/com/android/server/flags.aconfig @@ -0,0 +1,9 @@ +package: "android.server" + +flag { + namespace: "system_performance" + name: "telemetry_apis_service" + description: "Control service portion of telemetry APIs feature." + is_fixed_read_only: true + bug: "324153471" +} diff --git a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp index d479e52f92d8..682ed91c22dd 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp +++ b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp @@ -32,6 +32,7 @@ java_test_host { ":BackgroundInstallControlServiceTestApp", ":BackgroundInstallControlMockApp1", ":BackgroundInstallControlMockApp2", + ":BackgroundInstallControlMockApp3", ], test_suites: [ "general-tests", diff --git a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml index 1e7a78aa6f93..a352851d1297 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml +++ b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml @@ -34,6 +34,9 @@ <option name="push-file" key="BackgroundInstallControlMockApp2.apk" value="/data/local/tmp/BackgroundInstallControlMockApp2.apk" /> + <option name="push-file" + key="BackgroundInstallControlMockApp3.apk" + value="/data/local/tmp/BackgroundInstallControlMockApp3.apk" /> </target_preparer> <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > diff --git a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java index c99e7129853b..5092a4659eb9 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java +++ b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java @@ -68,6 +68,20 @@ public final class BackgroundInstallControlServiceHostTest extends BaseHostJUnit assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_2)).isNull(); } + @Test + public void testRegisterCallback() throws Exception { + runDeviceTest( + "BackgroundInstallControlServiceTest", + "testRegisterBackgroundInstallControlCallback"); + } + + @Test + public void testUnregisterCallback() throws Exception { + runDeviceTest( + "BackgroundInstallControlServiceTest", + "testUnregisterBackgroundInstallControlCallback"); + } + private void installPackage(String path) throws DeviceNotAvailableException { String cmd = "pm install -t --force-queryable " + path; CommandResult result = getDevice().executeShellV2Command(cmd); diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java index b23f59106881..ac041f492135 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java +++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java @@ -16,38 +16,59 @@ package com.android.server.pm.test.app; -import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES; - import static com.android.compatibility.common.util.SystemUtil.runShellCommand; import static com.google.common.truth.Truth.assertThat; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.IBackgroundInstallControlService; import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; +import android.os.Bundle; +import android.os.IRemoteCallback; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; +import android.util.Pair; +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ShellIdentityUtils; +import com.android.compatibility.common.util.ThrowingRunnable; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.FileInputStream; +import java.io.OutputStream; +import java.util.ArrayList; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.Collectors; @RunWith(AndroidJUnit4.class) public class BackgroundInstallControlServiceTest { private static final String TAG = "BackgroundInstallControlServiceTest"; + private static final String ACTION_INSTALL_COMMIT = + "com.android.server.pm.test.app.BackgroundInstallControlServiceTest" + + ".ACTION_INSTALL_COMMIT"; private static final String MOCK_PACKAGE_NAME = "com.android.servicestests.apps.bicmockapp3"; + private static final String TEST_DATA_DIR = "/data/local/tmp/"; + + private static final String MOCK_APK_FILE = "BackgroundInstallControlMockApp3.apk"; private IBackgroundInstallControlService mIBics; @Before @@ -74,10 +95,9 @@ public class BackgroundInstallControlServiceTest { PackageManager.MATCH_ALL, Process.myUserHandle() .getIdentifier()); } catch (RemoteException e) { - throw new RuntimeException(e); + throw e.rethrowFromSystemServer(); } - }, - GET_BACKGROUND_INSTALLED_PACKAGES); + }); assertThat(slice).isNotNull(); var packageList = slice.getList(); @@ -94,4 +114,150 @@ public class BackgroundInstallControlServiceTest { .collect(Collectors.toSet()); assertThat(actualPackageNames).containsExactlyElementsIn(expectedPackageNames); } + + @Test + public void testRegisterBackgroundInstallControlCallback() + throws Exception { + String testPackageName = "test"; + int testUserId = 1; + ArrayList<Pair<String, Integer>> sharedResource = new ArrayList<>(); + IRemoteCallback testCallback = + new IRemoteCallback.Stub() { + private final ArrayList<Pair<String, Integer>> mArray = sharedResource; + + @Override + public void sendResult(Bundle data) throws RemoteException { + mArray.add(new Pair(testPackageName, testUserId)); + } + }; + ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn( + mIBics, + (bics) -> { + try { + bics.registerBackgroundInstallCallback(testCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + }); + installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME); + + assertUntil(() -> sharedResource.size() == 1, 2000); + assertThat(sharedResource.get(0).first).isEqualTo(testPackageName); + assertThat(sharedResource.get(0).second).isEqualTo(testUserId); + } + + @Test + public void testUnregisterBackgroundInstallControlCallback() { + String testValue = "test"; + ArrayList<String> sharedResource = new ArrayList<>(); + IRemoteCallback testCallback = + new IRemoteCallback.Stub() { + private final ArrayList<String> mArray = sharedResource; + + @Override + public void sendResult(Bundle data) throws RemoteException { + mArray.add(testValue); + } + }; + ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn( + mIBics, + (bics) -> { + try { + bics.registerBackgroundInstallCallback(testCallback); + bics.unregisterBackgroundInstallCallback(testCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + }); + installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME); + + assertUntil(sharedResource::isEmpty, 2000); + } + + private static boolean installPackage(String apkPath, String packageName) { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + final CountDownLatch installLatch = new CountDownLatch(1); + final BroadcastReceiver installReceiver = + new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + int packageInstallStatus = + intent.getIntExtra( + PackageInstaller.EXTRA_STATUS, + PackageInstaller.STATUS_FAILURE_INVALID); + if (packageInstallStatus == PackageInstaller.STATUS_SUCCESS) { + installLatch.countDown(); + } + } + }; + final IntentFilter intentFilter = new IntentFilter(ACTION_INSTALL_COMMIT); + context.registerReceiver(installReceiver, intentFilter, Context.RECEIVER_EXPORTED); + + PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); + PackageInstaller.SessionParams params = + new PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL); + params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED); + try { + int sessionId = packageInstaller.createSession(params); + PackageInstaller.Session session = packageInstaller.openSession(sessionId); + OutputStream out = session.openWrite(packageName, 0, -1); + FileInputStream fis = new FileInputStream(apkPath); + byte[] buffer = new byte[65536]; + int size; + while ((size = fis.read(buffer)) != -1) { + out.write(buffer, 0, size); + } + session.fsync(out); + fis.close(); + out.close(); + + runWithShellPermissionIdentity( + () -> { + session.commit(createPendingIntent(context).getIntentSender()); + installLatch.await(5, TimeUnit.SECONDS); + }); + return true; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static PendingIntent createPendingIntent(Context context) { + PendingIntent pendingIntent = + PendingIntent.getBroadcast( + context, + 1, + new Intent(ACTION_INSTALL_COMMIT) + .setPackage( + BackgroundInstallControlServiceTest.class.getPackageName()), + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE); + return pendingIntent; + } + + private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable command) + throws Exception { + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + try { + command.run(); + } finally { + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + } + + private static void assertUntil(Supplier<Boolean> condition, int timeoutMs) { + long endTime = System.currentTimeMillis() + timeoutMs; + while (System.currentTimeMillis() <= endTime) { + if (condition.get()) return; + try { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + assertThat(condition.get()).isTrue(); + } }
\ No newline at end of file diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp index 7804f4ce9d02..39b0ff782b72 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp +++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp @@ -50,3 +50,11 @@ android_test_helper_app { "--rename-manifest-package com.android.servicestests.apps.bicmockapp2", ], } + +android_test_helper_app { + name: "BackgroundInstallControlMockApp3", + defaults: ["bic-mock-app-defaults"], + aaptflags: [ + "--rename-manifest-package com.android.servicestests.apps.bicmockapp3", + ], +} diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt index d307608e0be2..b374af6b15c8 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt @@ -149,8 +149,8 @@ class DomainVerificationEnforcerTest { callingUidInt.set(it.callingUid) callingUserIdInt.set(it.callingUserId) service.proxy = it.proxy - service.addPackage(visiblePkgState) - service.addPackage(invisiblePkgState) + service.addPackage(visiblePkgState, null) + service.addPackage(invisiblePkgState, null) service.block(it) } diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt index 5edf30a33def..a8100afc4ac4 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt @@ -566,7 +566,7 @@ class DomainVerificationManagerApiTest { } private fun DomainVerificationService.addPackages(vararg pkgStates: PackageStateInternal) = - pkgStates.forEach(::addPackage) + pkgStates.forEach {pkg: PackageStateInternal -> addPackage(pkg, null)} private fun makeManager(service: DomainVerificationService, userId: Int) = DomainVerificationManager( diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt index 85f012534113..e0407c1543b0 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt @@ -21,6 +21,7 @@ import android.content.pm.PackageManager import android.content.pm.Signature import android.content.pm.SigningDetails import android.content.pm.verify.domain.DomainOwner +import android.content.pm.verify.domain.DomainSet import android.content.pm.verify.domain.DomainVerificationInfo.STATE_MODIFIABLE_VERIFIED import android.content.pm.verify.domain.DomainVerificationInfo.STATE_NO_RESPONSE import android.content.pm.verify.domain.DomainVerificationInfo.STATE_SUCCESS @@ -48,16 +49,16 @@ import com.android.server.testutils.mockThrowOnUnmocked import com.android.server.testutils.spy import com.android.server.testutils.whenever import com.google.common.truth.Truth.assertThat -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.security.PublicKey -import java.util.UUID import org.junit.Test import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito.doReturn +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.security.PublicKey +import java.util.UUID class DomainVerificationPackageTest { @@ -88,7 +89,7 @@ class DomainVerificationPackageTest { @Test fun addPackageFirstTime() { val service = makeService(pkg1, pkg2) - service.addPackage(pkg1) + service.addPackage(pkg1, null) val info = service.getInfo(pkg1.packageName) assertThat(info.packageName).isEqualTo(pkg1.packageName) assertThat(info.identifier).isEqualTo(pkg1.domainSetId) @@ -120,8 +121,8 @@ class DomainVerificationPackageTest { systemConfiguredPackageNames = ArraySet(setOf(pkg1.packageName, pkg2.packageName)), pkg1, pkg2 ) - service.addPackage(pkg1) - service.addPackage(pkg2) + service.addPackage(pkg1, null) + service.addPackage(pkg2, null) service.getInfo(pkg1.packageName).apply { assertThat(packageName).isEqualTo(pkg1.packageName) @@ -198,7 +199,7 @@ class DomainVerificationPackageTest { val service = makeService(pkg1, pkg2) val computer = mockComputer(pkg1, pkg2) service.restoreSettings(computer, Xml.resolvePullParser(xml.byteInputStream())) - service.addPackage(pkg1) + service.addPackage(pkg1, null) val info = service.getInfo(pkg1.packageName) assertThat(info.packageName).isEqualTo(pkg1.packageName) assertThat(info.identifier).isEqualTo(pkg1.domainSetId) @@ -248,7 +249,7 @@ class DomainVerificationPackageTest { val service = makeService(pkg1, pkg2) val computer = mockComputer(pkg1, pkg2) service.restoreSettings(computer, Xml.resolvePullParser(xml.byteInputStream())) - service.addPackage(pkg1) + service.addPackage(pkg1, null) val info = service.getInfo(pkg1.packageName) assertThat(info.packageName).isEqualTo(pkg1.packageName) assertThat(info.identifier).isEqualTo(pkg1.domainSetId) @@ -307,7 +308,7 @@ class DomainVerificationPackageTest { service.readSettings(computer, Xml.resolvePullParser(it)) } - service.addPackage(pkg1) + service.addPackage(pkg1, null) assertAddPackageActivePendingRestoredState(service) } @@ -321,7 +322,7 @@ class DomainVerificationPackageTest { service.readSettings(computer, Xml.resolvePullParser(it)) } - service.addPackage(pkg1) + service.addPackage(pkg1, null) val userState = service.getUserState(pkg1.packageName) assertThat(userState.packageName).isEqualTo(pkg1.packageName) @@ -345,11 +346,55 @@ class DomainVerificationPackageTest { service.restoreSettings(computer, Xml.resolvePullParser(it)) } - service.addPackage(pkg1) + service.addPackage(pkg1, null) assertAddPackageActivePendingRestoredState(service, expectRestore = true) } + @Test + fun addPackageWithPreVerifiedDomains() { + val service = makeService(pkg1) + val pkg1 = mockPkgState( + PKG_ONE, + UUID_ONE, + SIGNATURE_ONE, + autoVerifyDomains = listOf(DOMAIN_1, DOMAIN_2), + otherDomains = listOf(DOMAIN_3, DOMAIN_4)) + service.addPackage(pkg1, DomainSet(setOf(DOMAIN_1, DOMAIN_3))) + val info = service.getInfo(pkg1.packageName) + assertThat(info.packageName).isEqualTo(pkg1.packageName) + assertThat(info.identifier).isEqualTo(pkg1.domainSetId) + // Test that DOMAIN_1 is pre-verified and DOMAIN_3 is ignored because autoVerify=false + assertThat(info.hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_MODIFIABLE_VERIFIED, + DOMAIN_2 to STATE_NO_RESPONSE, + )) + + val userState = service.getUserState(pkg1.packageName) + assertThat(userState.packageName).isEqualTo(pkg1.packageName) + assertThat(userState.identifier).isEqualTo(pkg1.domainSetId) + assertThat(userState.isLinkHandlingAllowed).isEqualTo(true) + assertThat(userState.user.identifier).isEqualTo(USER_ID) + assertThat(userState.hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_NONE, + )) + + assertThat(service.queryValidVerificationPackageNames()) + .containsExactly(pkg1.packageName) + + // Test that the pre-verified state can be overwritten to be disapproved + service.setDomainVerificationStatusInternal( + PKG_ONE, + DomainVerificationState.STATE_DENIED, + ArraySet(setOf(DOMAIN_1, DOMAIN_2))) + val infoUpdated = service.getInfo(pkg1.packageName) + assertThat(infoUpdated.hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_UNMODIFIABLE, + DOMAIN_2 to STATE_UNMODIFIABLE, + )) + } + /** * Shared string that contains invalid [DOMAIN_3] and [DOMAIN_4] which should be stripped from * the final state. @@ -447,7 +492,7 @@ class DomainVerificationPackageTest { val map = mutableMapOf<String, PackageStateInternal>() val service = makeService { map[it] } - service.addPackage(pkgBefore) + service.addPackage(pkgBefore, null) // Only insert the package after addPackage call to ensure the service doesn't access // a live package inside the addPackage logic. It should only use the provided input. @@ -482,7 +527,7 @@ class DomainVerificationPackageTest { map[pkgName] = pkgAfter - service.migrateState(pkgBefore, pkgAfter) + service.migrateState(pkgBefore, pkgAfter, null) assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( DOMAIN_1 to STATE_UNMODIFIABLE, @@ -503,7 +548,7 @@ class DomainVerificationPackageTest { val map = mutableMapOf<String, PackageStateInternal>() val service = makeService { map[it] } - service.addPackage(pkgBefore) + service.addPackage(pkgBefore, null) // Only insert the package after addPackage call to ensure the service doesn't access // a live package inside the addPackage logic. It should only use the provided input. @@ -522,7 +567,7 @@ class DomainVerificationPackageTest { // Now remove the package because migrateState shouldn't use it either map.remove(pkgName) - service.migrateState(pkgBefore, pkgAfter) + service.migrateState(pkgBefore, pkgAfter, null) map[pkgName] = pkgAfter @@ -550,7 +595,7 @@ class DomainVerificationPackageTest { val map = mutableMapOf<String, PackageStateInternal>() val service = makeService { map[it] } - service.addPackage(pkgBefore) + service.addPackage(pkgBefore, null) // Only insert the package after addPackage call to ensure the service doesn't access // a live package inside the addPackage logic. It should only use the provided input. @@ -571,7 +616,7 @@ class DomainVerificationPackageTest { // Now remove the package because migrateState shouldn't use it either map.remove(pkgName) - service.migrateState(pkgBefore, pkgAfter) + service.migrateState(pkgBefore, pkgAfter, null) map[pkgName] = pkgAfter @@ -596,7 +641,7 @@ class DomainVerificationPackageTest { val map = mutableMapOf<String, PackageStateInternal>() val service = makeService { map[it] } - service.addPackage(pkgBefore) + service.addPackage(pkgBefore, null) // Only insert the package after addPackage call to ensure the service doesn't access // a live package inside the addPackage logic. It should only use the provided input. @@ -615,7 +660,7 @@ class DomainVerificationPackageTest { // Now remove the package because migrateState shouldn't use it either map.remove(pkgName) - service.migrateState(pkgBefore, pkgAfter) + service.migrateState(pkgBefore, pkgAfter, null) map[pkgName] = pkgAfter @@ -640,7 +685,7 @@ class DomainVerificationPackageTest { val map = mutableMapOf<String, PackageStateInternal>() val service = makeService { map[it] } - service.addPackage(pkgBefore) + service.addPackage(pkgBefore, null) // Only insert the package after addPackage call to ensure the service doesn't access // a live package inside the addPackage logic. It should only use the provided input. @@ -667,7 +712,7 @@ class DomainVerificationPackageTest { // Now remove the package because migrateState shouldn't use it either map.remove(pkgName) - service.migrateState(pkgBefore, pkgAfter) + service.migrateState(pkgBefore, pkgAfter, null) map[pkgName] = pkgAfter @@ -685,6 +730,30 @@ class DomainVerificationPackageTest { } @Test + fun migratePackageWithPreVerifiedDomains() { + val pkgName = PKG_ONE + val pkgBefore = mockPkgState(pkgName, UUID_ONE, SIGNATURE_ONE, emptyList()) + val pkgAfter = mockPkgState(pkgName, UUID_TWO, SIGNATURE_TWO, listOf(DOMAIN_1, DOMAIN_2)) + + val map = mutableMapOf<String, PackageStateInternal>() + val service = makeService { map[it] } + service.addPackage(pkgBefore, null) + service.migrateState(pkgBefore, pkgAfter, DomainSet(setOf(DOMAIN_1, DOMAIN_3))) + + map[pkgName] = pkgAfter + + assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to STATE_MODIFIABLE_VERIFIED, + DOMAIN_2 to STATE_NO_RESPONSE, + )) + assertThat(service.getUserState(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_NONE, + )) + assertThat(service.queryValidVerificationPackageNames()).containsExactly(pkgName) + } + + @Test fun backupAndRestore() { // This test acts as a proxy for true user restore through PackageManager, // as that's much harder to test for real. @@ -694,8 +763,8 @@ class DomainVerificationPackageTest { listOf(DOMAIN_1, DOMAIN_2, DOMAIN_3)) val serviceBefore = makeService(pkg1, pkg2) val computerBefore = mockComputer(pkg1, pkg2) - serviceBefore.addPackage(pkg1) - serviceBefore.addPackage(pkg2) + serviceBefore.addPackage(pkg1, null) + serviceBefore.addPackage(pkg2, null) serviceBefore.setStatus(pkg1.domainSetId, setOf(DOMAIN_1), STATE_SUCCESS) serviceBefore.setDomainVerificationLinkHandlingAllowed(pkg1.packageName, false, 10) @@ -748,8 +817,8 @@ class DomainVerificationPackageTest { val serviceAfter = makeService(pkg1, pkg2) val computerAfter = mockComputer(pkg1, pkg2) - serviceAfter.addPackage(pkg1) - serviceAfter.addPackage(pkg2) + serviceAfter.addPackage(pkg1, null) + serviceAfter.addPackage(pkg2, null) // Check the state is default before the restoration applies listOf(0, 10).forEach { @@ -858,8 +927,8 @@ class DomainVerificationPackageTest { ) val service = makeService(pkg1, pkg2) - service.addPackage(pkg1) - service.addPackage(pkg2) + service.addPackage(pkg1, null) + service.addPackage(pkg2, null) // Approve domain 1, 3, and 4 for package 2 for both users USER_IDS.forEach { diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt index a5c4f6cc0289..97483070e69b 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt @@ -106,7 +106,7 @@ class DomainVerificationSettingsMutationTest { fun service(name: String, block: DomainVerificationService.() -> Unit) = Params(makeService, name) { service -> service.proxy = proxy - service.addPackage(mockPkgState()) + service.addPackage(mockPkgState(), null) service.block() } diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt index ae570a3a0ee2..56ab841154cc 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt @@ -97,8 +97,8 @@ class DomainVerificationUserStateOverrideTest { } } }) - addPackage(pkg1) - addPackage(pkg2) + addPackage(pkg1, null) + addPackage(pkg2, null) // Starting state for all tests is to have domain 1 enabled for the first package setDomainVerificationUserSelection(UUID_ONE, setOf(DOMAIN_ONE), true, USER_ID) diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java new file mode 100644 index 000000000000..574f3699edb8 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_PACKAGE_NAME_KEY; +import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_USER_ID_KEY; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.os.Bundle; +import android.os.IRemoteCallback; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +/** Unit tests for {@link BackgroundInstallControlCallbackHelper} */ +@Presubmit +@RunWith(JUnit4.class) +public class BackgroundInstallControlCallbackHelperTest { + + private final IRemoteCallback mCallback = + spy( + new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle extras) {} + }); + + private BackgroundInstallControlCallbackHelper mCallbackHelper; + + @Before + public void setup() { + mCallbackHelper = new BackgroundInstallControlCallbackHelper(); + } + + @Test + public void registerBackgroundInstallControlCallback_registers_successfully() { + mCallbackHelper.registerBackgroundInstallCallback(mCallback); + + synchronized (mCallbackHelper.mCallbacks) { + assertEquals(1, mCallbackHelper.mCallbacks.getRegisteredCallbackCount()); + assertEquals(mCallback, mCallbackHelper.mCallbacks.getRegisteredCallbackItem(0)); + } + } + + @Test + public void unregisterBackgroundInstallControlCallback_unregisters_successfully() { + synchronized (mCallbackHelper.mCallbacks) { + mCallbackHelper.mCallbacks.register(mCallback); + } + + mCallbackHelper.unregisterBackgroundInstallCallback(mCallback); + + synchronized (mCallbackHelper.mCallbacks) { + assertEquals(0, mCallbackHelper.mCallbacks.getRegisteredCallbackCount()); + } + } + + @Test + public void notifyAllCallbacks_broadcastsToCallbacks() + throws RemoteException { + String testPackageName = "testname"; + int testUserId = 1; + mCallbackHelper.registerBackgroundInstallCallback(mCallback); + + mCallbackHelper.notifyAllCallbacks(testUserId, testPackageName); + + ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + verify(mCallback, after(1000).times(1)).sendResult(bundleCaptor.capture()); + Bundle receivedBundle = bundleCaptor.getValue(); + assertEquals(testPackageName, receivedBundle.getString(FLAGGED_PACKAGE_NAME_KEY)); + assertEquals(testUserId, receivedBundle.getInt(FLAGGED_USER_ID_KEY)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index 9cdaec647a43..7a77392c4fa3 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -472,6 +472,7 @@ public class FingerprintAuthenticationClientTest { verify(mUdfpsOverlayController).hideUdfpsOverlay(anyInt()); verify(mSideFpsController).hide(anyInt()); + verify(mHal, times(2)).setIgnoreDisplayTouches(false); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java index 951c93935e30..3ee54f53d44f 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.same; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -327,6 +328,7 @@ public class FingerprintEnrollClientTest { verify(mUdfpsOverlayController).hideUdfpsOverlay(anyInt()); verify(mSideFpsController).hide(anyInt()); + verify(mHal, times(2)).setIgnoreDisplayTouches(false); } @Test diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java index 5943832586b3..07e6ab2d08fb 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java @@ -19,11 +19,10 @@ package com.android.server.companion.virtual; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.startsWith; -import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import android.hardware.display.DisplayManagerInternal; @@ -86,9 +85,8 @@ public class InputControllerTest { LocalServices.removeServiceForTest(InputManagerInternal.class); LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock); - final DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.uniqueId = "uniqueId"; - doReturn(displayInfo).when(mDisplayManagerInternalMock).getDisplayInfo(anyInt()); + setUpDisplay(1 /* displayId */); + setUpDisplay(2 /* displayId */); LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); @@ -100,6 +98,16 @@ public class InputControllerTest { threadVerifier); } + void setUpDisplay(int displayId) { + final String uniqueId = "uniqueId:" + displayId; + doAnswer((inv) -> { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.uniqueId = uniqueId; + return displayInfo; + }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId)); + mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId); + } + @After public void tearDown() { mInputManagerMockHelper.tearDown(); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java index 3722247566d9..74e854e49c2a 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java @@ -20,7 +20,6 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; import android.hardware.input.IInputDevicesChangedListener; @@ -28,12 +27,15 @@ import android.hardware.input.IInputManager; import android.hardware.input.InputManagerGlobal; import android.os.RemoteException; import android.testing.TestableLooper; +import android.view.Display; import android.view.InputDevice; import org.mockito.invocation.InvocationOnMock; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.IntStream; @@ -49,6 +51,10 @@ class InputManagerMockHelper { private final InputManagerGlobal.TestSession mInputManagerGlobalSession; private final List<InputDevice> mDevices = new ArrayList<>(); private IInputDevicesChangedListener mDevicesChangedListener; + private final Map<String /* uniqueId */, Integer /* displayId */> mDisplayIdMapping = + new HashMap<>(); + private final Map<String /* phys */, String /* uniqueId */> mUniqueIdAssociation = + new HashMap<>(); InputManagerMockHelper(TestableLooper testableLooper, InputController.NativeWrapper nativeWrapperMock, IInputManager iInputManagerMock) @@ -73,8 +79,10 @@ class InputManagerMockHelper { when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]); doAnswer(inv -> mDevices.get(inv.getArgument(0))) .when(mIInputManagerMock).getInputDevice(anyInt()); - doNothing().when(mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString()); - doNothing().when(mIInputManagerMock).removeUniqueIdAssociation(anyString()); + doAnswer(inv -> mUniqueIdAssociation.put(inv.getArgument(0), inv.getArgument(1))).when( + mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString()); + doAnswer(inv -> mUniqueIdAssociation.remove(inv.getArgument(0))).when( + mIInputManagerMock).removeUniqueIdAssociation(anyString()); // Set a new instance of InputManager for testing that uses the IInputManager mock as the // interface to the server. @@ -87,17 +95,25 @@ class InputManagerMockHelper { } } + public void addDisplayIdMapping(String uniqueId, int displayId) { + mDisplayIdMapping.put(uniqueId, displayId); + } + private long handleNativeOpenInputDevice(InvocationOnMock inv) { Objects.requireNonNull(mDevicesChangedListener, "InputController did not register an InputDevicesChangedListener."); + final String phys = inv.getArgument(3); final InputDevice device = new InputDevice.Builder() .setId(mDevices.size()) .setName(inv.getArgument(0)) .setVendorId(inv.getArgument(1)) .setProductId(inv.getArgument(2)) - .setDescriptor(inv.getArgument(3)) + .setDescriptor(phys) .setExternal(true) + .setAssociatedDisplayId( + mDisplayIdMapping.getOrDefault(mUniqueIdAssociation.get(phys), + Display.INVALID_DISPLAY)) .build(); mDevices.add(device); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 5442af875e86..157e8931edba 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -39,6 +39,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -2015,6 +2016,13 @@ public class VirtualDeviceManagerServiceTest { eq(virtualDevice), any(), any())).thenReturn(displayId); virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback, NONBLOCKED_APP_PACKAGE_NAME); + final String uniqueId = UNIQUE_ID + displayId; + doAnswer(inv -> { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.uniqueId = uniqueId; + return displayInfo; + }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId)); + mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId); } private ComponentName getPermissionDialogComponent() { diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java index 8656f60afc1e..bf87e3ac1f7e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java @@ -111,6 +111,8 @@ public final class BackgroundInstallControlServiceTest { private UsageStatsManagerInternal mUsageStatsManagerInternal; @Mock private PermissionManagerServiceInternal mPermissionManager; + @Mock + private BackgroundInstallControlCallbackHelper mCallbackHelper; @Captor private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor; @@ -982,5 +984,11 @@ public final class BackgroundInstallControlServiceTest { public File getDiskFile() { return mFile; } + + + @Override + public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() { + return mCallbackHelper; + } } } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java index 3778a32b34c3..f1d3ba9db489 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java @@ -23,7 +23,6 @@ import static org.testng.Assert.assertThrows; import android.content.pm.UserProperties; import android.os.Parcel; import android.platform.test.annotations.Presubmit; -import android.platform.test.flag.junit.SetFlagsRule; import android.util.Xml; import androidx.test.filters.MediumTest; @@ -32,7 +31,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -54,13 +52,10 @@ import java.util.function.Supplier; @RunWith(AndroidJUnit4.class) @MediumTest public class UserManagerServiceUserPropertiesTest { - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); /** Test that UserProperties can properly read the xml information that it writes. */ @Test public void testWriteReadXml() throws Exception { - mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final UserProperties defaultProps = new UserProperties.Builder() .setShowInLauncher(21) .setStartWithParent(false) @@ -123,7 +118,6 @@ public class UserManagerServiceUserPropertiesTest { /** Tests parcelling an object in which all properties are present. */ @Test public void testParcelUnparcel() throws Exception { - mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final UserProperties originalProps = new UserProperties.Builder() .setShowInLauncher(2145) .build(); @@ -134,7 +128,6 @@ public class UserManagerServiceUserPropertiesTest { /** Tests copying a UserProperties object varying permissions. */ @Test public void testCopyLacksPermissions() throws Exception { - mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final UserProperties defaultProps = new UserProperties.Builder() .setShowInLauncher(2145) .setStartWithParent(true) diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java index 6cdbc7428f7e..3047bcf4b146 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java @@ -41,7 +41,6 @@ import android.content.res.XmlResourceParser; import android.os.Bundle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; -import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArrayMap; import androidx.test.InstrumentationRegistry; @@ -51,7 +50,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.servicestests.R; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -73,11 +71,8 @@ public class UserManagerServiceUserTypeTest { public void setup() { mResources = InstrumentationRegistry.getTargetContext().getResources(); } - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Test public void testUserTypeBuilder_createUserType() { - mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final Bundle restrictions = makeRestrictionsBundle("r1", "r2"); final Bundle systemSettings = makeSettingsBundle("s1", "s2"); final Bundle secureSettings = makeSettingsBundle("secure_s1", "secure_s2"); @@ -207,7 +202,6 @@ public class UserManagerServiceUserTypeTest { @Test public void testUserTypeBuilder_defaults() { - mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); UserTypeDetails type = new UserTypeDetails.Builder() .setName("name") // Required (no default allowed) .setBaseType(FLAG_FULL) // Required (no default allowed) @@ -321,7 +315,6 @@ public class UserManagerServiceUserTypeTest { /** Tests {@link UserTypeFactory#customizeBuilders} for a reasonable xml file. */ @Test public void testUserTypeFactoryCustomize_profile() throws Exception { - mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); final String userTypeAosp1 = "android.test.1"; // Profile user that is not customized final String userTypeAosp2 = "android.test.2"; // Profile user that is customized final String userTypeOem1 = "custom.test.1"; // Custom-defined profile diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index 9323b482d5e2..df2069efb0ce 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -39,7 +39,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Postsubmit; import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.MediumTest; @@ -56,7 +55,6 @@ import com.google.common.collect.Range; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -99,8 +97,6 @@ public final class UserManagerTest { private UserSwitchWaiter mUserSwitchWaiter; private UserRemovalWaiter mUserRemovalWaiter; private int mOriginalCurrentUserId; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() throws Exception { @@ -172,7 +168,6 @@ public final class UserManagerTest { @Test public void testCloneUser() throws Exception { - mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); assumeCloneEnabled(); UserHandle mainUser = mUserManager.getMainUser(); assumeTrue("Main user is null", mainUser != null); @@ -229,7 +224,8 @@ public final class UserManagerTest { .isEqualTo(cloneUserProperties.getCrossProfileContentSharingStrategy()); assertThrows(SecurityException.class, cloneUserProperties::getDeleteAppWithParent); assertThrows(SecurityException.class, cloneUserProperties::getAlwaysVisible); - assertThrows(SecurityException.class, cloneUserProperties::getProfileApiVisibility); + assertThat(typeProps.getProfileApiVisibility()).isEqualTo( + cloneUserProperties.getProfileApiVisibility()); compareDrawables(mUserManager.getUserBadge(), Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain())); @@ -311,7 +307,6 @@ public final class UserManagerTest { @Test public void testPrivateProfile() throws Exception { - mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES); UserHandle mainUser = mUserManager.getMainUser(); assumeTrue("Main user is null", mainUser != null); // Get the default properties for private profile user type. @@ -353,8 +348,8 @@ public final class UserManagerTest { assertThrows(SecurityException.class, privateProfileUserProperties::getDeleteAppWithParent); assertThrows(SecurityException.class, privateProfileUserProperties::getAllowStoppingUserWithDelayedLocking); - assertThrows(SecurityException.class, - privateProfileUserProperties::getProfileApiVisibility); + assertThat(typeProps.getProfileApiVisibility()).isEqualTo( + privateProfileUserProperties.getProfileApiVisibility()); assertThrows(SecurityException.class, privateProfileUserProperties::areItemsRestrictedOnHomeScreen); compareDrawables(mUserManager.getUserBadge(), diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index f6cf4da84b3f..77be01c9099c 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -323,6 +323,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -12971,6 +12972,35 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void fixNotification_customAllowlistToken() + throws Exception { + Notification n = new Notification.Builder(mContext, "test") + .build(); + try { + Field allowlistToken = Class.forName("android.app.Notification"). + getDeclaredField("mAllowlistToken"); + allowlistToken.setAccessible(true); + allowlistToken.set(n, new Binder()); + } catch (Exception e) { + throw new RuntimeException(e); + } + + mService.fixNotification(n, PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE, true); + + IBinder actual = null; + try { + Field allowlistToken = Class.forName("android.app.Notification"). + getDeclaredField("mAllowlistToken"); + allowlistToken.setAccessible(true); + actual = (IBinder) allowlistToken.get(n); + } catch (Exception e) { + throw new RuntimeException(e); + } + + assertTrue(mService.ALLOWLIST_TOKEN == actual); + } + + @Test public void testCancelAllNotifications_IgnoreUserInitiatedJob() throws Exception { when(mJsi.isNotificationAssociatedWithAnyUserInitiatedJobs(anyInt(), anyInt(), anyString())) .thenReturn(true); diff --git a/telecomm/java/android/telecom/DisconnectCause.java b/telecomm/java/android/telecom/DisconnectCause.java index 331caa1bad7a..7ad26c901188 100644 --- a/telecomm/java/android/telecom/DisconnectCause.java +++ b/telecomm/java/android/telecom/DisconnectCause.java @@ -16,7 +16,10 @@ package android.telecom; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.media.ToneGenerator; import android.os.Parcel; import android.os.Parcelable; @@ -25,6 +28,8 @@ import android.telephony.PreciseDisconnectCause; import android.telephony.ims.ImsReasonInfo; import android.text.TextUtils; +import com.android.server.telecom.flags.Flags; + import java.util.Objects; /** @@ -169,7 +174,9 @@ public final class DisconnectCause implements Parcelable { } /** - * Creates a new DisconnectCause instance. + * Creates a new DisconnectCause instance. This is used by Telephony to pass in extra debug + * info to Telecom regarding the disconnect cause. + * * @param code The code for the disconnect cause. * @param label The localized label to show to the user to explain the disconnect. * @param description The localized description to show to the user to explain the disconnect. @@ -180,7 +187,10 @@ public final class DisconnectCause implements Parcelable { * @param imsReasonInfo The relevant {@link ImsReasonInfo}, or {@code null} if not available. * @hide */ - public DisconnectCause(int code, CharSequence label, CharSequence description, String reason, + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public DisconnectCause(int code, @NonNull CharSequence label, + @NonNull CharSequence description, @NonNull String reason, int toneToPlay, @Annotation.DisconnectCauses int telephonyDisconnectCause, @Annotation.PreciseDisconnectCauses int telephonyPreciseDisconnectCause, @Nullable ImsReasonInfo imsReasonInfo) { @@ -241,28 +251,40 @@ public final class DisconnectCause implements Parcelable { } /** - * Returns the telephony {@link android.telephony.DisconnectCause} for the call. + * Returns the telephony {@link android.telephony.DisconnectCause} for the call. This is only + * used internally by Telecom for providing extra debug information from Telephony. + * * @return The disconnect cause. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) public @Annotation.DisconnectCauses int getTelephonyDisconnectCause() { return mTelephonyDisconnectCause; } /** - * Returns the telephony {@link android.telephony.PreciseDisconnectCause} for the call. + * Returns the telephony {@link android.telephony.PreciseDisconnectCause} for the call. This is + * only used internally by Telecom for providing extra debug information from Telephony. + * * @return The precise disconnect cause. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) public @Annotation.PreciseDisconnectCauses int getTelephonyPreciseDisconnectCause() { return mTelephonyPreciseDisconnectCause; } /** - * Returns the telephony {@link ImsReasonInfo} associated with the call disconnection. + * Returns the telephony {@link ImsReasonInfo} associated with the call disconnection. This is + * only used internally by Telecom for providing extra debug information from Telephony. + * * @return The {@link ImsReasonInfo} or {@code null} if not known. * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) public @Nullable ImsReasonInfo getImsReasonInfo() { return mImsReasonInfo; } diff --git a/telephony/java/android/service/euicc/EuiccService.java b/telephony/java/android/service/euicc/EuiccService.java index b59e855825b9..5af2c3458368 100644 --- a/telephony/java/android/service/euicc/EuiccService.java +++ b/telephony/java/android/service/euicc/EuiccService.java @@ -19,6 +19,7 @@ import static android.telephony.euicc.EuiccCardManager.ResetOption; import android.Manifest; import android.annotation.CallSuper; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -38,6 +39,8 @@ import android.telephony.euicc.EuiccManager.OtaStatus; import android.text.TextUtils; import android.util.Log; +import com.android.internal.telephony.flags.Flags; + import java.io.PrintWriter; import java.io.StringWriter; import java.lang.annotation.Retention; @@ -759,6 +762,20 @@ public abstract class EuiccService extends Service { public abstract int onRetainSubscriptionsForFactoryReset(int slotId); /** + * Return the available memory in bytes of the eUICC. + * + * @param slotId ID of the SIM slot being queried. + * @return the available memory in bytes. + * @see android.telephony.euicc.EuiccManager#getAvailableMemoryInBytes + */ + @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY) + public long onGetAvailableMemoryInBytes(int slotId) { + // stub implementation, LPA needs to implement this + throw new UnsupportedOperationException("The connected LPA does not implement" + + "EuiccService#onGetAvailableMemoryInBytes(int)"); + } + + /** * Dump to a provided printWriter. */ public void dump(@NonNull PrintWriter printWriter) { @@ -834,6 +851,22 @@ public abstract class EuiccService extends Service { } @Override + @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY) + public void getAvailableMemoryInBytes( + int slotId, IGetAvailableMemoryInBytesCallback callback) { + mExecutor.execute( + () -> { + long availableMemoryInBytes = + EuiccService.this.onGetAvailableMemoryInBytes(slotId); + try { + callback.onSuccess(availableMemoryInBytes); + } catch (RemoteException e) { + // Can't communicate with the phone process; ignore. + } + }); + } + + @Override public void startOtaIfNecessary( int slotId, IOtaStatusChangedCallback statusChangedCallback) { mExecutor.execute(new Runnable() { diff --git a/telephony/java/android/service/euicc/IEuiccService.aidl b/telephony/java/android/service/euicc/IEuiccService.aidl index f8d5ae9ca86d..0f8c72bea5de 100644 --- a/telephony/java/android/service/euicc/IEuiccService.aidl +++ b/telephony/java/android/service/euicc/IEuiccService.aidl @@ -19,6 +19,7 @@ package android.service.euicc; import android.service.euicc.IDeleteSubscriptionCallback; import android.service.euicc.IDownloadSubscriptionCallback; import android.service.euicc.IEraseSubscriptionsCallback; +import android.service.euicc.IGetAvailableMemoryInBytesCallback; import android.service.euicc.IGetDefaultDownloadableSubscriptionListCallback; import android.service.euicc.IGetDownloadableSubscriptionMetadataCallback; import android.service.euicc.IGetEidCallback; @@ -60,4 +61,5 @@ oneway interface IEuiccService { void retainSubscriptionsForFactoryReset( int slotId, in IRetainSubscriptionsForFactoryResetCallback callback); void dump(in IEuiccServiceDumpResultCallback callback); -}
\ No newline at end of file + void getAvailableMemoryInBytes(int slotId, in IGetAvailableMemoryInBytesCallback callback); +} diff --git a/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl new file mode 100644 index 000000000000..bd6d19b81d47 --- /dev/null +++ b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.euicc; + +/** @hide */ +oneway interface IGetAvailableMemoryInBytesCallback { + void onSuccess(long availableMemoryInBytes); +} diff --git a/telephony/java/android/telephony/DomainSelectionService.java b/telephony/java/android/telephony/DomainSelectionService.java index 0f54e8d9457e..3c11da5f2daa 100644 --- a/telephony/java/android/telephony/DomainSelectionService.java +++ b/telephony/java/android/telephony/DomainSelectionService.java @@ -90,7 +90,7 @@ import java.util.function.Consumer; */ @SystemApi @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE) -public class DomainSelectionService extends Service { +public abstract class DomainSelectionService extends Service { private static final String LOG_TAG = "DomainSelectionService"; @@ -152,7 +152,7 @@ public class DomainSelectionService extends Service { private boolean mIsExitedFromAirplaneMode; private @Nullable ImsReasonInfo mImsReasonInfo; private @PreciseDisconnectCauses int mCause; - private @Nullable EmergencyRegResult mEmergencyRegResult; + private @Nullable EmergencyRegistrationResult mEmergencyRegistrationResult; /** * @param slotIndex The logical slot index. @@ -172,7 +172,7 @@ public class DomainSelectionService extends Service { @Nullable Uri address, @SelectorType int selectorType, boolean video, boolean emergency, boolean isTest, boolean exited, @Nullable ImsReasonInfo imsReasonInfo, @PreciseDisconnectCauses int cause, - @Nullable EmergencyRegResult regResult) { + @Nullable EmergencyRegistrationResult regResult) { mSlotIndex = slotIndex; mSubId = subscriptionId; mCallId = callId; @@ -184,7 +184,7 @@ public class DomainSelectionService extends Service { mIsExitedFromAirplaneMode = exited; mImsReasonInfo = imsReasonInfo; mCause = cause; - mEmergencyRegResult = regResult; + mEmergencyRegistrationResult = regResult; } /** @@ -204,7 +204,7 @@ public class DomainSelectionService extends Service { mIsExitedFromAirplaneMode = s.mIsExitedFromAirplaneMode; mImsReasonInfo = s.mImsReasonInfo; mCause = s.mCause; - mEmergencyRegResult = s.mEmergencyRegResult; + mEmergencyRegistrationResult = s.mEmergencyRegistrationResult; } /** @@ -296,8 +296,8 @@ public class DomainSelectionService extends Service { /** * @return The current registration state of cellular network. */ - public @Nullable EmergencyRegResult getEmergencyRegResult() { - return mEmergencyRegResult; + public @Nullable EmergencyRegistrationResult getEmergencyRegistrationResult() { + return mEmergencyRegistrationResult; } @Override @@ -313,7 +313,7 @@ public class DomainSelectionService extends Service { + ", airplaneMode=" + mIsExitedFromAirplaneMode + ", reasonInfo=" + mImsReasonInfo + ", cause=" + mCause - + ", regResult=" + mEmergencyRegResult + + ", regResult=" + mEmergencyRegistrationResult + " }"; } @@ -331,14 +331,15 @@ public class DomainSelectionService extends Service { && mIsExitedFromAirplaneMode == that.mIsExitedFromAirplaneMode && equalsHandlesNulls(mImsReasonInfo, that.mImsReasonInfo) && mCause == that.mCause - && equalsHandlesNulls(mEmergencyRegResult, that.mEmergencyRegResult); + && equalsHandlesNulls(mEmergencyRegistrationResult, + that.mEmergencyRegistrationResult); } @Override public int hashCode() { return Objects.hash(mCallId, mAddress, mImsReasonInfo, mIsVideoCall, mIsEmergency, mIsTestEmergencyNumber, mIsExitedFromAirplaneMode, - mEmergencyRegResult, mSlotIndex, mSubId, mSelectorType, mCause); + mEmergencyRegistrationResult, mSlotIndex, mSubId, mSelectorType, mCause); } @Override @@ -359,7 +360,7 @@ public class DomainSelectionService extends Service { out.writeBoolean(mIsExitedFromAirplaneMode); out.writeParcelable(mImsReasonInfo, 0); out.writeInt(mCause); - out.writeParcelable(mEmergencyRegResult, 0); + out.writeParcelable(mEmergencyRegistrationResult, 0); } private void readFromParcel(@NonNull Parcel in) { @@ -376,8 +377,9 @@ public class DomainSelectionService extends Service { mImsReasonInfo = in.readParcelable(ImsReasonInfo.class.getClassLoader(), android.telephony.ims.ImsReasonInfo.class); mCause = in.readInt(); - mEmergencyRegResult = in.readParcelable(EmergencyRegResult.class.getClassLoader(), - EmergencyRegResult.class); + mEmergencyRegistrationResult = in.readParcelable( + EmergencyRegistrationResult.class.getClassLoader(), + EmergencyRegistrationResult.class); } public static final @NonNull Creator<SelectionAttributes> CREATOR = @@ -413,7 +415,7 @@ public class DomainSelectionService extends Service { private boolean mIsExitedFromAirplaneMode; private @Nullable ImsReasonInfo mImsReasonInfo; private @PreciseDisconnectCauses int mCause; - private @Nullable EmergencyRegResult mEmergencyRegResult; + private @Nullable EmergencyRegistrationResult mEmergencyRegistrationResult; /** * Default constructor for Builder. @@ -430,7 +432,7 @@ public class DomainSelectionService extends Service { * @param callId The call identifier. * @return The same instance of the builder. */ - public @NonNull Builder setCallId(@NonNull String callId) { + public @NonNull Builder setCallId(@Nullable String callId) { mCallId = callId; return this; } @@ -441,7 +443,7 @@ public class DomainSelectionService extends Service { * @param address The dialed address. * @return The same instance of the builder. */ - public @NonNull Builder setAddress(@NonNull Uri address) { + public @NonNull Builder setAddress(@Nullable Uri address) { mAddress = address; return this; } @@ -497,7 +499,7 @@ public class DomainSelectionService extends Service { * @param info The reason why the last PS attempt failed. * @return The same instance of the builder. */ - public @NonNull Builder setPsDisconnectCause(@NonNull ImsReasonInfo info) { + public @NonNull Builder setPsDisconnectCause(@Nullable ImsReasonInfo info) { mImsReasonInfo = info; return this; } @@ -519,8 +521,9 @@ public class DomainSelectionService extends Service { * @param regResult The current registration result for emergency services. * @return The same instance of the builder. */ - public @NonNull Builder setEmergencyRegResult(@NonNull EmergencyRegResult regResult) { - mEmergencyRegResult = regResult; + public @NonNull Builder setEmergencyRegistrationResult( + @Nullable EmergencyRegistrationResult regResult) { + mEmergencyRegistrationResult = regResult; return this; } @@ -532,7 +535,7 @@ public class DomainSelectionService extends Service { return new SelectionAttributes(mSlotIndex, mSubId, mCallId, mAddress, mSelectorType, mIsVideoCall, mIsEmergency, mIsTestEmergencyNumber, mIsExitedFromAirplaneMode, mImsReasonInfo, - mCause, mEmergencyRegResult); + mCause, mEmergencyRegistrationResult); } } } @@ -697,7 +700,7 @@ public class DomainSelectionService extends Service { public void onRequestEmergencyNetworkScan(@NonNull List<Integer> preferredNetworks, @EmergencyScanType int scanType, boolean resetScan, @NonNull CancellationSignal signal, - @NonNull Consumer<EmergencyRegResult> consumer) { + @NonNull Consumer<EmergencyRegistrationResult> consumer) { try { if (signal != null) signal.setOnCancelListener(this); mResultCallback = new IWwanSelectorResultCallbackAdapter(consumer, mExecutor); @@ -721,17 +724,18 @@ public class DomainSelectionService extends Service { private class IWwanSelectorResultCallbackAdapter extends IWwanSelectorResultCallback.Stub { - private final @NonNull Consumer<EmergencyRegResult> mConsumer; + private final @NonNull Consumer<EmergencyRegistrationResult> mConsumer; private final @NonNull Executor mExecutor; - IWwanSelectorResultCallbackAdapter(@NonNull Consumer<EmergencyRegResult> consumer, + IWwanSelectorResultCallbackAdapter( + @NonNull Consumer<EmergencyRegistrationResult> consumer, @NonNull Executor executor) { mConsumer = consumer; mExecutor = executor; } @Override - public void onComplete(@NonNull EmergencyRegResult result) { + public void onComplete(@NonNull EmergencyRegistrationResult result) { if (mConsumer == null) return; executeMethodAsyncNoException(mExecutor, @@ -759,9 +763,8 @@ public class DomainSelectionService extends Service { * @param attr Required to determine the domain. * @param callback The callback instance being registered. */ - public void onDomainSelection(@NonNull SelectionAttributes attr, - @NonNull TransportSelectorCallback callback) { - } + public abstract void onDomainSelection(@NonNull SelectionAttributes attr, + @NonNull TransportSelectorCallback callback); /** * Notifies the change in {@link ServiceState} for a specific logical slot index. @@ -836,7 +839,7 @@ public class DomainSelectionService extends Service { /** @hide */ @Override - public @Nullable IBinder onBind(@Nullable Intent intent) { + public final @Nullable IBinder onBind(@Nullable Intent intent) { if (intent == null) return null; if (SERVICE_INTERFACE.equals(intent.getAction())) { Log.i(LOG_TAG, "DomainSelectionService Bound."); @@ -863,7 +866,7 @@ public class DomainSelectionService extends Service { * @return {@link Executor} instance. * @hide */ - public @NonNull Executor getCachedExecutor() { + public final @NonNull Executor getCachedExecutor() { synchronized (mExecutorLock) { if (mExecutor == null) { Executor e = onCreateExecutor(); diff --git a/telephony/java/android/telephony/EmergencyRegResult.aidl b/telephony/java/android/telephony/EmergencyRegistrationResult.aidl index f7229621c0c1..3056031d6f03 100644 --- a/telephony/java/android/telephony/EmergencyRegResult.aidl +++ b/telephony/java/android/telephony/EmergencyRegistrationResult.aidl @@ -16,4 +16,4 @@ package android.telephony; -parcelable EmergencyRegResult; +parcelable EmergencyRegistrationResult; diff --git a/telephony/java/android/telephony/EmergencyRegResult.java b/telephony/java/android/telephony/EmergencyRegistrationResult.java index 15579be2d786..7041f5b3b556 100644 --- a/telephony/java/android/telephony/EmergencyRegResult.java +++ b/telephony/java/android/telephony/EmergencyRegistrationResult.java @@ -35,7 +35,7 @@ import java.util.Objects; */ @SystemApi @FlaggedApi(Flags.FLAG_USE_OEM_DOMAIN_SELECTION_SERVICE) -public final class EmergencyRegResult implements Parcelable { +public final class EmergencyRegistrationResult implements Parcelable { /** * Indicates the cellular network type of the acquired system. @@ -101,7 +101,7 @@ public final class EmergencyRegResult implements Parcelable { * @param iso The ISO-3166-1 alpha-2 country code equivalent, empty string if unknown. * @hide */ - public EmergencyRegResult( + public EmergencyRegistrationResult( @AccessNetworkConstants.RadioAccessNetworkType int accessNetwork, @NetworkRegistrationInfo.RegistrationState int regState, @NetworkRegistrationInfo.Domain int domain, @@ -125,7 +125,7 @@ public final class EmergencyRegResult implements Parcelable { * @param s Source emergency scan result * @hide */ - public EmergencyRegResult(@NonNull EmergencyRegResult s) { + public EmergencyRegistrationResult(@NonNull EmergencyRegistrationResult s) { mAccessNetworkType = s.mAccessNetworkType; mRegState = s.mRegState; mDomain = s.mDomain; @@ -139,9 +139,9 @@ public final class EmergencyRegResult implements Parcelable { } /** - * Construct a EmergencyRegResult object from the given parcel. + * Construct a EmergencyRegistrationResult object from the given parcel. */ - private EmergencyRegResult(@NonNull Parcel in) { + private EmergencyRegistrationResult(@NonNull Parcel in) { readFromParcel(in); } @@ -258,7 +258,7 @@ public final class EmergencyRegResult implements Parcelable { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - EmergencyRegResult that = (EmergencyRegResult) o; + EmergencyRegistrationResult that = (EmergencyRegistrationResult) o; return mAccessNetworkType == that.mAccessNetworkType && mRegState == that.mRegState && mDomain == that.mDomain @@ -311,16 +311,16 @@ public final class EmergencyRegResult implements Parcelable { mCountryIso = in.readString8(); } - public static final @NonNull Creator<EmergencyRegResult> CREATOR = - new Creator<EmergencyRegResult>() { - @Override - public EmergencyRegResult createFromParcel(@NonNull Parcel in) { - return new EmergencyRegResult(in); - } - - @Override - public EmergencyRegResult[] newArray(int size) { - return new EmergencyRegResult[size]; - } - }; + public static final @NonNull Creator<EmergencyRegistrationResult> CREATOR = + new Creator<EmergencyRegistrationResult>() { + @Override + public EmergencyRegistrationResult createFromParcel(@NonNull Parcel in) { + return new EmergencyRegistrationResult(in); + } + + @Override + public EmergencyRegistrationResult[] newArray(int size) { + return new EmergencyRegistrationResult[size]; + } + }; } diff --git a/telephony/java/android/telephony/WwanSelectorCallback.java b/telephony/java/android/telephony/WwanSelectorCallback.java index ea83815c146d..b900af3a986b 100644 --- a/telephony/java/android/telephony/WwanSelectorCallback.java +++ b/telephony/java/android/telephony/WwanSelectorCallback.java @@ -48,7 +48,8 @@ public interface WwanSelectorCallback { */ void onRequestEmergencyNetworkScan(@NonNull List<Integer> preferredNetworks, @EmergencyScanType int scanType, boolean resetScan, - @NonNull CancellationSignal signal, @NonNull Consumer<EmergencyRegResult> consumer); + @NonNull CancellationSignal signal, + @NonNull Consumer<EmergencyRegistrationResult> consumer); /** * Notifies the FW that the domain has been selected. After this method is called, diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java index 0fe43b3fbf71..7935d243397c 100644 --- a/telephony/java/android/telephony/euicc/EuiccManager.java +++ b/telephony/java/android/telephony/euicc/EuiccManager.java @@ -23,6 +23,7 @@ import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; +import android.annotation.SuppressAutoDoc; import android.annotation.SystemApi; import android.app.Activity; import android.app.PendingIntent; @@ -863,6 +864,10 @@ public class EuiccManager { */ public static final int ERROR_INVALID_PORT = 10017; + /** Temporary failure to retrieve available memory because eUICC is not ready. */ + @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY) + public static final long EUICC_MEMORY_FIELD_UNAVAILABLE = -1L; + /** * Apps targeting on Android T and beyond will get exception whenever switchToSubscription * without portIndex is called for disable subscription. @@ -963,6 +968,35 @@ public class EuiccManager { } /** + * Returns the available memory in bytes of the eUICC. + * + * @return the available memory in bytes. May be {@link #EUICC_MEMORY_FIELD_UNAVAILABLE} if the + * eUICC is not ready. Check {@link #isEnabled} for more information. + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_EUICC} or + * device doesn't support querying this information from the eUICC. + */ + @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges + @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY) + @RequiresPermission( + anyOf = { + Manifest.permission.READ_PHONE_STATE, + Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + "carrier privileges" + }) + public long getAvailableMemoryInBytes() { + if (!isEnabled()) { + return EUICC_MEMORY_FIELD_UNAVAILABLE; + } + try { + return getIEuiccController() + .getAvailableMemoryInBytes(mCardId, mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Returns the current status of eUICC OTA. * * <p>Requires the {@link android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission. diff --git a/telephony/java/android/telephony/satellite/EnableRequestAttributes.java b/telephony/java/android/telephony/satellite/EnableRequestAttributes.java new file mode 100644 index 000000000000..bc9d23081214 --- /dev/null +++ b/telephony/java/android/telephony/satellite/EnableRequestAttributes.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telephony.satellite; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; + +import com.android.internal.telephony.flags.Flags; + +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * EnableRequestAttributes is used to store the attributes of the request + * {@link SatelliteManager#requestEnabled(EnableRequestAttributes, Executor, Consumer)} + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) +public class EnableRequestAttributes { + /** {@code true} to enable satellite and {@code false} to disable satellite */ + private boolean mIsEnabled; + /** + * {@code true} to enable demo mode and {@code false} to disable. When disabling satellite, + * {@code mIsDemoMode} is always considered as {@code false} by Telephony. + */ + private boolean mIsDemoMode; + /** + * {@code true} means satellite is enabled for emergency mode, {@code false} otherwise. When + * disabling satellite, {@code isEmergencyMode} is always considered as {@code false} by + * Telephony. + */ + private boolean mIsEmergencyMode; + + /** + * Constructor from builder. + * + * @param builder Builder of {@link EnableRequestAttributes}. + */ + private EnableRequestAttributes(@NonNull Builder builder) { + this.mIsEnabled = builder.mIsEnabled; + this.mIsDemoMode = builder.mIsDemoMode; + this.mIsEmergencyMode = builder.mIsEmergencyMode; + } + + /** + * @return Whether satellite is to be enabled + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * @return Whether demo mode is to be enabled + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public boolean isDemoMode() { + return mIsDemoMode; + } + + /** + * @return Whether satellite is to be enabled for emergency mode + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public boolean isEmergencyMode() { + return mIsEmergencyMode; + } + + /** + * The builder class of {@link EnableRequestAttributes} + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public static final class Builder { + private boolean mIsEnabled; + private boolean mIsDemoMode = false; + private boolean mIsEmergencyMode = false; + + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public Builder(boolean isEnabled) { + mIsEnabled = isEnabled; + } + + /** + * Set demo mode + * + * @param isDemoMode {@code true} to enable demo mode and {@code false} to disable. When + * disabling satellite, {@code isDemoMode} is always considered as + * {@code false} by Telephony. + * @return The builder object + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + @NonNull + public Builder setDemoMode(boolean isDemoMode) { + if (mIsEnabled) { + mIsDemoMode = isDemoMode; + } + return this; + } + + /** + * Set emergency mode + * + * @param isEmergencyMode {@code true} means satellite is enabled for emergency mode, + * {@code false} otherwise. When disabling satellite, + * {@code isEmergencyMode} is always considered as {@code false} by + * Telephony. + * @return The builder object + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + @NonNull + public Builder setEmergencyMode(boolean isEmergencyMode) { + if (mIsEnabled) { + mIsEmergencyMode = isEmergencyMode; + } + return this; + } + + /** + * Build the {@link EnableRequestAttributes} + * + * @return The {@link EnableRequestAttributes} instance. + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + @NonNull + public EnableRequestAttributes build() { + return new EnableRequestAttributes(this); + } + } +} diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index b97822a9d913..2c141cf4a1f4 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -162,6 +162,13 @@ public final class SatelliteManager { /** * Bundle key to get the response from + * {@link #requestIsEmergencyModeEnabled(Executor, OutcomeReceiver)}. + * @hide + */ + public static final String KEY_EMERGENCY_MODE_ENABLED = "emergency_mode_enabled"; + + /** + * Bundle key to get the response from * {@link #requestIsSupported(Executor, OutcomeReceiver)}. * @hide */ @@ -341,6 +348,13 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23; + /** + * Telephony framework timeout to receive ACK or response from the satellite modem after + * sending a request to the modem. + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24; + /** @hide */ @IntDef(prefix = {"SATELLITE_RESULT_"}, value = { SATELLITE_RESULT_SUCCESS, @@ -366,7 +380,8 @@ public final class SatelliteManager { SATELLITE_RESULT_NOT_SUPPORTED, SATELLITE_RESULT_REQUEST_IN_PROGRESS, SATELLITE_RESULT_MODEM_BUSY, - SATELLITE_RESULT_ILLEGAL_STATE + SATELLITE_RESULT_ILLEGAL_STATE, + SATELLITE_RESULT_MODEM_TIMEOUT }) @Retention(RetentionPolicy.SOURCE) public @interface SatelliteResult {} @@ -482,9 +497,7 @@ public final class SatelliteManager { * aligned with the satellite, user can send a message and also receive a reply in demo mode. * If enableSatellite is {@code false}, enableDemoMode has no impact on the behavior. * - * @param enableSatellite {@code true} to enable the satellite modem and - * {@code false} to disable. - * @param enableDemoMode {@code true} to enable demo mode and {@code false} to disable. + * @param attributes The attributes of the enable request. * @param executor The executor on which the error code listener will be called. * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * @@ -493,9 +506,10 @@ public final class SatelliteManager { */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) - public void requestEnabled(boolean enableSatellite, boolean enableDemoMode, + public void requestEnabled(@NonNull EnableRequestAttributes attributes, @NonNull @CallbackExecutor Executor executor, @SatelliteResult @NonNull Consumer<Integer> resultListener) { + Objects.requireNonNull(attributes); Objects.requireNonNull(executor); Objects.requireNonNull(resultListener); @@ -509,8 +523,8 @@ public final class SatelliteManager { () -> resultListener.accept(result))); } }; - telephony.requestSatelliteEnabled(mSubId, enableSatellite, enableDemoMode, - errorCallback); + telephony.requestSatelliteEnabled(mSubId, attributes.isEnabled(), + attributes.isDemoMode(), attributes.isEmergencyMode(), errorCallback); } else { throw new IllegalStateException("telephony service is null."); } @@ -631,6 +645,61 @@ public final class SatelliteManager { } /** + * Request to get whether the satellite service is enabled for emergency mode. + * + * @param executor The executor on which the callback will be called. + * @param callback The callback object to which the result will be delivered. + * If the request is successful, {@link OutcomeReceiver#onResult(Object)} + * will return a {@code boolean} with value {@code true} if satellite is enabled + * for emergency mode and {@code false} otherwise. + * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} + * will return a {@link SatelliteException} with the {@link SatelliteResult}. + * + * @throws SecurityException if the caller doesn't have required permission. + */ + @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public void requestIsEmergencyModeEnabled(@NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + try { + ITelephony telephony = getITelephony(); + if (telephony != null) { + ResultReceiver receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == SATELLITE_RESULT_SUCCESS) { + if (resultData.containsKey(KEY_EMERGENCY_MODE_ENABLED)) { + boolean isEmergencyModeEnabled = + resultData.getBoolean(KEY_EMERGENCY_MODE_ENABLED); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> + callback.onResult(isEmergencyModeEnabled))); + } else { + loge("KEY_EMERGENCY_MODE_ENABLED does not exist."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> + callback.onError(new SatelliteException( + SATELLITE_RESULT_REQUEST_FAILED)))); + } + } else { + executor.execute(() -> Binder.withCleanCallingIdentity(() -> + callback.onError(new SatelliteException(resultCode)))); + } + } + }; + telephony.requestIsEmergencyModeEnabled(mSubId, receiver); + } else { + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); + } + } catch (RemoteException ex) { + loge("requestIsEmergencyModeEnabled() RemoteException: " + ex); + ex.rethrowAsRuntimeException(); + } + } + + /** * Request to get whether the satellite service is supported on the device. * * <p> diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 213fbc55d597..bd47b1fc2dc0 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2742,14 +2742,19 @@ interface ITelephony { * Request to enable or disable the satellite modem. * * @param subId The subId of the subscription to enable or disable the satellite modem for. - * @param enable True to enable the satellite modem and false to disable. - * @param isDemoModeEnabled True if demo mode is enabled and false otherwise. + * @param enableSatellite True to enable the satellite modem and false to disable. + * @param enableDemoMode True if demo mode is enabled and false otherwise. When + * disabling satellite, {@code enableDemoMode} is always considered as + * {@code false} by Telephony. + * @param isEmergency {@code true} means satellite is enabled for emergency mode, {@code false} + * otherwise. When disabling satellite, {@code isEmergency} is always + * considered as {@code false} by Telephony. * @param callback The callback to get the result of the request. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") - void requestSatelliteEnabled(int subId, boolean enable, boolean isDemoModeEnabled, - in IIntegerConsumer callback); + void requestSatelliteEnabled(int subId, boolean enableSatellite, boolean enableDemoMode, + boolean isEmergency, in IIntegerConsumer callback); /** * Request to get whether the satellite modem is enabled. @@ -2775,6 +2780,18 @@ interface ITelephony { void requestIsDemoModeEnabled(int subId, in ResultReceiver receiver); /** + * Request to get whether the satellite service is enabled with emergency mode. + * + * @param subId The subId of the subscription to request whether the satellite demo mode is + * enabled for. + * @param receiver Result receiver to get the error code of the request and whether the + * satellite is enabled with emergency mode. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + + "android.Manifest.permission.SATELLITE_COMMUNICATION)") + void requestIsEmergencyModeEnabled(int subId, in ResultReceiver receiver); + + /** * Request to get whether the satellite service is supported on the device. * * @param subId The subId of the subscription to check whether satellite is supported for. diff --git a/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl b/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl index 0d61fcbb266e..091974a30184 100644 --- a/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl +++ b/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl @@ -16,8 +16,8 @@ package com.android.internal.telephony; -import android.telephony.EmergencyRegResult; +import android.telephony.EmergencyRegistrationResult; oneway interface IWwanSelectorResultCallback { - void onComplete(in EmergencyRegResult result); + void onComplete(in EmergencyRegistrationResult result); } diff --git a/telephony/java/com/android/internal/telephony/PhoneConstants.java b/telephony/java/com/android/internal/telephony/PhoneConstants.java index 07f2916838e8..a9ebd5c2d7cd 100644 --- a/telephony/java/com/android/internal/telephony/PhoneConstants.java +++ b/telephony/java/com/android/internal/telephony/PhoneConstants.java @@ -250,9 +250,6 @@ public class PhoneConstants { */ public static final int DOMAIN_NON_3GPP_PS = 4; - /** Key to enable comparison of domain selection results from legacy and new code. */ - public static final String EXTRA_COMPARE_DOMAIN = "compare_domain"; - /** The key to specify the emergency service category */ public static final String EXTRA_EMERGENCY_SERVICE_CATEGORY = "emergency_service_category"; } diff --git a/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl b/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl index d41777256b64..053bc7d0eece 100644 --- a/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl +++ b/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl @@ -59,4 +59,5 @@ interface IEuiccController { boolean isCompatChangeEnabled(String callingPackage, long changeId); void setPsimConversionSupportedCarriers(in int[] carrierIds); boolean isPsimConversionSupported(in int carrierId); + long getAvailableMemoryInBytes(int cardId, String callingPackage); } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index 73cc2f2b4d18..f628af14a0b9 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -340,6 +340,14 @@ open class PipAppHelper(instrumentation: Instrumentation) : wmHelper.StateSyncBuilder().withPipGone().withHomeActivityVisible().waitForAndVerify() } + open fun tapPipToShowMenu(wmHelper: WindowManagerStateHelper) { + val windowRect = getWindowRect(wmHelper) + uiDevice.click(windowRect.centerX(), windowRect.centerY()) + // search and interact with the dismiss button + val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") + uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) + } + /** Close the pip window by pressing the expand button */ fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) { val windowRect = getWindowRect(wmHelper) |