diff options
860 files changed, 29887 insertions, 9903 deletions
diff --git a/Android.bp b/Android.bp index 54d9255b8a03..4473d9458d25 100644 --- a/Android.bp +++ b/Android.bp @@ -690,8 +690,6 @@ stubs_defaults { } build = [ - "StubLibraries.bp", - "ApiDocs.bp", "ProtoLibraries.bp", "TestProtoLibraries.bp", ] diff --git a/ProtoLibraries.bp b/ProtoLibraries.bp index d8f693cdedcd..fcff58187e8d 100644 --- a/ProtoLibraries.bp +++ b/ProtoLibraries.bp @@ -81,8 +81,6 @@ java_library_host { name: "platformprotos", srcs: [ ":ipconnectivity-proto-src", - ":libstats_atom_enum_protos", - ":libstats_atom_message_protos", ":libstats_internal_protos", ":statsd_internal_protos", "cmds/am/proto/instrumentation_data.proto", diff --git a/TestProtoLibraries.bp b/TestProtoLibraries.bp index 9e2a64c652fc..2d878416a156 100644 --- a/TestProtoLibraries.bp +++ b/TestProtoLibraries.bp @@ -15,8 +15,6 @@ java_library_host { name: "platformtestprotos", srcs: [ - ":libstats_atom_enum_protos", - ":libstats_atom_message_protos", ":libstats_internal_protos", ":statsd_internal_protos", ], diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 37ceb091f035..526e63cf7e29 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -1384,8 +1384,8 @@ public class JobInfo implements Parcelable { * want to call one of these methods. * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, - * an app must hold the {@link android.Manifest.permission#INTERNET} and - * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to + * an app must hold the + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to * schedule a job that requires a network. * * <p class="note"> @@ -1445,8 +1445,8 @@ public class JobInfo implements Parcelable { * constraint. * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, - * an app must hold the {@link android.Manifest.permission#INTERNET} and - * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to + * an app must hold the + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to * schedule a job that requires a network. * * @param networkRequest The detailed description of the kind of network diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java index 8e9f4746798f..17076bc4eea4 100644 --- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java +++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java @@ -98,6 +98,15 @@ public class PowerExemptionManager { public static final int TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED = 1; /** + * Delay freezing the app when the broadcast is delivered. This flag is not required if + * TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED or + * TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED are specified, as those will + * already defer freezing during the allowlist duration. + * @hide temporarily until the next release + */ + public static final int TEMPORARY_ALLOW_LIST_TYPE_APP_FREEZING_DELAYED = 1 << 2; + + /** * The list of temp allow list types. * @hide */ @@ -105,6 +114,7 @@ public class PowerExemptionManager { TEMPORARY_ALLOW_LIST_TYPE_NONE, TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED, TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, + TEMPORARY_ALLOW_LIST_TYPE_APP_FREEZING_DELAYED }) @Retention(RetentionPolicy.SOURCE) public @interface TempAllowListType {} @@ -216,6 +226,11 @@ public class PowerExemptionManager { * Set temp-allow-list for transferring accounts between users. */ public static final int REASON_ACCOUNT_TRANSFER = 104; + /** + * Set temp-allow-list for server push messaging that can be deferred. + * @hide temporarily until the next release + */ + public static final int REASON_PUSH_MESSAGING_DEFERRABLE = 105; /* Reason code range 200-299 are reserved for broadcast actions */ /** @@ -449,6 +464,7 @@ public class PowerExemptionManager { REASON_PUSH_MESSAGING_OVER_QUOTA, REASON_ACTIVITY_RECOGNITION, REASON_ACCOUNT_TRANSFER, + REASON_PUSH_MESSAGING_DEFERRABLE, REASON_BOOT_COMPLETED, REASON_PRE_BOOT_COMPLETED, REASON_LOCKED_BOOT_COMPLETED, @@ -781,6 +797,8 @@ public class PowerExemptionManager { return "ACTIVITY_RECOGNITION"; case REASON_ACCOUNT_TRANSFER: return "REASON_ACCOUNT_TRANSFER"; + case REASON_PUSH_MESSAGING_DEFERRABLE: + return "PUSH_MESSAGING_DEFERRABLE"; case REASON_BOOT_COMPLETED: return "BOOT_COMPLETED"; case REASON_PRE_BOOT_COMPLETED: diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index a0634f0e74eb..dc608e7fddfd 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -810,7 +810,7 @@ class JobConcurrencyManager { mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable, mRecycledAssignmentInfo, mRecycledPrivilegedState); - noteConcurrency(); + noteConcurrency(true); } @VisibleForTesting @@ -1437,11 +1437,13 @@ class JobConcurrencyManager { } } - private void noteConcurrency() { + private void noteConcurrency(boolean logForHistogram) { mService.mJobPackageTracker.noteConcurrency(mRunningJobs.size(), // TODO: log per type instead of only TOP mWorkCountTracker.getRunningJobCount(WORK_TYPE_TOP)); - sConcurrencyHistogramLogger.logSample(mActiveServices.size()); + if (logForHistogram) { + sConcurrencyHistogramLogger.logSample(mActiveServices.size()); + } } @GuardedBy("mLock") @@ -1582,7 +1584,9 @@ class JobConcurrencyManager { final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue(); if (pendingJobQueue.size() == 0) { worker.clearPreferredUid(); - noteConcurrency(); + // Don't log the drop in concurrency to the histogram, otherwise, we'll end up + // overcounting lower concurrency values as jobs end execution. + noteConcurrency(false); return; } if (mActiveServices.size() >= mSteadyStateConcurrencyLimit) { @@ -1612,7 +1616,9 @@ class JobConcurrencyManager { // scheduled), but we should // be able to stop the other jobs soon so don't start running anything new until we // get back below the limit. - noteConcurrency(); + // Don't log the drop in concurrency to the histogram, otherwise, we'll end up + // overcounting lower concurrency values as jobs end execution. + noteConcurrency(false); return; } } @@ -1761,7 +1767,9 @@ class JobConcurrencyManager { } } - noteConcurrency(); + // Don't log the drop in concurrency to the histogram, otherwise, we'll end up + // overcounting lower concurrency values as jobs end execution. + noteConcurrency(false); } /** diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index e4e3de270aee..92fc78e273ed 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -195,7 +195,7 @@ public class JobSchedulerService extends com.android.server.SystemService private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L; /** - * Require the app to have the INTERNET and ACCESS_NETWORK_STATE permissions when scheduling + * Require the app to have the ACCESS_NETWORK_STATE permissions when scheduling * a job with a connectivity constraint. */ @ChangeId @@ -1503,6 +1503,16 @@ public class JobSchedulerService extends com.android.server.SystemService } toCancel.enqueueWorkLocked(work); + if (toCancel.getJob().isUserInitiated()) { + // The app is in a state to successfully schedule a UI job. Presumably, the + // user has asked for this additional bit of work, so remove any demotion + // flags. Only do this for UI jobs since they have strict scheduling + // requirements; it's harder to assume other jobs were scheduled due to + // user interaction/request. + toCancel.removeInternalFlags( + JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + } mJobs.touchJob(toCancel); sEnqueuedJwiHighWaterMarkLogger.logSampleWithUid(uId, toCancel.getWorkCount()); @@ -2585,39 +2595,49 @@ public class JobSchedulerService extends com.android.server.SystemService // or the user stopped the job somehow. if (internalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH || internalStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT + || internalStopReason == JobParameters.INTERNAL_STOP_REASON_ANR || stopReason == JobParameters.STOP_REASON_USER) { numFailures++; } else { numSystemStops++; } - final int backoffAttempts = Math.max(1, - numFailures + numSystemStops / mConstants.SYSTEM_STOP_TO_FAILURE_RATIO); - long delayMillis; + final int backoffAttempts = + numFailures + numSystemStops / mConstants.SYSTEM_STOP_TO_FAILURE_RATIO; + final long earliestRuntimeMs; - switch (job.getBackoffPolicy()) { - case JobInfo.BACKOFF_POLICY_LINEAR: { - long backoff = initialBackoffMillis; - if (backoff < mConstants.MIN_LINEAR_BACKOFF_TIME_MS) { - backoff = mConstants.MIN_LINEAR_BACKOFF_TIME_MS; - } - delayMillis = backoff * backoffAttempts; - } break; - default: - if (DEBUG) { - Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential."); + if (backoffAttempts == 0) { + earliestRuntimeMs = JobStatus.NO_EARLIEST_RUNTIME; + } else { + long delayMillis; + switch (job.getBackoffPolicy()) { + case JobInfo.BACKOFF_POLICY_LINEAR: { + long backoff = initialBackoffMillis; + if (backoff < mConstants.MIN_LINEAR_BACKOFF_TIME_MS) { + backoff = mConstants.MIN_LINEAR_BACKOFF_TIME_MS; + } + delayMillis = backoff * backoffAttempts; } - case JobInfo.BACKOFF_POLICY_EXPONENTIAL: { - long backoff = initialBackoffMillis; - if (backoff < mConstants.MIN_EXP_BACKOFF_TIME_MS) { - backoff = mConstants.MIN_EXP_BACKOFF_TIME_MS; + break; + default: + if (DEBUG) { + Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential."); + } + // Intentional fallthrough. + case JobInfo.BACKOFF_POLICY_EXPONENTIAL: { + long backoff = initialBackoffMillis; + if (backoff < mConstants.MIN_EXP_BACKOFF_TIME_MS) { + backoff = mConstants.MIN_EXP_BACKOFF_TIME_MS; + } + delayMillis = (long) Math.scalb(backoff, backoffAttempts - 1); } - delayMillis = (long) Math.scalb(backoff, backoffAttempts - 1); - } break; + break; + } + delayMillis = + Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS); + earliestRuntimeMs = elapsedNowMillis + delayMillis; } - delayMillis = - Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS); JobStatus newJob = new JobStatus(failureToReschedule, - elapsedNowMillis + delayMillis, + earliestRuntimeMs, JobStatus.NO_LATEST_RUNTIME, numFailures, numSystemStops, failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis(), failureToReschedule.getCumulativeExecutionTimeMs()); @@ -4001,12 +4021,6 @@ public class JobSchedulerService extends com.android.server.SystemService if (job.getRequiredNetwork() != null && CompatChanges.isChangeEnabled( REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { - // All networking, including with the local network and even local to the device, - // requires the INTERNET permission. - if (!hasPermission(uid, pid, Manifest.permission.INTERNET)) { - throw new SecurityException(Manifest.permission.INTERNET - + " required for jobs with a connectivity constraint"); - } if (!hasPermission(uid, pid, Manifest.permission.ACCESS_NETWORK_STATE)) { throw new SecurityException(Manifest.permission.ACCESS_NETWORK_STATE + " required for jobs with a connectivity constraint"); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 2944095ec8db..bf2e4560a4ef 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -551,9 +551,6 @@ public final class JobServiceContext implements ServiceConnection { if (CompatChanges.isChangeEnabled( JobSchedulerService.REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { final String pkgName = job.getServiceComponent().getPackageName(); - if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.INTERNET)) { - return false; - } if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.ACCESS_NETWORK_STATE)) { return false; } 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 6445c3bb6f8d..b5d763c17e59 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 @@ -1177,6 +1177,10 @@ public final class JobStatus { mInternalFlags |= flags; } + public void removeInternalFlags(int flags) { + mInternalFlags = mInternalFlags & ~flags; + } + int getPreferredConstraintFlags() { return mPreferredConstraints; } diff --git a/api/Android.bp b/api/Android.bp index 24b30048fab7..f5bafe8bf8da 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -258,3 +258,8 @@ java_genrule { out: ["api_fingerprint.txt"], cmd: "cat $(in) | md5sum | cut -d' ' -f1 > $(out)", } + +build = [ + "ApiDocs.bp", + "StubLibraries.bp", +] diff --git a/ApiDocs.bp b/api/ApiDocs.bp index fbcaa52f9bb4..fbcaa52f9bb4 100644 --- a/ApiDocs.bp +++ b/api/ApiDocs.bp diff --git a/StubLibraries.bp b/api/StubLibraries.bp index f08745b5cd2c..f08745b5cd2c 100644 --- a/StubLibraries.bp +++ b/api/StubLibraries.bp diff --git a/api/docs b/api/docs new file mode 120000 index 000000000000..a9594bfe4ab6 --- /dev/null +++ b/api/docs @@ -0,0 +1 @@ +../docs
\ No newline at end of file diff --git a/core/api/test-current.txt b/core/api/test-current.txt index e42e52619320..9eb9d66cb71a 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -4156,6 +4156,7 @@ package android.window { public static class WindowInfosListenerForTest.WindowInfo { field @NonNull public final android.graphics.Rect bounds; field public final boolean isTrustedOverlay; + field public final boolean isVisible; field @NonNull public final String name; field @NonNull public final android.os.IBinder windowToken; } diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java index c0c59a24dd8d..ac9c497f2a36 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -17,7 +17,9 @@ package android.app; import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.AppOpsManager.MODE_FOREGROUND; +import static android.app.AppOpsManager.MODE_IGNORED; import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA; @@ -36,6 +38,8 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESS import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED; +import static android.permission.PermissionCheckerManager.PERMISSION_HARD_DENIED; +import static android.permission.PermissionCheckerManager.PERMISSION_SOFT_DENIED; import android.Manifest; import android.annotation.IntDef; @@ -43,6 +47,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.compat.CompatChanges; +import android.app.role.RoleManager; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.Disabled; @@ -58,6 +63,7 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.permission.PermissionCheckerManager; import android.provider.DeviceConfig; import android.text.TextUtils; @@ -74,6 +80,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Optional; /** @@ -264,7 +271,8 @@ public abstract class ForegroundServiceTypePolicy { null /* allOfPermissions */, null /* anyOfPermissions */, null /* permissionEnforcementFlag */, - false /* permissionEnforcementFlagDefaultValue */ + false /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -280,7 +288,8 @@ public abstract class ForegroundServiceTypePolicy { null /* allOfPermissions */, null /* anyOfPermissions */, null /* permissionEnforcementFlag */, - false /* permissionEnforcementFlagDefaultValue */ + false /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -298,7 +307,8 @@ public abstract class ForegroundServiceTypePolicy { }, true), null /* anyOfPermissions */, FGS_TYPE_PERM_ENFORCEMENT_FLAG_DATA_SYNC /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -316,7 +326,8 @@ public abstract class ForegroundServiceTypePolicy { }, true), null /* anyOfPermissions */, FGS_TYPE_PERM_ENFORCEMENT_FLAG_MEDIA_PLAYBACK /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -333,10 +344,12 @@ public abstract class ForegroundServiceTypePolicy { new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL) }, true), new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { - new RegularPermission(Manifest.permission.MANAGE_OWN_CALLS) + new RegularPermission(Manifest.permission.MANAGE_OWN_CALLS), + new RolePermission(RoleManager.ROLE_DIALER) }, false), FGS_TYPE_PERM_ENFORCEMENT_FLAG_PHONE_CALL /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -357,7 +370,8 @@ public abstract class ForegroundServiceTypePolicy { new RegularPermission(Manifest.permission.ACCESS_FINE_LOCATION), }, false), FGS_TYPE_PERM_ENFORCEMENT_FLAG_LOCATION /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + true /* foregroundOnlyPermission */ ); /** @@ -387,7 +401,8 @@ public abstract class ForegroundServiceTypePolicy { new UsbAccessoryPermission(), }, false), FGS_TYPE_PERM_ENFORCEMENT_FLAG_CONNECTED_DEVICE /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -408,7 +423,8 @@ public abstract class ForegroundServiceTypePolicy { new AppOpPermission(AppOpsManager.OP_PROJECT_MEDIA) }, false), FGS_TYPE_PERM_ENFORCEMENT_FLAG_MEDIA_PROJECTION /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -429,7 +445,8 @@ public abstract class ForegroundServiceTypePolicy { new RegularPermission(Manifest.permission.SYSTEM_CAMERA), }, false), FGS_TYPE_PERM_ENFORCEMENT_FLAG_CAMERA /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + true /* foregroundOnlyPermission */ ); /** @@ -454,7 +471,8 @@ public abstract class ForegroundServiceTypePolicy { new RegularPermission(Manifest.permission.RECORD_AUDIO), }, false), FGS_TYPE_PERM_ENFORCEMENT_FLAG_MICROPHONE /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + true /* foregroundOnlyPermission */ ); /** @@ -476,7 +494,8 @@ public abstract class ForegroundServiceTypePolicy { new RegularPermission(Manifest.permission.HIGH_SAMPLING_RATE_SENSORS), }, false), FGS_TYPE_PERM_ENFORCEMENT_FLAG_HEALTH /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -494,7 +513,8 @@ public abstract class ForegroundServiceTypePolicy { }, true), null /* anyOfPermissions */, FGS_TYPE_PERM_ENFORCEMENT_FLAG_REMOTE_MESSAGING /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -516,7 +536,8 @@ public abstract class ForegroundServiceTypePolicy { new AppOpPermission(AppOpsManager.OP_ACTIVATE_VPN), }, false), FGS_TYPE_PERM_ENFORCEMENT_FLAG_SYSTEM_EXEMPTED /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -532,7 +553,8 @@ public abstract class ForegroundServiceTypePolicy { null /* allOfPermissions */, null /* anyOfPermissions */, null /* permissionEnforcementFlag */, - false /* permissionEnforcementFlagDefaultValue */ + false /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -550,7 +572,8 @@ public abstract class ForegroundServiceTypePolicy { }, true), null /* anyOfPermissions */, null /* permissionEnforcementFlag */, - false /* permissionEnforcementFlagDefaultValue */ + false /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -568,7 +591,8 @@ public abstract class ForegroundServiceTypePolicy { }, true), null /* anyOfPermissions */, FGS_TYPE_PERM_ENFORCEMENT_FLAG_SPECIAL_USE /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */ + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */ ); /** @@ -636,6 +660,29 @@ public abstract class ForegroundServiceTypePolicy { public @interface ForegroundServicePolicyCheckCode{} /** + * Whether or not to require that app to have actual access to certain foreground only + * permissions before starting the foreground service. + * + * <p> + * Examples here are microphone, camera and fg location related permissions. + * When the user grants the permission, its permission state is set to "granted", + * but the actual capability to access these sensors, is to be evaluated according to + * its process state. The Android {@link android.os.Build.VERSION_CODES#R} introduced + * the while-in-use permission, basically the background-started FGS will not have access + * to these sensors. In this context, there is no legitimate reasons to start a FGS from + * the background with these types. This flag controls the behavior of the enforcement, + * when it's enabled, in the aforementioned case, the FGS start will result in + * a SecurityException. </p> + */ + private static final String FGS_TYPE_FG_PERM_ENFORCEMENT_FLAG = + "fgs_type_fg_perm_enforcement_flag"; + + /** + * The default value to the {@link #FGS_TYPE_FG_PERM_ENFORCEMENT_FLAG}. + */ + private static final boolean DEFAULT_FGS_TYPE_FG_PERM_ENFORCEMENT_FLAG_VALUE = true; + + /** * @return The policy info for the given type. */ @NonNull @@ -677,6 +724,11 @@ public abstract class ForegroundServiceTypePolicy { } } + private static boolean isFgsTypeFgPermissionEnforcementEnabled() { + return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + FGS_TYPE_FG_PERM_ENFORCEMENT_FLAG, DEFAULT_FGS_TYPE_FG_PERM_ENFORCEMENT_FLAG_VALUE); + } + /** * Constructor. * @@ -734,6 +786,12 @@ public abstract class ForegroundServiceTypePolicy { final boolean mPermissionEnforcementFlagDefaultValue; /** + * Whether or not the permissions here are limited to foreground only. + * Typical examples are microphone/camera/location. + */ + final boolean mForegroundOnlyPermission; + + /** * A customized check for the permissions. */ @Nullable ForegroundServiceTypePermission mCustomPermission; @@ -769,7 +827,8 @@ public abstract class ForegroundServiceTypePolicy { @Nullable ForegroundServiceTypePermissions allOfPermissions, @Nullable ForegroundServiceTypePermissions anyOfPermissions, @Nullable String permissionEnforcementFlag, - boolean permissionEnforcementFlagDefaultValue) { + boolean permissionEnforcementFlagDefaultValue, + boolean foregroundOnlyPermission) { mType = type; mDeprecationChangeId = deprecationChangeId; mDisabledChangeId = disabledChangeId; @@ -778,6 +837,7 @@ public abstract class ForegroundServiceTypePolicy { mPermissionEnforcementFlag = permissionEnforcementFlag; mPermissionEnforcementFlagDefaultValue = permissionEnforcementFlagDefaultValue; mPermissionEnforcementFlagValue = permissionEnforcementFlagDefaultValue; + mForegroundOnlyPermission = foregroundOnlyPermission; } /** @@ -880,6 +940,14 @@ public abstract class ForegroundServiceTypePolicy { } /** + * Whether or not the permissions here are limited to foreground only. + * Typical examples are microphone/camera/location. + */ + public boolean hasForegroundOnlyPermission() { + return mForegroundOnlyPermission; + } + + /** * Override the type disabling change Id. * * For test only. @@ -1077,26 +1145,45 @@ public abstract class ForegroundServiceTypePolicy { @PackageManager.PermissionResult int checkPermission(@NonNull Context context, @NonNull String name, int callerUid, int callerPid, String packageName, boolean allowWhileInUse) { - // Simple case, check if it's already granted. - @PackageManager.PermissionResult int result; - if ((result = PermissionChecker.checkPermissionForPreflight(context, name, - callerPid, callerUid, packageName)) == PERMISSION_GRANTED) { - return PERMISSION_GRANTED; + @PermissionCheckerManager.PermissionResult final int result = + PermissionChecker.checkPermissionForPreflight(context, name, + callerPid, callerUid, packageName); + if (result == PERMISSION_HARD_DENIED) { + // If the user didn't grant this permission at all. + return PERMISSION_DENIED; } - if (allowWhileInUse && result == PermissionCheckerManager.PERMISSION_SOFT_DENIED) { - // Check its appops - final int opCode = AppOpsManager.permissionToOpCode(name); - final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); - if (opCode != AppOpsManager.OP_NONE) { - final int currentMode = appOpsManager.unsafeCheckOpRawNoThrow(opCode, callerUid, - packageName); - if (currentMode == MODE_FOREGROUND) { - // It's in foreground only mode and we're allowing while-in-use. - return PERMISSION_GRANTED; - } - } + final int opCode = AppOpsManager.permissionToOpCode(name); + if (opCode == AppOpsManager.OP_NONE) { + // Simple case, check if it's already granted. + return result == PermissionCheckerManager.PERMISSION_GRANTED + ? PERMISSION_GRANTED : PERMISSION_DENIED; + } + final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); + final int mode = appOpsManager.unsafeCheckOpRawNoThrow(opCode, callerUid, packageName); + switch (mode) { + case MODE_ALLOWED: + // The appop is just allowed, plain and simple. + return PERMISSION_GRANTED; + case MODE_DEFAULT: + // Follow the permission check result. + return result == PermissionCheckerManager.PERMISSION_GRANTED + ? PERMISSION_GRANTED : PERMISSION_DENIED; + case MODE_FOREGROUND: + // If the enforcement flag is OFF, we silently allow it. Or, if it's in + // the foreground only mode and we're allowing while-in-use, allow it. + return !isFgsTypeFgPermissionEnforcementEnabled() || allowWhileInUse + ? PERMISSION_GRANTED : PERMISSION_DENIED; + case MODE_IGNORED: + // If it's soft denied with the mode "ignore", semantically it's a silent + // failure and no exception should be thrown, we might not want to allow + // the FGS. However, since the user has agreed with this permission + // (otherwise it's going to be a hard denial), and we're allowing + // while-in-use here, it's safe to allow the FGS run here. + return allowWhileInUse && result == PERMISSION_SOFT_DENIED + ? PERMISSION_GRANTED : PERMISSION_DENIED; + default: + return PERMISSION_DENIED; } - return PERMISSION_DENIED; } } @@ -1123,6 +1210,29 @@ public abstract class ForegroundServiceTypePolicy { } /** + * This represents a particular role an app needs to hold for a specific service type. + */ + static class RolePermission extends ForegroundServiceTypePermission { + final String mRole; + + RolePermission(@NonNull String role) { + super(role); + mRole = role; + } + + @Override + @PackageManager.PermissionResult + public int checkPermission(@NonNull Context context, int callerUid, int callerPid, + String packageName, boolean allowWhileInUse) { + final RoleManager rm = context.getSystemService(RoleManager.class); + final List<String> holders = rm.getRoleHoldersAsUser(mRole, + UserHandle.getUserHandleForUid(callerUid)); + return holders != null && holders.contains(packageName) + ? PERMISSION_GRANTED : PERMISSION_DENIED; + } + } + + /** * This represents a special Android permission to be required for accessing usb devices. */ static class UsbDevicePermission extends ForegroundServiceTypePermission { diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index e15e08fc0ef0..0ec3847d29f4 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -147,13 +147,13 @@ interface IActivityManager { int checkPermission(in String permission, int pid, int uid); /** Logs start of an API call to associate with an FGS, used for FGS Type Metrics */ - void logFgsApiBegin(int apiType, int appUid, int appPid); + oneway void logFgsApiBegin(int apiType, int appUid, int appPid); /** Logs stop of an API call to associate with an FGS, used for FGS Type Metrics */ - void logFgsApiEnd(int apiType, int appUid, int appPid); + oneway void logFgsApiEnd(int apiType, int appUid, int appPid); /** Logs API state change to associate with an FGS, used for FGS Type Metrics */ - void logFgsApiStateChanged(int apiType, int state, int appUid, int appPid); + oneway void logFgsApiStateChanged(int apiType, int state, int appUid, int appPid); // =============== End of transactions used on native side as well ============================ // Special low-level communication with activity manager. diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index 94c46fa68edb..a3b82e935673 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -258,4 +258,11 @@ interface IWallpaperManager { * @hide */ boolean isLockscreenLiveWallpaperEnabled(); + + /** + * Temporary method for project b/270726737. + * Return true if the wallpaper supports different crops for different display dimensions. + * @hide + */ + boolean isMultiCropEnabled(); } diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index a6313dbf52df..776e34bb4792 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -571,6 +571,15 @@ public class StatusBarManager { @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) private static final long MEDIA_CONTROL_SESSION_ACTIONS = 203800354L; + /** + * Media controls based on {@link android.app.Notification.MediaStyle} notifications will be + * required to include a non-empty title, either in the {@link android.media.MediaMetadata} or + * notification title. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private static final long MEDIA_CONTROL_REQUIRES_TITLE = 274775190L; + @UnsupportedAppUsage private Context mContext; private IStatusBarService mService; @@ -1217,6 +1226,21 @@ public class StatusBarManager { } /** + * Checks whether the given package must include a non-empty title for its media controls. + * + * @param packageName App posting media controls + * @param user Current user handle + * @return true if the app is required to provide a non-empty title + * + * @hide + */ + @RequiresPermission(allOf = {android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG, + android.Manifest.permission.LOG_COMPAT_CHANGE}) + public static boolean isMediaTitleRequiredForApp(String packageName, UserHandle user) { + return CompatChanges.isChangeEnabled(MEDIA_CONTROL_REQUIRES_TITLE, packageName, user); + } + + /** * Checks whether the supplied activity can {@link Activity#startActivityForResult(Intent, int)} * a system activity that captures content on the screen to take a screenshot. * diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java index be1d8b8ad7d3..b710644a308c 100644 --- a/core/java/android/app/WallpaperColors.java +++ b/core/java/android/app/WallpaperColors.java @@ -588,7 +588,7 @@ public final class WallpaperColors implements Parcelable { int hints = 0; double meanLuminance = totalLuminance / pixels.length; - if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) { + if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels <= maxDarkPixels) { hints |= HINT_SUPPORTS_DARK_TEXT; } if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) { diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 1603cd9e2c81..57935e3bd5b1 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -314,6 +314,7 @@ public class WallpaperManager { private final boolean mWcgEnabled; private final ColorManagementProxy mCmProxy; private static Boolean sIsLockscreenLiveWallpaperEnabled = null; + private static Boolean sIsMultiCropEnabled = null; /** * Special drawable that draws a wallpaper as fast as possible. Assumes @@ -866,6 +867,26 @@ public class WallpaperManager { } /** + * Temporary method for project b/270726737 + * @return true if the wallpaper supports different crops for different display dimensions + * @hide + */ + public static boolean isMultiCropEnabled() { + if (sGlobals == null) { + sIsMultiCropEnabled = SystemProperties.getBoolean( + "persist.wm.debug.wallpaper_multi_crop", false); + } + if (sIsMultiCropEnabled == null) { + try { + sIsMultiCropEnabled = sGlobals.mService.isMultiCropEnabled(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + return sIsMultiCropEnabled; + } + + /** * Indicate whether wcg (Wide Color Gamut) should be enabled. * <p> * Some devices lack of capability of mixed color spaces composition, @@ -1659,14 +1680,14 @@ public class WallpaperManager { * @hide */ public void addOnColorsChangedListener(@NonNull LocalWallpaperColorConsumer callback, - List<RectF> regions) throws IllegalArgumentException { + List<RectF> regions, int which) throws IllegalArgumentException { for (RectF region : regions) { if (!LOCAL_COLOR_BOUNDS.contains(region)) { throw new IllegalArgumentException("Regions must be within bounds " + LOCAL_COLOR_BOUNDS); } } - sGlobals.addOnColorsChangedListener(callback, regions, FLAG_SYSTEM, + sGlobals.addOnColorsChangedListener(callback, regions, which, mContext.getUserId(), mContext.getDisplayId()); } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index e9fb8110b4b2..783e7d3e2afc 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -8359,6 +8359,26 @@ public class DevicePolicyManager { * from Android {@link android.os.Build.VERSION_CODES#R}, requests to disable camera from * legacy device admins targeting SDK version {@link android.os.Build.VERSION_CODES#P} or * below will be silently ignored. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the camera disabled + * 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 returned from + * {@link DevicePolicyIdentifiers#getIdentifierForUserRestriction(String)} with user restriction + * {@link UserManager#DISALLOW_CAMERA} + * <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 or null if the caller is not a device admin @@ -9783,6 +9803,27 @@ public class DevicePolicyManager { * <p> * The calling device admin must be a profile owner or device owner. If it is not, a security * exception will be thrown. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the persistent preferred + * activity 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#PERSISTENT_PREFERRED_ACTIVITY_POLICY} + * <li> The additional policy params bundle, which contains + * {@link PolicyUpdateReceiver#EXTRA_INTENT_FILTER} the intent filter the policy applies to + * <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. * * <p>NOTE: Performs disk I/O and shouldn't be called on the main thread. * @@ -9816,6 +9857,27 @@ public class DevicePolicyManager { * <p> * The calling device admin must be a profile owner. If it is not, a security exception will be * thrown. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the persistent preferred + * activity policy has been cleared, {@link PolicyUpdateReceiver#onPolicySetResult(Context, + * String, Bundle, TargetUser, PolicyUpdateResult)} will notify the admin on whether the policy + * was successfully cleared or not. This callback will contain: + * <ul> + * <li> The policy identifier + * {@link DevicePolicyIdentifiers#PERSISTENT_PREFERRED_ACTIVITY_POLICY} + * <li> The additional policy params bundle, which contains + * {@link PolicyUpdateReceiver#EXTRA_INTENT_FILTER} the intent filter the policy applies to + * <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 cleared or the + * reason the policy failed to be cleared + * (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. @@ -9862,6 +9924,9 @@ public class DevicePolicyManager { * profile owner of an organization-owned managed profile. * @throws IllegalArgumentException if called on the parent profile and the package * provided is not a pre-installed system package. + * @throws IllegalStateException while trying to set default sms app on the profile and + * {@link ManagedSubscriptionsPolicy#TYPE_ALL_MANAGED_SUBSCRIPTIONS} + * policy is not set. */ @RequiresPermission(value = MANAGE_DEVICE_POLICY_DEFAULT_SMS, conditional = true) public void setDefaultSmsApplication(@Nullable ComponentName admin, @@ -11472,6 +11537,26 @@ public class DevicePolicyManager { * {@link #getParentProfileInstance(ComponentName)}. To set a restriction globally, call * {@link #addUserRestrictionGlobally} instead. * + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the user restriction + * 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 returned from + * {@link DevicePolicyIdentifiers#getIdentifierForUserRestriction(String)} + * <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. * @param key The key of the restriction. * @throws SecurityException if {@code admin} is not a device or profile owner and if the caller @@ -11504,6 +11589,25 @@ public class DevicePolicyManager { * <p> See the constants in {@link android.os.UserManager} for the list of restrictions that can * be enforced device-wide. These constants will also state in their documentation which * permission is required to manage the restriction using this API. + * <p> + * After the user restriction 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 returned from + * {@link DevicePolicyIdentifiers#getIdentifierForUserRestriction(String)} + * <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 key The key of the restriction. * @throws SecurityException if {@code admin} is not a device or profile owner and if the @@ -11541,6 +11645,26 @@ public class DevicePolicyManager { * above, calling this API will result in clearing any local and global restriction with the * specified key that was previously set by the caller. * + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the user restriction + * policy has been cleared, {@link PolicyUpdateReceiver#onPolicySetResult(Context, String, + * Bundle, TargetUser, PolicyUpdateResult)} will notify the admin on whether the policy was + * successfully cleared or not. This callback will contain: + * <ul> + * <li> The policy identifier returned from + * {@link DevicePolicyIdentifiers#getIdentifierForUserRestriction(String)} + * <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 cleared or the + * reason the policy failed to be cleared + * (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. * @param key The key of the restriction. * @throws SecurityException if {@code admin} is not a device or profile owner and if the @@ -11689,6 +11813,27 @@ public class DevicePolicyManager { * {@link #getParentProfileInstance(ComponentName)}, where the caller must be the profile owner * of an organization-owned managed profile and the package must be a system package. If called * on the parent instance, then the package is hidden or unhidden in the personal profile. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the application hidden + * 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#APPLICATION_HIDDEN_POLICY} + * <li> The additional policy params bundle, which contains + * {@link PolicyUpdateReceiver#EXTRA_PACKAGE_NAME} the package name the policy applies to + * <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, or * {@code null} if the caller is not a device admin. @@ -11728,6 +11873,9 @@ public class DevicePolicyManager { * of an organization-owned managed profile and the package must be a system package. If called * on the parent instance, this will determine whether the package is hidden or unhidden in the * personal profile. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, 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, or * {@code null} if the caller is not a device admin. @@ -11852,6 +12000,27 @@ public class DevicePolicyManager { * {@link #getParentProfileInstance(ComponentName)} by the profile owner on an * organization-owned device, to restrict accounts that may not be managed on the primary * profile. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the account management + * disabled 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#ACCOUNT_MANAGEMENT_DISABLED_POLICY} + * <li> The additional policy params bundle, which contains + * {@link PolicyUpdateReceiver#EXTRA_ACCOUNT_TYPE} the account type the policy applies to + * <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. @@ -11984,6 +12153,28 @@ public class DevicePolicyManager { * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCK_TASK}. See * {@link #isAffiliatedUser}. * Any package set via this method will be cleared if the user becomes unaffiliated. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the lock task 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#LOCK_TASK_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. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, lock task features and lock task + * packages are bundled as one policy. A failure to apply one will result in a failure to apply + * the other. * * @param packages The list of packages allowed to enter lock task mode * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the @@ -12013,6 +12204,9 @@ public class DevicePolicyManager { /** * Returns the list of packages allowed to start the lock task mode. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, 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. @@ -12065,6 +12259,28 @@ public class DevicePolicyManager { * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCK_TASK}. See * {@link #isAffiliatedUser}. * Any features set using this method are cleared if the user becomes unaffiliated. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the lock task features + * 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#LOCK_TASK_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. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, lock task features and lock task + * packages are bundled as one policy. A failure to apply one will result in a failure to apply + * the other. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the * caller is not a device admin. @@ -12089,6 +12305,9 @@ public class DevicePolicyManager { /** * Gets which system features are enabled for LockTask mode. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, 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. @@ -12574,6 +12793,27 @@ public class DevicePolicyManager { * profile owner, or by a delegate given the {@link #DELEGATION_BLOCK_UNINSTALL} scope via * {@link #setDelegatedScopes} or holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APPS_CONTROL}. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the set uninstall blocked + * 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#PACKAGE_UNINSTALL_BLOCKED_POLICY} + * <li> The additional policy params bundle, which contains + * {@link PolicyUpdateReceiver#EXTRA_PACKAGE_NAME} the package name the policy applies to + * <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. @@ -12611,6 +12851,9 @@ public class DevicePolicyManager { * <strong>Note:</strong> If your app targets Android 11 (API level 30) or higher, * this method returns a filtered result. Learn more about how to * <a href="/training/basics/intents/package-visibility">manage package visibility</a>. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, the returned policy will be the + * current resolved policy rather than the policy set by the calling admin. * * @param admin The name of the admin component whose blocking policy will be checked, or * {@code null} to check whether any admin has blocked the uninstallation. Starting @@ -15492,7 +15735,7 @@ public class DevicePolicyManager { throwIfParentInstance("getAllCrossProfilePackages"); if (mService != null) { try { - return new ArraySet<>(mService.getAllCrossProfilePackages()); + return new ArraySet<>(mService.getAllCrossProfilePackages(mContext.getUserId())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -15720,6 +15963,25 @@ public class DevicePolicyManager { * control over apps. User will not be able to clear app data or force-stop packages. When * called by a device owner, applies to all users on the device. Packages with user control * disabled are exempted from App Standby Buckets. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, after the user control disabled + * packages 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#USER_CONTROL_DISABLED_PACKAGES_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. @@ -15746,6 +16008,9 @@ public class DevicePolicyManager { * Returns the list of packages over which user control is disabled by a device or profile * owner or holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APPS_CONTROL}. + * <p> + * Starting from {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, 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. diff --git a/core/java/android/app/admin/DevicePolicyManagerInternal.java b/core/java/android/app/admin/DevicePolicyManagerInternal.java index 53459473cd54..0e78275fa30b 100644 --- a/core/java/android/app/admin/DevicePolicyManagerInternal.java +++ b/core/java/android/app/admin/DevicePolicyManagerInternal.java @@ -23,6 +23,7 @@ import android.content.ComponentName; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; +import android.os.UserManager.EnforcingUser; import java.util.List; import java.util.Set; @@ -206,7 +207,7 @@ public abstract class DevicePolicyManagerInternal { * * @hide */ - public abstract List<String> getAllCrossProfilePackages(); + public abstract List<String> getAllCrossProfilePackages(int userId); /** * Returns the default package names set by the OEM that are allowed to communicate @@ -326,4 +327,10 @@ public abstract class DevicePolicyManagerInternal { */ public abstract List<Bundle> getApplicationRestrictionsPerAdminForUser( String packageName, @UserIdInt int userId); + + /** + * Returns a list of users who set a user restriction on a given user. + */ + public abstract List<EnforcingUser> getUserRestrictionSources(String restriction, + @UserIdInt int userId); } diff --git a/core/java/android/app/admin/DevicePolicyResources.java b/core/java/android/app/admin/DevicePolicyResources.java index 0512c7511ec6..456c6af134cf 100644 --- a/core/java/android/app/admin/DevicePolicyResources.java +++ b/core/java/android/app/admin/DevicePolicyResources.java @@ -1864,9 +1864,54 @@ public final class DevicePolicyResources { public static final String WORK_PROFILE_TELEPHONY_PAUSED_TURN_ON_BUTTON = PREFIX + "TURN_ON_WORK_PROFILE_BUTTON_TEXT"; + /** + * Information section shown on a dialog when the user is unable to place a call in + * the personal profile due to admin restrictions, and must choose whether to place + * the call from the work profile or cancel. + */ + public static final String MINIRESOLVER_WORK_TELEPHONY_CALL_BLOCKED_INFORMATION = + PREFIX + "MINIRESOLVER_WORK_TELEPHONY_INFORMATION"; + + /** + * Information section shown on a dialog when the user is unable to send a text in + * the personal profile due to admin restrictions, and must choose whether to place + * the call from the work profile or cancel. + */ + public static final String MINIRESOLVER_WORK_TELEPHONY_TEXT_BLOCKED_INFORMATION = + PREFIX + "MINIRESOLVER_WORK_TELEPHONY_INFORMATION"; + + + /** + * Button for a dialog shown when the user is unable to place a call in the personal + * profile due to admin restrictions, and must choose whether to place the call from + * the work profile or cancel. + */ + public static final String MINIRESOLVER_CALL_FROM_WORK = + PREFIX + "MINIRESOLVER_CALL_FROM_WORK"; + + /** + * Button for a dialog shown when the user has no apps capable of handling an intent + * in the personal profile, and must choose whether to open the intent in a + * cross-profile app in the work profile, or cancel. + */ + public static final String MINIRESOLVER_SWITCH_TO_WORK = + PREFIX + "MINIRESOLVER_SWITCH_TO_WORK"; + + /** + * Title for a dialog shown when the user has no apps capable of handling an intent + * in the personal profile, and must choose whether to open the intent in a + * cross-profile app in the work profile, or open in the same profile browser. Accepts + * the app name as a param. + */ public static final String MINIRESOLVER_OPEN_IN_WORK = PREFIX + "MINIRESOLVER_OPEN_IN_WORK"; + /** + * Title for a dialog shown when the user has no apps capable of handling an intent + * in the personal profile, and must choose whether to open the intent in a + * cross-profile app in the personal profile, or open in the same profile browser. + * Accepts the app name as a param. + */ public static final String MINIRESOLVER_OPEN_IN_PERSONAL = PREFIX + "MINIRESOLVER_OPEN_IN_PERSONAL"; diff --git a/core/java/android/app/admin/DeviceStateCache.java b/core/java/android/app/admin/DeviceStateCache.java index d1d130d88a39..f37f5411be2c 100644 --- a/core/java/android/app/admin/DeviceStateCache.java +++ b/core/java/android/app/admin/DeviceStateCache.java @@ -50,6 +50,14 @@ public abstract class DeviceStateCache { public abstract boolean isUserOrganizationManaged(@UserIdInt int userHandle); /** + * Returns whether a user has affiliated IDs. + */ + + public boolean hasAffiliationWithDevice(int userId) { + return false; + } + + /** * Empty implementation. */ private static class EmptyDeviceStateCache extends DeviceStateCache { diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 9795caba95b8..003e804831a4 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -515,7 +515,7 @@ interface IDevicePolicyManager { void setCrossProfilePackages(in ComponentName admin, in List<String> packageNames); List<String> getCrossProfilePackages(in ComponentName admin); - List<String> getAllCrossProfilePackages(); + List<String> getAllCrossProfilePackages(int userId); List<String> getDefaultCrossProfilePackages(); boolean isManagedKiosk(); diff --git a/core/java/android/app/admin/PolicyKey.java b/core/java/android/app/admin/PolicyKey.java index 3544c19fe869..9b12e5969c2e 100644 --- a/core/java/android/app/admin/PolicyKey.java +++ b/core/java/android/app/admin/PolicyKey.java @@ -25,6 +25,7 @@ import android.os.Parcelable; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import android.util.Log; import org.xmlpull.v1.XmlPullParserException; @@ -41,6 +42,9 @@ import java.util.Objects; @SuppressLint({"ParcelNotFinal", "ParcelCreator"}) @SystemApi public abstract class PolicyKey implements Parcelable { + + static final String TAG = "PolicyKey"; + /** * @hide */ @@ -76,9 +80,14 @@ public abstract class PolicyKey implements Parcelable { /** * @hide */ + @Nullable public static PolicyKey readGenericPolicyKeyFromXml(TypedXmlPullParser parser) { String identifier = parser.getAttributeValue( /* namespace= */ null, ATTR_POLICY_IDENTIFIER); + if (identifier == null) { + Log.wtf(TAG, "Error parsing generic policy key, identifier is null."); + return null; + } return new NoArgsPolicyKey(identifier); } diff --git a/core/java/android/app/assist/OWNERS b/core/java/android/app/assist/OWNERS index e857c72bb28e..80ecaa41dbf3 100644 --- a/core/java/android/app/assist/OWNERS +++ b/core/java/android/app/assist/OWNERS @@ -3,3 +3,5 @@ joannechung@google.com markpun@google.com lpeter@google.com tymtsai@google.com +hackz@google.com +volnov@google.com
\ No newline at end of file diff --git a/core/java/android/app/backup/BackupManagerMonitorWrapper.java b/core/java/android/app/backup/BackupManagerMonitorWrapper.java index 0b189958ef0a..39bfb1bd7a55 100644 --- a/core/java/android/app/backup/BackupManagerMonitorWrapper.java +++ b/core/java/android/app/backup/BackupManagerMonitorWrapper.java @@ -16,9 +16,12 @@ package android.app.backup; +import android.annotation.Nullable; import android.os.Bundle; import android.os.RemoteException; +import com.android.internal.annotations.VisibleForTesting; + /** * Wrapper around {@link BackupManagerMonitor} that helps with IPC between the caller of backup * APIs and the backup service. @@ -26,16 +29,24 @@ import android.os.RemoteException; * The caller implements {@link BackupManagerMonitor} and passes it into framework APIs that run on * the caller's process. Those framework APIs will then wrap it around this class when doing the * actual IPC. + * + * @hide */ -class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub { +@VisibleForTesting +public class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub { + @Nullable private final BackupManagerMonitor mMonitor; - BackupManagerMonitorWrapper(BackupManagerMonitor monitor) { + public BackupManagerMonitorWrapper(@Nullable BackupManagerMonitor monitor) { mMonitor = monitor; } @Override public void onEvent(final Bundle event) throws RemoteException { + if (mMonitor == null) { + // It's valid for the underlying monitor to be null, so just return. + return; + } mMonitor.onEvent(event); } } diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index af5c6dd8e977..2e672251cf68 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -1331,10 +1331,12 @@ public final class CompanionDeviceManager { /** * Enable or disable secure transport for testing. Defaults to enabled. + * Should not be used outside of testing. * * @param enabled true to enable. false to disable. * @hide */ + @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) public void enableSecureTransport(boolean enabled) { try { mService.enableSecureTransport(enabled); diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index fa99b59888f5..2200af679531 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -145,7 +145,7 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall private boolean mExported; private boolean mNoPerms; private boolean mSingleUser; - private SparseBooleanArray mUsersRedirectedToOwner = new SparseBooleanArray(); + private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray(); private ThreadLocal<AttributionSource> mCallingAttributionSource; @@ -874,34 +874,43 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall return true; } - if (isAuthorityRedirectedForCloneProfile(mAuthority)) { - if (mUsersRedirectedToOwner.indexOfKey(callingUserId) >= 0) { - return mUsersRedirectedToOwner.get(callingUserId); + // Provider user-id will be determined from User Space of the calling app. + return isContentRedirectionAllowedForUser(callingUserId); + } + + /** + * Verify that content redirection is allowed or not. + * We check: + * 1. Type of Authority + * 2. UserProperties allow content sharing + * + * @param incomingUserId - Provider's user-id to be passed should be based upon: + * 1. If client is a cloned app running in user 10, it should be that (10) + * 2. If client is accessing content by hinting user space of content, + * like sysUi (residing in user 0) accessing 'content://11@media/external' + * then it should be 11. + */ + private boolean isContentRedirectionAllowedForUser(int incomingUserId) { + if (MediaStore.AUTHORITY.equals(mAuthority)) { + int incomingUserIdIndex = mUsersRedirectedToOwnerForMedia.indexOfKey(incomingUserId); + if (incomingUserIdIndex >= 0) { + return mUsersRedirectedToOwnerForMedia.valueAt(incomingUserIdIndex); } // Haven't seen this user yet, look it up - try { - UserHandle callingUser = UserHandle.getUserHandleForUid(uid); - Context callingUserContext = mContext.createPackageContextAsUser("system", - 0, callingUser); - UserManager um = callingUserContext.getSystemService(UserManager.class); - - if (um != null && um.isCloneProfile()) { - UserHandle parent = um.getProfileParent(callingUser); - - if (parent != null && parent.equals(myUserHandle())) { - mUsersRedirectedToOwner.put(callingUserId, true); - return true; - } + UserManager um = mContext.getSystemService(UserManager.class); + if (um != null && um.getUserProperties(UserHandle.of(incomingUserId)) + .isMediaSharedWithParent()) { + UserHandle parent = um.getProfileParent(UserHandle.of(incomingUserId)); + if (parent != null && parent.equals(myUserHandle())) { + mUsersRedirectedToOwnerForMedia.put(incomingUserId, true); + return true; } - } catch (PackageManager.NameNotFoundException e) { - // ignore } - mUsersRedirectedToOwner.put(callingUserId, false); + mUsersRedirectedToOwnerForMedia.put(incomingUserId, false); return false; } - return false; } @@ -2734,7 +2743,11 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall String auth = uri.getAuthority(); if (!mSingleUser) { int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT); - if (userId != UserHandle.USER_CURRENT && userId != mContext.getUserId()) { + if (userId != UserHandle.USER_CURRENT + && userId != mContext.getUserId() + // Since userId specified in content uri, the provider userId would be + // determined from it. + && !isContentRedirectionAllowedForUser(userId)) { throw new SecurityException("trying to query a ContentProvider in user " + mContext.getUserId() + " with a uri belonging to user " + userId); } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 307f30619be2..e763e951fbc1 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -6907,7 +6907,7 @@ public class Intent implements Parcelable, Cloneable { * * @hide */ - public static final int FLAG_IGNORE_EPHEMERAL = 0x00000200; + public static final int FLAG_IGNORE_EPHEMERAL = 0x80000000; /** * If set, the new activity is not kept in the history stack. As soon as diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index b5d2f2c9177a..036a4eb22ba6 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1698,27 +1698,6 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { } /** - * Returns whether the activity supports size changes. - * @hide - */ - @SizeChangesSupportMode - public int supportsSizeChanges() { - if (isChangeEnabled(FORCE_NON_RESIZE_APP)) { - return SIZE_CHANGES_UNSUPPORTED_OVERRIDE; - } - - if (supportsSizeChanges) { - return SIZE_CHANGES_SUPPORTED_METADATA; - } - - if (isChangeEnabled(FORCE_RESIZE_APP)) { - return SIZE_CHANGES_SUPPORTED_OVERRIDE; - } - - return SIZE_CHANGES_UNSUPPORTED_METADATA; - } - - /** * Returns if the activity should never be sandboxed to the activity window bounds. * @hide */ diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java index 7ac8f37c645e..c3df17d4b53e 100644 --- a/core/java/android/content/pm/CrossProfileApps.java +++ b/core/java/android/content/pm/CrossProfileApps.java @@ -27,6 +27,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UserHandleAware; import android.app.Activity; +import android.app.ActivityOptions; import android.app.AppOpsManager.Mode; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; @@ -110,8 +111,8 @@ public class CrossProfileApps { component, targetUser.getIdentifier(), true, - null, - null); + mContext.getActivityToken(), + ActivityOptions.makeBasic().toBundle()); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index afe375c4ac20..96118f6c43ea 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -616,6 +616,7 @@ public class PackageInstaller { /** {@hide} */ public PackageInstaller(IPackageInstaller installer, String installerPackageName, String installerAttributionTag, int userId) { + Objects.requireNonNull(installer, "installer cannot be null"); mInstaller = installer; mInstallerPackageName = installerPackageName; mAttributionTag = installerAttributionTag; diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 7e0954a55560..be8b2a20cfb1 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -168,7 +168,8 @@ public class ServiceInfo extends ComponentInfo * <p>Starting foreground service with this type from apps targeting API level * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission * {@link android.Manifest.permission#FOREGROUND_SERVICE_PHONE_CALL} and - * {@link android.Manifest.permission#MANAGE_OWN_CALLS}. + * {@link android.Manifest.permission#MANAGE_OWN_CALLS} or holding the default + * {@link android.app.role.RoleManager#ROLE_DIALER dialer role}. */ @RequiresPermission( allOf = { diff --git a/core/java/android/content/pm/parsing/ApkLite.java b/core/java/android/content/pm/parsing/ApkLite.java index 408f7ed9f766..269bec256282 100644 --- a/core/java/android/content/pm/parsing/ApkLite.java +++ b/core/java/android/content/pm/parsing/ApkLite.java @@ -138,11 +138,6 @@ public class ApkLite { */ private final boolean mIsSdkLibrary; - /** - * Indicates if this package allows an installer to declare update ownership of it. - */ - private final boolean mAllowUpdateOwnership; - public ApkLite(String path, String packageName, String splitName, boolean isFeatureSplit, String configForSplit, String usesSplitName, boolean isSplitRequired, int versionCode, int versionCodeMajor, int revisionCode, int installLocation, @@ -153,7 +148,7 @@ public class ApkLite { String requiredSystemPropertyName, String requiredSystemPropertyValue, int minSdkVersion, int targetSdkVersion, int rollbackDataPolicy, Set<String> requiredSplitTypes, Set<String> splitTypes, - boolean hasDeviceAdminReceiver, boolean isSdkLibrary, boolean allowUpdateOwnership) { + boolean hasDeviceAdminReceiver, boolean isSdkLibrary) { mPath = path; mPackageName = packageName; mSplitName = splitName; @@ -187,7 +182,6 @@ public class ApkLite { mRollbackDataPolicy = rollbackDataPolicy; mHasDeviceAdminReceiver = hasDeviceAdminReceiver; mIsSdkLibrary = isSdkLibrary; - mAllowUpdateOwnership = allowUpdateOwnership; } /** @@ -480,9 +474,6 @@ public class ApkLite { return mRollbackDataPolicy; } - /** - * Indicates if this app contains a {@link android.app.admin.DeviceAdminReceiver}. - */ @DataClass.Generated.Member public boolean isHasDeviceAdminReceiver() { return mHasDeviceAdminReceiver; @@ -496,19 +487,11 @@ public class ApkLite { return mIsSdkLibrary; } - /** - * Indicates if this package allows an installer to declare update ownership of it. - */ - @DataClass.Generated.Member - public boolean isAllowUpdateOwnership() { - return mAllowUpdateOwnership; - } - @DataClass.Generated( - time = 1680122754650L, + time = 1643063342990L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/parsing/ApkLite.java", - inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\nprivate final boolean mAllowUpdateOwnership\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)") + inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)") @Deprecated private void __metadata() {} diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index d209b35ac810..4f6bcb6f0be5 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -127,8 +127,7 @@ public class ApkLiteParseUtils { null /* isFeatureSplits */, null /* usesSplitNames */, null /* configForSplit */, null /* splitApkPaths */, null /* splitRevisionCodes */, baseApk.getTargetSdkVersion(), - null /* requiredSplitTypes */, null, /* splitTypes */ - baseApk.isAllowUpdateOwnership())); + null /* requiredSplitTypes */, null /* splitTypes */)); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } @@ -154,8 +153,7 @@ public class ApkLiteParseUtils { null /* isFeatureSplits */, null /* usesSplitNames */, null /* configForSplit */, null /* splitApkPaths */, null /* splitRevisionCodes */, baseApk.getTargetSdkVersion(), - null /* requiredSplitTypes */, null, /* splitTypes */ - baseApk.isAllowUpdateOwnership())); + null /* requiredSplitTypes */, null /* splitTypes */)); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } @@ -187,37 +185,41 @@ public class ApkLiteParseUtils { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parseApkLite"); try { for (File file : files) { - if (isApkFile(file)) { - final ParseResult<ApkLite> result = parseApkLite(input, file, flags); - if (result.isError()) { - return input.error(result); - } + if (!isApkFile(file)) { + continue; + } - final ApkLite lite = result.getResult(); - // Assert that all package names and version codes are - // consistent with the first one we encounter. - if (packageName == null) { - packageName = lite.getPackageName(); - versionCode = lite.getVersionCode(); - } else { - if (!packageName.equals(lite.getPackageName())) { - return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST, - "Inconsistent package " + lite.getPackageName() + " in " + file - + "; expected " + packageName); - } - if (versionCode != lite.getVersionCode()) { - return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST, - "Inconsistent version " + lite.getVersionCode() + " in " + file - + "; expected " + versionCode); - } - } + final ParseResult<ApkLite> result = parseApkLite(input, file, flags); + if (result.isError()) { + return input.error(result); + } - // Assert that each split is defined only oncuses-static-libe - if (apks.put(lite.getSplitName(), lite) != null) { + final ApkLite lite = result.getResult(); + // Assert that all package names and version codes are + // consistent with the first one we encounter. + if (packageName == null) { + packageName = lite.getPackageName(); + versionCode = lite.getVersionCode(); + } else { + if (!packageName.equals(lite.getPackageName())) { return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST, - "Split name " + lite.getSplitName() - + " defined more than once; most recent was " + file); + "Inconsistent package " + lite.getPackageName() + " in " + file + + "; expected " + packageName); } + if (versionCode != lite.getVersionCode()) { + return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Inconsistent version " + lite.getVersionCode() + " in " + file + + "; expected " + versionCode); + } + } + + // Assert that each split is defined only once + ApkLite prev = apks.put(lite.getSplitName(), lite); + if (prev != null) { + return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Split name " + lite.getSplitName() + + " defined more than once; most recent was " + file + + ", previous was " + prev.getPath()); } } baseApk = apks.remove(null); @@ -301,8 +303,7 @@ public class ApkLiteParseUtils { return input.success( new PackageLite(codePath, baseCodePath, baseApk, splitNames, isFeatureSplits, usesSplitNames, configForSplits, splitCodePaths, splitRevisionCodes, - baseApk.getTargetSdkVersion(), requiredSplitTypes, splitTypes, - baseApk.isAllowUpdateOwnership())); + baseApk.getTargetSdkVersion(), requiredSplitTypes, splitTypes)); } /** @@ -429,8 +430,6 @@ public class ApkLiteParseUtils { "isFeatureSplit", false); boolean isSplitRequired = parser.getAttributeBooleanValue(ANDROID_RES_NAMESPACE, "isSplitRequired", false); - boolean allowUpdateOwnership = parser.getAttributeBooleanValue(ANDROID_RES_NAMESPACE, - "allowUpdateOwnership", true); String configForSplit = parser.getAttributeValue(null, "configForSplit"); int targetSdkVersion = DEFAULT_TARGET_SDK_VERSION; @@ -614,7 +613,7 @@ public class ApkLiteParseUtils { overlayIsStatic, overlayPriority, requiredSystemPropertyName, requiredSystemPropertyValue, minSdkVersion, targetSdkVersion, rollbackDataPolicy, requiredSplitTypes.first, requiredSplitTypes.second, - hasDeviceAdminReceiver, isSdkLibrary, allowUpdateOwnership)); + hasDeviceAdminReceiver, isSdkLibrary)); } private static boolean isDeviceAdminReceiver( diff --git a/core/java/android/content/pm/parsing/PackageLite.java b/core/java/android/content/pm/parsing/PackageLite.java index e24b9320110e..e2789c93516f 100644 --- a/core/java/android/content/pm/parsing/PackageLite.java +++ b/core/java/android/content/pm/parsing/PackageLite.java @@ -110,16 +110,10 @@ public class PackageLite { */ private final boolean mIsSdkLibrary; - /** - * Indicates if this package allows an installer to declare update ownership of it. - */ - private final boolean mAllowUpdateOwnership; - public PackageLite(String path, String baseApkPath, ApkLite baseApk, String[] splitNames, boolean[] isFeatureSplits, String[] usesSplitNames, String[] configForSplit, String[] splitApkPaths, int[] splitRevisionCodes, - int targetSdk, Set<String>[] requiredSplitTypes, Set<String>[] splitTypes, - boolean allowUpdateOwnership) { + int targetSdk, Set<String>[] requiredSplitTypes, Set<String>[] splitTypes) { // The following paths may be different from the path in ApkLite because we // move or rename the APK files. Use parameters to indicate the correct paths. mPath = path; @@ -150,7 +144,6 @@ public class PackageLite { mSplitApkPaths = splitApkPaths; mSplitRevisionCodes = splitRevisionCodes; mTargetSdk = targetSdk; - mAllowUpdateOwnership = allowUpdateOwnership; } /** @@ -421,19 +414,12 @@ public class PackageLite { return mIsSdkLibrary; } - /** - * Indicates if this package allows an installer to declare update ownership of it. - */ - @DataClass.Generated.Member - public boolean isAllowUpdateOwnership() { - return mAllowUpdateOwnership; - } - @DataClass.Generated( - time = 1680125514341L, + time = 1643132127068L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/parsing/PackageLite.java", - inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mTargetSdk\nprivate final int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mProfileableByShell\nprivate final boolean mUseEmbeddedDex\nprivate final boolean mIsSdkLibrary\nprivate final boolean mAllowUpdateOwnership\npublic java.util.List<java.lang.String> getAllApkPaths()\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)") + inputSignatures = + "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mTargetSdk\nprivate final int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mProfileableByShell\nprivate final boolean mUseEmbeddedDex\nprivate final boolean mIsSdkLibrary\npublic java.util.List<java.lang.String> getAllApkPaths()\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)") @Deprecated private void __metadata() {} diff --git a/core/java/android/hardware/CameraSessionStats.java b/core/java/android/hardware/CameraSessionStats.java index d59295ebe940..1c2cbbc81971 100644 --- a/core/java/android/hardware/CameraSessionStats.java +++ b/core/java/android/hardware/CameraSessionStats.java @@ -65,6 +65,7 @@ public class CameraSessionStats implements Parcelable { private String mUserTag; private int mVideoStabilizationMode; private int mSessionIndex; + private CameraExtensionSessionStats mCameraExtensionSessionStats; public CameraSessionStats() { mFacing = -1; @@ -82,6 +83,7 @@ public class CameraSessionStats implements Parcelable { mStreamStats = new ArrayList<CameraStreamStats>(); mVideoStabilizationMode = -1; mSessionIndex = 0; + mCameraExtensionSessionStats = new CameraExtensionSessionStats(); } public CameraSessionStats(String cameraId, int facing, int newCameraState, @@ -101,6 +103,7 @@ public class CameraSessionStats implements Parcelable { mInternalReconfigure = internalReconfigure; mStreamStats = new ArrayList<CameraStreamStats>(); mSessionIndex = sessionIdx; + mCameraExtensionSessionStats = new CameraExtensionSessionStats(); } public static final @android.annotation.NonNull Parcelable.Creator<CameraSessionStats> CREATOR = @@ -145,6 +148,7 @@ public class CameraSessionStats implements Parcelable { dest.writeString(mUserTag); dest.writeInt(mVideoStabilizationMode); dest.writeInt(mSessionIndex); + mCameraExtensionSessionStats.writeToParcel(dest, 0); } public void readFromParcel(Parcel in) { @@ -170,6 +174,7 @@ public class CameraSessionStats implements Parcelable { mUserTag = in.readString(); mVideoStabilizationMode = in.readInt(); mSessionIndex = in.readInt(); + mCameraExtensionSessionStats = CameraExtensionSessionStats.CREATOR.createFromParcel(in); } public String getCameraId() { @@ -243,4 +248,8 @@ public class CameraSessionStats implements Parcelable { public int getSessionIndex() { return mSessionIndex; } + + public CameraExtensionSessionStats getExtensionSessionStats() { + return mCameraExtensionSessionStats; + } } diff --git a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java index 257ad7162e9e..5b24fb6860a2 100644 --- a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java +++ b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java @@ -218,7 +218,8 @@ public interface BiometricFingerprintConstants { FINGERPRINT_ACQUIRED_UNKNOWN, FINGERPRINT_ACQUIRED_IMMOBILE, FINGERPRINT_ACQUIRED_TOO_BRIGHT, - FINGERPRINT_ACQUIRED_POWER_PRESSED}) + FINGERPRINT_ACQUIRED_POWER_PRESSED, + FINGERPRINT_ACQUIRED_RE_ENROLL}) @Retention(RetentionPolicy.SOURCE) @interface FingerprintAcquired {} @@ -310,6 +311,12 @@ public interface BiometricFingerprintConstants { int FINGERPRINT_ACQUIRED_POWER_PRESSED = 11; /** + * This message is sent to encourage the user to re-enroll their fingerprints. + * @hide + */ + int FINGERPRINT_ACQUIRED_RE_ENROLL = 12; + + /** * @hide */ int FINGERPRINT_ACQUIRED_VENDOR_BASE = 1000; diff --git a/core/java/android/hardware/biometrics/ComponentInfoInternal.java b/core/java/android/hardware/biometrics/ComponentInfoInternal.java index 2e708de21762..3b61a56bd9f1 100644 --- a/core/java/android/hardware/biometrics/ComponentInfoInternal.java +++ b/core/java/android/hardware/biometrics/ComponentInfoInternal.java @@ -19,8 +19,6 @@ package android.hardware.biometrics; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; -import android.util.IndentingPrintWriter; /** * The internal class for storing the component info for a subsystem of the biometric sensor, @@ -92,19 +90,12 @@ public class ComponentInfoInternal implements Parcelable { dest.writeString(softwareVersion); } - /** - * Print the component info into the given stream. - * - * @param pw The stream to dump the info into. - * @hide - */ - public void dump(@NonNull IndentingPrintWriter pw) { - pw.println(TextUtils.formatSimple("componentId: %s", componentId)); - pw.increaseIndent(); - pw.println(TextUtils.formatSimple("hardwareVersion: %s", hardwareVersion)); - pw.println(TextUtils.formatSimple("firmwareVersion: %s", firmwareVersion)); - pw.println(TextUtils.formatSimple("serialNumber: %s", serialNumber)); - pw.println(TextUtils.formatSimple("softwareVersion: %s", softwareVersion)); - pw.decreaseIndent(); + @Override + public String toString() { + return "ComponentId: " + componentId + + ", HardwareVersion: " + hardwareVersion + + ", FirmwareVersion: " + firmwareVersion + + ", SerialNumber " + serialNumber + + ", SoftwareVersion: " + softwareVersion; } } diff --git a/core/java/android/hardware/biometrics/IBiometricService.aidl b/core/java/android/hardware/biometrics/IBiometricService.aidl index 1286046e6a01..18c8d1bd3a1e 100644 --- a/core/java/android/hardware/biometrics/IBiometricService.aidl +++ b/core/java/android/hardware/biometrics/IBiometricService.aidl @@ -58,10 +58,10 @@ interface IBiometricService { boolean hasEnrolledBiometrics(int userId, String opPackageName); // Registers an authenticator (e.g. face, fingerprint, iris). - // Sensor Id in sensor props must be unique, whereas modality doesn't need to be. + // Id must be unique, whereas strength and modality don't need to be. // TODO(b/123321528): Turn strength and modality into enums. @EnforcePermission("USE_BIOMETRIC_INTERNAL") - void registerAuthenticator(int modality, in SensorPropertiesInternal props, + void registerAuthenticator(int id, int modality, int strength, IBiometricAuthenticator authenticator); // Register callback for when keyguard biometric eligibility changes. diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index e908ced06acd..48b5cac2a519 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -212,14 +212,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri @GuardedBy("mLock") private boolean mFoldedDeviceState; - private final CameraManager.DeviceStateListener mFoldStateListener = - new CameraManager.DeviceStateListener() { - @Override - public final void onDeviceStateChanged(boolean folded) { - synchronized (mLock) { - mFoldedDeviceState = folded; - } - }}; + private CameraManager.DeviceStateListener mFoldStateListener; private static final String TAG = "CameraCharacteristics"; @@ -245,7 +238,18 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri /** * Return the device state listener for this Camera characteristics instance */ - CameraManager.DeviceStateListener getDeviceStateListener() { return mFoldStateListener; } + CameraManager.DeviceStateListener getDeviceStateListener() { + if (mFoldStateListener == null) { + mFoldStateListener = new CameraManager.DeviceStateListener() { + @Override + public final void onDeviceStateChanged(boolean folded) { + synchronized (mLock) { + mFoldedDeviceState = folded; + } + }}; + } + return mFoldStateListener; + } /** * Overrides the property value diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index c6fc69efb85c..85f8ca66715b 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -30,6 +30,7 @@ import android.compat.annotation.Overridable; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Point; +import android.hardware.CameraExtensionSessionStats; import android.hardware.CameraStatus; import android.hardware.ICameraService; import android.hardware.ICameraServiceListener; @@ -1727,6 +1728,30 @@ public final class CameraManager { } /** + * Reports {@link CameraExtensionSessionStats} to the {@link ICameraService} to be logged for + * currently active session. Validation is done downstream. + * + * @param extStats Extension Session stats to be logged by cameraservice + * + * @return the key to be used with the next call. + * See {@link ICameraService#reportExtensionSessionStats}. + * @hide + */ + public static String reportExtensionSessionStats(CameraExtensionSessionStats extStats) { + ICameraService cameraService = CameraManagerGlobal.get().getCameraService(); + if (cameraService == null) { + Log.e(TAG, "CameraService not available. Not reporting extension stats."); + return ""; + } + try { + return cameraService.reportExtensionSessionStats(extStats); + } catch (RemoteException e) { + Log.e(TAG, "Failed to report extension session stats to cameraservice.", e); + } + return ""; + } + + /** * A per-process global camera manager instance, to retain a connection to the camera service, * and to distribute camera availability notices to API-registered callbacks */ @@ -1811,6 +1836,7 @@ public final class CameraManager { ctx.getSystemService(DeviceStateManager.class).registerCallback( new HandlerExecutor(mDeviceStateHandler), mFoldStateListener); } catch (IllegalStateException e) { + mFoldStateListener = null; Log.v(TAG, "Failed to register device state listener!"); Log.v(TAG, "Device state dependent characteristics updates will not be" + "functional!"); diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java index e787779d0f57..d87226c2b895 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -53,6 +53,7 @@ import android.hardware.camera2.params.DynamicRangeProfiles; import android.hardware.camera2.params.ExtensionSessionConfiguration; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; +import android.hardware.camera2.utils.ExtensionSessionStatsAggregator; import android.hardware.camera2.utils.SurfaceUtils; import android.media.Image; import android.media.ImageReader; @@ -96,6 +97,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes private CameraCaptureSession mCaptureSession = null; private ISessionProcessorImpl mSessionProcessor = null; private final InitializeSessionHandler mInitializeHandler; + private final ExtensionSessionStatsAggregator mStatsAggregator; private boolean mInitialized; @@ -205,6 +207,10 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface, burstCaptureSurface, postviewSurface, config.getStateCallback(), config.getExecutor(), sessionId); + + ret.mStatsAggregator.setClientName(ctx.getOpPackageName()); + ret.mStatsAggregator.setExtensionType(config.getExtension()); + ret.initialize(); return ret; @@ -234,6 +240,9 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes mInitializeHandler = new InitializeSessionHandler(); mSessionId = sessionId; mInterfaceLock = cameraDevice.mInterfaceLock; + + mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(), + /*isAdvanced=*/true); } /** @@ -523,11 +532,26 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes Log.e(TAG, "Failed to stop the repeating request or end the session," + " , extension service does not respond!") ; } + // Commit stats before closing the capture session + mStatsAggregator.commit(/*isFinal*/true); mCaptureSession.close(); } } } + /** + * Called by {@link CameraDeviceImpl} right before the capture session is closed, and before it + * calls {@link #release} + */ + public void commitStats() { + synchronized (mInterfaceLock) { + if (mInitialized) { + // Only commit stats if a capture session was initialized + mStatsAggregator.commit(/*isFinal*/true); + } + } + } + public void release(boolean skipCloseNotification) { boolean notifyClose = false; @@ -608,6 +632,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes public void onConfigured(@NonNull CameraCaptureSession session) { synchronized (mInterfaceLock) { mCaptureSession = session; + // Commit basic stats as soon as the capture session is created + mStatsAggregator.commit(/*isFinal*/false); } try { diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index b8e451c2f816..e23bbc6c347d 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -700,6 +700,14 @@ public class CameraDeviceImpl extends CameraDevice + " input configuration yet."); } + if (mCurrentExtensionSession != null) { + mCurrentExtensionSession.commitStats(); + } + + if (mCurrentAdvancedExtensionSession != null) { + mCurrentAdvancedExtensionSession.commitStats(); + } + // Notify current session that it's going away, before starting camera operations // After this call completes, the session is not allowed to call into CameraDeviceImpl if (mCurrentSession != null) { @@ -1414,6 +1422,15 @@ public class CameraDeviceImpl extends CameraDevice mOfflineSwitchService = null; } + // Let extension sessions commit stats before disconnecting remoteDevice + if (mCurrentExtensionSession != null) { + mCurrentExtensionSession.commitStats(); + } + + if (mCurrentAdvancedExtensionSession != null) { + mCurrentAdvancedExtensionSession.commitStats(); + } + if (mRemoteDevice != null) { mRemoteDevice.disconnect(); mRemoteDevice.unlinkToDeath(this, /*flags*/0); diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index 8e7c7e0cfca8..9ebef0b59ece 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -30,7 +30,6 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraExtensionCharacteristics; import android.hardware.camera2.CameraExtensionSession; -import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; @@ -49,6 +48,7 @@ import android.hardware.camera2.params.DynamicRangeProfiles; import android.hardware.camera2.params.ExtensionSessionConfiguration; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; +import android.hardware.camera2.utils.ExtensionSessionStatsAggregator; import android.hardware.camera2.utils.SurfaceUtils; import android.media.Image; import android.media.ImageReader; @@ -90,6 +90,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { private final int mSessionId; private final Set<CaptureRequest.Key> mSupportedRequestKeys; private final Set<CaptureResult.Key> mSupportedResultKeys; + private final ExtensionSessionStatsAggregator mStatsAggregator; private boolean mCaptureResultsSupported; private CameraCaptureSession mCaptureSession = null; @@ -242,6 +243,9 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { extensionChars.getAvailableCaptureRequestKeys(config.getExtension()), extensionChars.getAvailableCaptureResultKeys(config.getExtension())); + session.mStatsAggregator.setClientName(ctx.getOpPackageName()); + session.mStatsAggregator.setExtensionType(config.getExtension()); + session.initialize(); return session; @@ -280,6 +284,9 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { mSupportedResultKeys = resultKeys; mCaptureResultsSupported = !resultKeys.isEmpty(); mInterfaceLock = cameraDevice.mInterfaceLock; + + mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(), + /*isAdvanced=*/false); } private void initializeRepeatingRequestPipeline() throws RemoteException { @@ -793,11 +800,27 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { new CloseRequestHandler(mRepeatingRequestImageCallback), mHandler); } + mStatsAggregator.commit(/*isFinal*/true); // Commit stats before closing session mCaptureSession.close(); } } } + /** + * Called by {@link CameraDeviceImpl} right before the capture session is closed, and before it + * calls {@link #release} + * + * @hide + */ + public void commitStats() { + synchronized (mInterfaceLock) { + if (mInitialized) { + // Only commit stats if a capture session was initialized + mStatsAggregator.commit(/*isFinal*/true); + } + } + } + private void setInitialCaptureRequest(List<CaptureStageImpl> captureStageList, InitialRequestHandler requestHandler) throws CameraAccessException { @@ -828,7 +851,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { synchronized (mInterfaceLock) { mInternalRepeatingRequestEnabled = false; - mHandlerThread.quitSafely(); + mHandlerThread.quit(); try { mPreviewExtender.onDeInit(); @@ -955,6 +978,8 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { public void onConfigured(@NonNull CameraCaptureSession session) { synchronized (mInterfaceLock) { mCaptureSession = session; + // Commit basic stats as soon as the capture session is created + mStatsAggregator.commit(/*isFinal*/false); try { finishPipelineInitialization(); CameraExtensionCharacteristics.initializeSession(mInitializeHandler); @@ -1368,88 +1393,98 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { @Override public void onImageAvailable(ImageReader reader) { Image img; - try { - img = reader.acquireNextImage(); - } catch (IllegalStateException e) { - Log.e(TAG, "Failed to acquire image, too many images pending!"); - mOutOfBuffers = true; - return; - } - if (img == null) { - Log.e(TAG, "Invalid image!"); - return; - } + synchronized (mInterfaceLock) { + try { + img = reader.acquireNextImage(); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to acquire image, too many images pending!"); + mOutOfBuffers = true; + return; + } + if (img == null) { + Log.e(TAG, "Invalid image!"); + return; + } - Long timestamp = img.getTimestamp(); - if (mImageListenerMap.containsKey(timestamp)) { - Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.remove(timestamp); - if (entry.second != null) { - entry.second.onImageAvailable(reader, img); + Long timestamp = img.getTimestamp(); + if (mImageListenerMap.containsKey(timestamp)) { + Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.remove( + timestamp); + if (entry.second != null) { + entry.second.onImageAvailable(reader, img); + } else { + Log.w(TAG, "Invalid image listener, dropping frame!"); + img.close(); + } } else { - Log.w(TAG, "Invalid image listener, dropping frame!"); - img.close(); + mImageListenerMap.put(timestamp, new Pair<>(img, null)); } - } else { - mImageListenerMap.put(img.getTimestamp(), new Pair<>(img, null)); - } - notifyDroppedImages(timestamp); + notifyDroppedImages(timestamp); + } } private void notifyDroppedImages(long timestamp) { - Set<Long> timestamps = mImageListenerMap.keySet(); - ArrayList<Long> removedTs = new ArrayList<>(); - for (long ts : timestamps) { - if (ts < timestamp) { - Log.e(TAG, "Dropped image with ts: " + ts); - Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.get(ts); - if (entry.second != null) { - entry.second.onImageDropped(ts); - } - if (entry.first != null) { - entry.first.close(); + synchronized (mInterfaceLock) { + Set<Long> timestamps = mImageListenerMap.keySet(); + ArrayList<Long> removedTs = new ArrayList<>(); + for (long ts : timestamps) { + if (ts < timestamp) { + Log.e(TAG, "Dropped image with ts: " + ts); + Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.get(ts); + if (entry.second != null) { + entry.second.onImageDropped(ts); + } + if (entry.first != null) { + entry.first.close(); + } + removedTs.add(ts); } - removedTs.add(ts); } - } - for (long ts : removedTs) { - mImageListenerMap.remove(ts); + for (long ts : removedTs) { + mImageListenerMap.remove(ts); + } } } public void registerListener(Long timestamp, OnImageAvailableListener listener) { - if (mImageListenerMap.containsKey(timestamp)) { - Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.remove(timestamp); - if (entry.first != null) { - listener.onImageAvailable(mImageReader, entry.first); - if (mOutOfBuffers) { - mOutOfBuffers = false; - Log.w(TAG,"Out of buffers, retry!"); - onImageAvailable(mImageReader); + synchronized (mInterfaceLock) { + if (mImageListenerMap.containsKey(timestamp)) { + Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.remove( + timestamp); + if (entry.first != null) { + listener.onImageAvailable(mImageReader, entry.first); + if (mOutOfBuffers) { + mOutOfBuffers = false; + Log.w(TAG,"Out of buffers, retry!"); + onImageAvailable(mImageReader); + } + } else { + Log.w(TAG, "No valid image for listener with ts: " + + timestamp.longValue()); } } else { - Log.w(TAG, "No valid image for listener with ts: " + - timestamp.longValue()); + mImageListenerMap.put(timestamp, new Pair<>(null, listener)); } - } else { - mImageListenerMap.put(timestamp, new Pair<>(null, listener)); } } @Override public void close() { - for (Pair<Image, OnImageAvailableListener> entry : mImageListenerMap.values()) { - if (entry.first != null) { - entry.first.close(); + synchronized (mInterfaceLock) { + for (Pair<Image, OnImageAvailableListener> entry : mImageListenerMap.values()) { + if (entry.first != null) { + entry.first.close(); + } } - } - for (long timestamp : mImageListenerMap.keySet()) { - Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.get(timestamp); - if (entry.second != null) { - entry.second.onImageDropped(timestamp); + for (long timestamp : mImageListenerMap.keySet()) { + Pair<Image, OnImageAvailableListener> entry = mImageListenerMap.get(timestamp); + if (entry.second != null) { + entry.second.onImageDropped(timestamp); + } } + mImageListenerMap.clear(); } - mImageListenerMap.clear(); } } diff --git a/core/java/android/hardware/camera2/utils/ExtensionSessionStatsAggregator.java b/core/java/android/hardware/camera2/utils/ExtensionSessionStatsAggregator.java new file mode 100644 index 000000000000..8cd5e83241ad --- /dev/null +++ b/core/java/android/hardware/camera2/utils/ExtensionSessionStatsAggregator.java @@ -0,0 +1,121 @@ +/* + * 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.hardware.camera2.utils; + +import android.annotation.NonNull; +import android.hardware.CameraExtensionSessionStats; +import android.hardware.camera2.CameraManager; +import android.util.Log; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Utility class to aggregate metrics specific to Camera Extensions and pass them to + * {@link CameraManager}. {@link android.hardware.camera2.CameraExtensionSession} should call + * {@link #commit} before closing the session. + * + * @hide + */ +public class ExtensionSessionStatsAggregator { + private static final boolean DEBUG = false; + private static final String TAG = ExtensionSessionStatsAggregator.class.getSimpleName(); + + private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + + private final Object mLock = new Object(); // synchronizes access to all fields of the class + private boolean mIsDone = false; // marks the aggregator as "done". + // Mutations and commits become no-op if this is true. + private final CameraExtensionSessionStats mStats; + + public ExtensionSessionStatsAggregator(@NonNull String cameraId, boolean isAdvanced) { + if (DEBUG) { + Log.v(TAG, "Creating new Extension Session Stats Aggregator"); + } + mStats = new CameraExtensionSessionStats(); + mStats.key = ""; + mStats.cameraId = cameraId; + mStats.isAdvanced = isAdvanced; + } + + /** + * Set client package name + * + * @param clientName package name of the client that these stats are associated with. + */ + public void setClientName(@NonNull String clientName) { + synchronized (mLock) { + if (mIsDone) { + return; + } + if (DEBUG) { + Log.v(TAG, "Setting clientName: " + clientName); + } + mStats.clientName = clientName; + } + } + + /** + * Set extension type. + * + * @param extensionType Type of extension. Must match one of + * {@code CameraExtensionCharacteristics#EXTENSION_*} + */ + public void setExtensionType(int extensionType) { + synchronized (mLock) { + if (mIsDone) { + return; + } + if (DEBUG) { + Log.v(TAG, "Setting type: " + extensionType); + } + mStats.type = extensionType; + } + } + + /** + * Asynchronously commits the stats to CameraManager on a background thread. + * + * @param isFinal marks the stats as final and prevents any further commits or changes. This + * should be set to true when the stats are considered final for logging, + * for example right before the capture session is about to close + */ + public void commit(boolean isFinal) { + // Call binder on a background thread to reduce latencies from metrics logging. + mExecutor.execute(() -> { + synchronized (mLock) { + if (mIsDone) { + return; + } + mIsDone = isFinal; + if (DEBUG) { + Log.v(TAG, "Committing: " + prettyPrintStats(mStats)); + } + mStats.key = CameraManager.reportExtensionSessionStats(mStats); + } + }); + } + + private static String prettyPrintStats(@NonNull CameraExtensionSessionStats stats) { + return CameraExtensionSessionStats.class.getSimpleName() + ":\n" + + " key: '" + stats.key + "'\n" + + " cameraId: '" + stats.cameraId + "'\n" + + " clientName: '" + stats.clientName + "'\n" + + " type: '" + stats.type + "'\n" + + " isAdvanced: '" + stats.isAdvanced + "'\n"; + } +} diff --git a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl index 099316099738..3e72d81d88cf 100644 --- a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl +++ b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl @@ -21,7 +21,14 @@ import android.hardware.devicestate.IDeviceStateManagerCallback; /** @hide */ interface IDeviceStateManager { - /** Returns the current device state info. */ + /** + * Returns the current device state info. This {@link DeviceStateInfo} object will always + * include the list of supported states. If there has been no base state or committed state + * provided yet to the system server, this {@link DeviceStateInfo} object will include + * {@link DeviceStateManager#INVALID_DEVICE_STATE} for each respectively. + * + * This method should not be used to notify callback clients. + */ DeviceStateInfo getDeviceStateInfo(); /** diff --git a/core/java/android/hardware/display/BrightnessChangeEvent.java b/core/java/android/hardware/display/BrightnessChangeEvent.java index 6b7d8c3dde49..7ae88b80d02a 100644 --- a/core/java/android/hardware/display/BrightnessChangeEvent.java +++ b/core/java/android/hardware/display/BrightnessChangeEvent.java @@ -22,6 +22,7 @@ import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; +import java.util.Arrays; import java.util.Objects; /** @@ -233,6 +234,31 @@ public final class BrightnessChangeEvent implements Parcelable { dest.writeLong(colorSampleDuration); } + @Override + public String toString() { + return "BrightnessChangeEvent{" + + "brightness: " + brightness + + ", timeStamp: " + timeStamp + + ", packageName: " + packageName + + ", userId: " + userId + + ", uniqueDisplayId: " + uniqueDisplayId + + ", luxValues: " + Arrays.toString(luxValues) + + ", luxTimestamps: " + Arrays.toString(luxTimestamps) + + ", batteryLevel: " + batteryLevel + + ", powerBrightnessFactor: " + powerBrightnessFactor + + ", nightMode: " + nightMode + + ", colorTemperature: " + colorTemperature + + ", reduceBrightColors: " + reduceBrightColors + + ", reduceBrightColorsStrength: " + reduceBrightColorsStrength + + ", reduceBrightColorsOffset: " + reduceBrightColorsOffset + + ", lastBrightness: " + lastBrightness + + ", isDefaultBrightnessConfig: " + isDefaultBrightnessConfig + + ", isUserSetBrightness: " + isUserSetBrightness + + ", colorValueBuckets: " + Arrays.toString(colorValueBuckets) + + ", colorSampleDuration: " + colorSampleDuration + + "}"; + } + /** @hide */ public static class Builder { private float mBrightness; diff --git a/core/java/android/os/BatteryStatsManager.java b/core/java/android/os/BatteryStatsManager.java index 071bdea5e3ac..955fad3d1a48 100644 --- a/core/java/android/os/BatteryStatsManager.java +++ b/core/java/android/os/BatteryStatsManager.java @@ -520,14 +520,10 @@ public final class BatteryStatsManager { * @param uid calling package uid * @param reason why Bluetooth has been turned on * @param packageName package responsible for this change + * @Deprecated Bluetooth self report its state and no longer call this */ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void reportBluetoothOn(int uid, int reason, @NonNull String packageName) { - try { - mBatteryStats.noteBluetoothOn(uid, reason, packageName); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } } /** @@ -536,14 +532,10 @@ public final class BatteryStatsManager { * @param uid calling package uid * @param reason why Bluetooth has been turned on * @param packageName package responsible for this change + * @Deprecated Bluetooth self report its state and no longer call this */ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void reportBluetoothOff(int uid, int reason, @NonNull String packageName) { - try { - mBatteryStats.noteBluetoothOff(uid, reason, packageName); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } } /** diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 8606687d8c68..7d68b44581de 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -250,6 +250,10 @@ public class UserManager { * use {@link android.accounts.AccountManager} APIs to add or remove accounts when account * management is disallowed. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -267,6 +271,9 @@ public class UserManager { * primary user or by a profile owner of an organization-owned managed profile on the parent * profile, it disallows the primary user from changing Wi-Fi access points. * + * <p>Holders of the permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_WIFI} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -285,6 +292,9 @@ public class UserManager { * When it is set by any of these owners, it applies globally - i.e., it disables airplane mode * from changing Wi-Fi state. * + * <p>Holders of the permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_WIFI} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -306,6 +316,9 @@ public class UserManager { * When it is set by any of these owners, it prevents all users from using * Wi-Fi tethering. Other forms of tethering are not affected. * + * <p>Holders of the permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_WIFI} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * This user restriction disables only Wi-Fi tethering. * Use {@link #DISALLOW_CONFIG_TETHERING} to limit all forms of tethering. * When {@link #DISALLOW_CONFIG_TETHERING} is set, this user restriction becomes obsolete. @@ -346,6 +359,9 @@ public class UserManager { * sharing Wi-Fi for networks configured by these owners. * Other networks not configured by these owners are not affected. * + * <p>Holders of the permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_WIFI} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -365,6 +381,9 @@ public class UserManager { * When it is set by any of these owners, it prevents all users from using * Wi-Fi Direct. * + * <p>Holders of the permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_WIFI} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -384,6 +403,9 @@ public class UserManager { * a new Wi-Fi configuration. This does not limit the owner and carrier's ability * to add a new configuration. * + * <p>Holders of the permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_WIFI} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -398,6 +420,9 @@ public class UserManager { * Specifies if a user is disallowed from changing the device * language. The default value is <code>false</code>. * + * <p>Holders of the permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCALE} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -411,6 +436,10 @@ public class UserManager { * prevents device owners and profile owners installing apps. The default value is * {@code false}. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APPS_CONTROL} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -423,6 +452,10 @@ public class UserManager { * Specifies if a user is disallowed from uninstalling applications. * The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APPS_CONTROL} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -442,6 +475,10 @@ public class UserManager { * managed profile on the parent profile, it prevents the primary user from turning on * location sharing. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCATION} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -460,6 +497,10 @@ public class UserManager { * When it is set by any of these owners, it applies globally - i.e., it disables airplane mode * on the entire device. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_AIRPLANE_MODE} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -476,6 +517,10 @@ public class UserManager { * * <p>The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_DISPLAY} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>This user restriction has no effect on managed profiles. * <p>Key for user restrictions. * <p>Type: Boolean @@ -490,6 +535,10 @@ public class UserManager { * * <p>The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_DISPLAY} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>This user restriction has no effect on managed profiles. * <p>Key for user restrictions. * <p>Type: Boolean @@ -504,6 +553,10 @@ public class UserManager { * * <p>The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_DISPLAY} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>This user restriction has no effect on managed profiles. * <p>Key for user restrictions. * <p>Type: Boolean @@ -519,6 +572,10 @@ public class UserManager { * Unknown sources exclude adb and special apps such as trusted app stores. * The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -536,6 +593,10 @@ public class UserManager { * This restriction can be enabled by the profile owner, in which case all accounts and * profiles will be affected. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -559,6 +620,10 @@ public class UserManager { * primary user or by a profile owner of an organization-owned managed profile on the parent * profile, it disallows the primary user from configuring bluetooth. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_BLUETOOTH} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -581,6 +646,10 @@ public class UserManager { * profile, it disables the primary user from using bluetooth and configuring bluetooth * in Settings. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_BLUETOOTH} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -599,6 +668,10 @@ public class UserManager { * profile owner of an organization-owned managed profile on the parent profile, it disables * the primary user from any outgoing bluetooth sharing. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_BLUETOOTH} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Default is <code>true</code> for managed profiles and false otherwise. * * <p>When a device upgrades to {@link android.os.Build.VERSION_CODES#O}, the system sets it @@ -626,6 +699,10 @@ public class UserManager { * user on the device is able to use file transfer over USB because the UI for file transfer * is always associated with the primary user. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -640,6 +717,10 @@ public class UserManager { * Specifies if a user is disallowed from configuring user * credentials. The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -654,6 +735,10 @@ public class UserManager { * This restriction has no effect on managed profiles. * The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_USERS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -692,6 +777,10 @@ public class UserManager { * that user only, including starting activities, making service calls, accessing content * providers, sending broadcasts, installing/uninstalling packages, clearing user data, etc. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -712,6 +801,10 @@ public class UserManager { * <p>From Android 12 ({@linkplain android.os.Build.VERSION_CODES#S API level 31}) enforcing * this restriction clears currently active VPN if it was configured by the user. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_VPN} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -729,6 +822,10 @@ public class UserManager { * managed profile on the parent profile, it disallows the primary user from turning location * on or off. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCATION} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>This user restriction is different from {@link #DISALLOW_SHARE_LOCATION}, @@ -755,6 +852,10 @@ public class UserManager { * from configuring date, time and timezone and disables all configuring of date, time and * timezone in Settings. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_TIME} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -776,6 +877,10 @@ public class UserManager { * the parent profile, it disables the primary user from using Tethering and hotspots and * disables all configuring of Tethering and hotspots in Settings. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>In Android 9.0 or higher, if tethering is enabled when this restriction is set, @@ -796,6 +901,10 @@ public class UserManager { * <p>This restriction has no effect on secondary users and managed profiles since only the * primary user can reset the network settings of the device. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -811,6 +920,10 @@ public class UserManager { * <p>This restriction has no effect on non-admin users since they cannot factory reset the * device. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_FACTORY_RESET} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -827,6 +940,10 @@ public class UserManager { * <p> When the device is an organization-owned device provisioned with a managed profile, * this restriction will be set as a base restriction which cannot be removed by any admin. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_USERS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -858,6 +975,10 @@ public class UserManager { * <p>The default value for an unmanaged user is <code>false</code>. * For users with a device owner set, the default is <code>true</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_PROFILES} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -876,6 +997,10 @@ public class UserManager { * the system enforces app verification across all users on the device. Running in earlier * Android versions, this restriction affects only the profile that sets it. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -893,6 +1018,10 @@ public class UserManager { * on the primary user or by a profile owner of an organization-owned managed profile on * the parent profile, it disables the primary user from configuring cell broadcasts. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>This restriction has no effect on secondary users and managed profiles since only the @@ -915,6 +1044,10 @@ public class UserManager { * on the primary user or by a profile owner of an organization-owned managed profile on * the parent profile, it disables the primary user from configuring mobile networks. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>This restriction has no effect on secondary users and managed profiles since only the @@ -949,6 +1082,10 @@ public class UserManager { * {@link DevicePolicyManager#addPersistentPreferredActivity(ComponentName, IntentFilter, ComponentName)} * to add a default intent handler for a given intent filter. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APPS_CONTROL} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -966,6 +1103,10 @@ public class UserManager { * on the primary user or by a profile owner of an organization-owned managed profile on * the parent profile, it disables the primary user from mounting physical external media. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_PHYSICAL_MEDIA} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -986,6 +1127,10 @@ public class UserManager { * organization-owned managed profile on the parent profile, it will disallow the primary user * from adjusting the microphone volume. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MICROPHONE} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -1004,6 +1149,10 @@ public class UserManager { * <p>When the restriction is set by profile owners, then it only applies to relevant * profiles. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_AUDIO_OUTPUT} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>This restriction has no effect on managed profiles. * <p>Key for user restrictions. * <p>Type: Boolean @@ -1022,6 +1171,10 @@ public class UserManager { * primary user or by a profile owner of an organization-owned managed profile on the parent * profile, it disallows the primary user from making outgoing phone calls. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CALLS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -1041,6 +1194,10 @@ public class UserManager { * on the primary user or by a profile owner of an organization-owned managed profile on * the parent profile, it disables the primary user from sending or receiving SMS messages. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_SMS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -1056,6 +1213,10 @@ public class UserManager { * device owner may wish to prevent the user from experiencing amusement or * joy while using the device. The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_FUN} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1073,6 +1234,10 @@ public class UserManager { * <p>This can only be set by device owners and profile owners on the primary user. * The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_WINDOWS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1091,6 +1256,10 @@ public class UserManager { * the profile owner of the primary user or a secondary user, the restriction affects only the * calling user. This user restriction has no effect on managed profiles. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1108,6 +1277,10 @@ public class UserManager { * optical character recognition (OCR), we strongly recommend combining this user restriction * with {@link DevicePolicyManager#setScreenCaptureDisabled(ComponentName, boolean)}. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_PROFILE_INTERACTION} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1120,6 +1293,10 @@ public class UserManager { * Specifies if the user is not allowed to use NFC to beam out data from apps. * The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1145,6 +1322,10 @@ public class UserManager { * are able to set wallpaper regardless of this restriction. * The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_WALLPAPER} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1163,6 +1344,10 @@ public class UserManager { * the parent profile, it disables the primary user from rebooting the device into safe * boot mode. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_SAFE_BOOT} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -1191,6 +1376,10 @@ public class UserManager { * * <p>This restriction can be set by device owners and profile owners. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_RUN_IN_BACKGROUND} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) * @see #getUserRestrictions() @@ -1208,6 +1397,10 @@ public class UserManager { * profile owner of an organization-owned managed profile on the parent profile, it disables * the primary user from using camera. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CAMERA} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1220,6 +1413,10 @@ public class UserManager { /** * Specifies if a user is not allowed to unmute the device's global volume. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_AUDIO_OUTPUT} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * @see DevicePolicyManager#setMasterVolumeMuted(ComponentName, boolean) * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) * @see #getUserRestrictions() @@ -1236,6 +1433,10 @@ public class UserManager { * on the primary user or by a profile owner of an organization-owned managed profile on * the parent profile, it disables the primary user from using cellular data when roaming. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1249,6 +1450,10 @@ public class UserManager { * can set this restriction. When it is set by device owner, only the target user will be * affected. The default value is <code>false</code>. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_USERS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1285,6 +1490,11 @@ public class UserManager { * * <p>Can be set by profile owners. It only has effect on managed profiles when set by managed * profile owner. Has no effect on non-managed profiles or users. + * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>Key for user restrictions. * <p>Type: Boolean * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1302,7 +1512,12 @@ public class UserManager { * {@link android.content.Intent#ACTION_VIEW}, * category {@link android.content.Intent#CATEGORY_BROWSABLE}, scheme http or https, and which * define a host can handle intents from the managed profile. - * The default value is <code>false</code>. + * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_PROFILES} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * + * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. * <p>Type: Boolean @@ -1319,6 +1534,10 @@ public class UserManager { * <p>Device owner and profile owner can set this restriction. When it is set by device owner, * only the target user will be affected. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_AUTOFILL} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1336,6 +1555,10 @@ public class UserManager { * managed profile on the parent profile, it disables the primary user's screen from being * captured for artificial intelligence purposes. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_SCREEN_CONTENT} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1353,6 +1576,10 @@ public class UserManager { * managed profile on the parent profile, it disables the primary user from receiving content * suggestions for selections based on the contents of their screen. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_SCREEN_CONTENT} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1369,6 +1596,10 @@ public class UserManager { * {@link DevicePolicyManager#switchUser(ComponentName, UserHandle)} when this restriction is * set. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_USERS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1391,7 +1622,12 @@ public class UserManager { * This restriction is only meaningful when set by profile owner. When it is set by device * owner, it does not have any effect. * <p> - * The default value is <code>false</code>. + * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_PROFILE_INTERACTION} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * + * <p>The default value is <code>false</code>. * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) @@ -1404,6 +1640,10 @@ public class UserManager { * * This restriction can be set by device or profile owner. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_PRINTING} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * The default value is {@code false}. * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1419,6 +1659,10 @@ public class UserManager { * organization-owned managed profile on the parent profile. When it is set by either of these * owners, it applies globally. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_RESTRICT_PRIVATE_DNS} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * <p>Key for user restrictions. @@ -1525,6 +1769,10 @@ public class UserManager { * In all cases, the setting applies globally on the device and will prevent the device from * scanning for or connecting to 2g networks, except in the case of an emergency. * + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} + * can set this restriction using the DevicePolicyManager APIs mentioned below. + * * <p>The default value is <code>false</code>. * * @see DevicePolicyManager#addUserRestriction(ComponentName, String) @@ -1537,15 +1785,19 @@ public class UserManager { * This user restriction specifies if Ultra-wideband is disallowed on the device. If * Ultra-wideband is disallowed it cannot be turned on via Settings. * + * <p> + * Ultra-wideband (UWB) is a radio technology that can use a very low energy level + * for short-range, high-bandwidth communications over a large portion of the radio spectrum. + * * <p>This restriction can only be set by a device owner or a profile owner of an * organization-owned managed profile on the parent profile. * In both cases, the restriction applies globally on the device and will turn off the * ultra-wideband radio if it's currently on and prevent the radio from being turned on in * the future. * - * <p> - * Ultra-wideband (UWB) is a radio technology that can use a very low energy level - * for short-range, high-bandwidth communications over a large portion of the radio spectrum. + * <p>Holders of the permission + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION} + * can set this restriction using the DevicePolicyManager APIs mentioned below. * * <p>Default is <code>false</code>. * diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index a42af1af2d46..5c79f697ba13 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3036,7 +3036,9 @@ public final class Settings { public void destroy() { try { - if (!mArray.isClosed()) { + // If this process is the system server process, mArray is the same object as + // the memory int array kept inside SettingsProvider, so skipping the close() + if (!Settings.isInSystemServer() && !mArray.isClosed()) { mArray.close(); } } catch (IOException e) { @@ -12446,6 +12448,17 @@ public final class Settings { "bypass_device_policy_management_role_qualifications"; /** + * Whether work profile telephony feature is enabled for non + * {@link android.app.role.RoleManager#ROLE_DEVICE_POLICY_MANAGEMENT} holders. + * ("0" = false, "1" = true). + * + * @hide + */ + @Readable + public static final String ALLOW_WORK_PROFILE_TELEPHONY_FOR_NON_DPM_ROLE_HOLDERS = + "allow_work_profile_telephony_for_non_dpm_role_holders"; + + /** * Indicates whether mobile data should be allowed while the device is being provisioned. * This allows the provisioning process to turn off mobile data before the user * has an opportunity to set things up, preventing other processes from burning diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java index 5f7486addd69..d04ff8e361b3 100644 --- a/core/java/android/service/autofill/Dataset.java +++ b/core/java/android/service/autofill/Dataset.java @@ -123,47 +123,50 @@ public final class Dataset implements Parcelable { */ public static final int PICK_REASON_UNKNOWN = 0; /** - * This dataset is picked because of autofill provider detection was chosen. + * This dataset is picked because pcc wasn't enabled. * @hide */ - public static final int PICK_REASON_AUTOFILL_PROVIDER_DETECTION = 1; + public static final int PICK_REASON_NO_PCC = 1; /** - * This dataset is picked because of PCC detection was chosen. + * This dataset is picked because provider gave this dataset. * @hide */ - public static final int PICK_REASON_PCC_DETECTION = 2; + public static final int PICK_REASON_PROVIDER_DETECTION_ONLY = 2; /** - * This dataset is picked because of Framework detection was chosen. + * This dataset is picked because provider detection was preferred. However, provider also made + * this dataset available for PCC detected types, so they could've been picked up by PCC + * detection. This however doesn't imply that this dataset would've been chosen for sure. For + * eg, if PCC Detection was preferred, and PCC detected other field types, which wasn't + * applicable to this dataset, it wouldn't have been shown. * @hide */ - public static final int PICK_REASON_FRAMEWORK_DETECTION = 3; + public static final int PICK_REASON_PROVIDER_DETECTION_PREFERRED_WITH_PCC = 3; /** - * This dataset is picked because of Autofill Provider being a fallback. + * This dataset is picked because of PCC detection was chosen. * @hide */ - public static final int PICK_REASON_AUTOFILL_PROVIDER_FALLBACK = 4; + public static final int PICK_REASON_PCC_DETECTION_ONLY = 4; /** - * This dataset is picked because of PCC detection being a fallback. + * This dataset is picked because of PCC Detection was preferred. However, Provider also gave + * this dataset, so if PCC wasn't enabled, this dataset would've been eligible anyway. * @hide */ - public static final int PICK_REASON_PCC_DETECTION_FALLBACK = 5; + public static final int PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER = 5; + /** - * This dataset is picked because of Framework detection being a fallback. + * Reason why the dataset was eligible for autofill. * @hide */ - public static final int PICK_REASON_FRAMEWORK_FALLBACK = 6; - @IntDef(prefix = { "PICK_REASON_" }, value = { PICK_REASON_UNKNOWN, - PICK_REASON_AUTOFILL_PROVIDER_DETECTION, - PICK_REASON_PCC_DETECTION, - PICK_REASON_FRAMEWORK_DETECTION, - PICK_REASON_AUTOFILL_PROVIDER_FALLBACK, - PICK_REASON_PCC_DETECTION_FALLBACK, - PICK_REASON_FRAMEWORK_FALLBACK, + PICK_REASON_NO_PCC, + PICK_REASON_PROVIDER_DETECTION_ONLY, + PICK_REASON_PROVIDER_DETECTION_PREFERRED_WITH_PCC, + PICK_REASON_PCC_DETECTION_ONLY, + PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER, }) @Retention(RetentionPolicy.SOURCE) - @interface DatasetEligibleReason{} + public @interface DatasetEligibleReason{} private @DatasetEligibleReason int mEligibleReason; diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java index c7b89eb284b6..a4f129ec247e 100644 --- a/core/java/android/service/notification/ZenModeDiff.java +++ b/core/java/android/service/notification/ZenModeDiff.java @@ -205,12 +205,24 @@ public class ZenModeDiff { private final ArrayMap<String, RuleDiff> mAutomaticRulesDiff = new ArrayMap<>(); private RuleDiff mManualRuleDiff; - // Helpers for string generation - private static final String ALLOW_CALLS_FROM_FIELD = "allowCallsFrom"; - private static final String ALLOW_MESSAGES_FROM_FIELD = "allowMessagesFrom"; - private static final String ALLOW_CONVERSATIONS_FROM_FIELD = "allowConversationsFrom"; + // Field name constants + public static final String FIELD_USER = "user"; + public static final String FIELD_ALLOW_ALARMS = "allowAlarms"; + public static final String FIELD_ALLOW_MEDIA = "allowMedia"; + public static final String FIELD_ALLOW_SYSTEM = "allowSystem"; + public static final String FIELD_ALLOW_CALLS = "allowCalls"; + public static final String FIELD_ALLOW_REMINDERS = "allowReminders"; + public static final String FIELD_ALLOW_EVENTS = "allowEvents"; + public static final String FIELD_ALLOW_REPEAT_CALLERS = "allowRepeatCallers"; + public static final String FIELD_ALLOW_MESSAGES = "allowMessages"; + public static final String FIELD_ALLOW_CONVERSATIONS = "allowConversations"; + public static final String FIELD_ALLOW_CALLS_FROM = "allowCallsFrom"; + public static final String FIELD_ALLOW_MESSAGES_FROM = "allowMessagesFrom"; + public static final String FIELD_ALLOW_CONVERSATIONS_FROM = "allowConversationsFrom"; + public static final String FIELD_SUPPRESSED_VISUAL_EFFECTS = "suppressedVisualEffects"; + public static final String FIELD_ARE_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd"; private static final Set<String> PEOPLE_TYPE_FIELDS = - Set.of(ALLOW_CALLS_FROM_FIELD, ALLOW_MESSAGES_FROM_FIELD); + Set.of(FIELD_ALLOW_CALLS_FROM, FIELD_ALLOW_MESSAGES_FROM); /** * Create a diff that contains diffs between the "from" and "to" ZenModeConfigs. @@ -232,57 +244,57 @@ public class ZenModeDiff { // Now we compare all the fields, knowing there's a diff and that neither is null if (from.user != to.user) { - addField("user", new FieldDiff<>(from.user, to.user)); + addField(FIELD_USER, new FieldDiff<>(from.user, to.user)); } if (from.allowAlarms != to.allowAlarms) { - addField("allowAlarms", new FieldDiff<>(from.allowAlarms, to.allowAlarms)); + addField(FIELD_ALLOW_ALARMS, new FieldDiff<>(from.allowAlarms, to.allowAlarms)); } if (from.allowMedia != to.allowMedia) { - addField("allowMedia", new FieldDiff<>(from.allowMedia, to.allowMedia)); + addField(FIELD_ALLOW_MEDIA, new FieldDiff<>(from.allowMedia, to.allowMedia)); } if (from.allowSystem != to.allowSystem) { - addField("allowSystem", new FieldDiff<>(from.allowSystem, to.allowSystem)); + addField(FIELD_ALLOW_SYSTEM, new FieldDiff<>(from.allowSystem, to.allowSystem)); } if (from.allowCalls != to.allowCalls) { - addField("allowCalls", new FieldDiff<>(from.allowCalls, to.allowCalls)); + addField(FIELD_ALLOW_CALLS, new FieldDiff<>(from.allowCalls, to.allowCalls)); } if (from.allowReminders != to.allowReminders) { - addField("allowReminders", + addField(FIELD_ALLOW_REMINDERS, new FieldDiff<>(from.allowReminders, to.allowReminders)); } if (from.allowEvents != to.allowEvents) { - addField("allowEvents", new FieldDiff<>(from.allowEvents, to.allowEvents)); + addField(FIELD_ALLOW_EVENTS, new FieldDiff<>(from.allowEvents, to.allowEvents)); } if (from.allowRepeatCallers != to.allowRepeatCallers) { - addField("allowRepeatCallers", + addField(FIELD_ALLOW_REPEAT_CALLERS, new FieldDiff<>(from.allowRepeatCallers, to.allowRepeatCallers)); } if (from.allowMessages != to.allowMessages) { - addField("allowMessages", + addField(FIELD_ALLOW_MESSAGES, new FieldDiff<>(from.allowMessages, to.allowMessages)); } if (from.allowConversations != to.allowConversations) { - addField("allowConversations", + addField(FIELD_ALLOW_CONVERSATIONS, new FieldDiff<>(from.allowConversations, to.allowConversations)); } if (from.allowCallsFrom != to.allowCallsFrom) { - addField("allowCallsFrom", + addField(FIELD_ALLOW_CALLS_FROM, new FieldDiff<>(from.allowCallsFrom, to.allowCallsFrom)); } if (from.allowMessagesFrom != to.allowMessagesFrom) { - addField("allowMessagesFrom", + addField(FIELD_ALLOW_MESSAGES_FROM, new FieldDiff<>(from.allowMessagesFrom, to.allowMessagesFrom)); } if (from.allowConversationsFrom != to.allowConversationsFrom) { - addField("allowConversationsFrom", + addField(FIELD_ALLOW_CONVERSATIONS_FROM, new FieldDiff<>(from.allowConversationsFrom, to.allowConversationsFrom)); } if (from.suppressedVisualEffects != to.suppressedVisualEffects) { - addField("suppressedVisualEffects", + addField(FIELD_SUPPRESSED_VISUAL_EFFECTS, new FieldDiff<>(from.suppressedVisualEffects, to.suppressedVisualEffects)); } if (from.areChannelsBypassingDnd != to.areChannelsBypassingDnd) { - addField("areChannelsBypassingDnd", + addField(FIELD_ARE_CHANNELS_BYPASSING_DND, new FieldDiff<>(from.areChannelsBypassingDnd, to.areChannelsBypassingDnd)); } @@ -366,7 +378,7 @@ public class ZenModeDiff { sb.append(ZenModeConfig.sourceToString((int) diff.from())); sb.append("->"); sb.append(ZenModeConfig.sourceToString((int) diff.to())); - } else if (key.equals(ALLOW_CONVERSATIONS_FROM_FIELD)) { + } else if (key.equals(FIELD_ALLOW_CONVERSATIONS_FROM)) { sb.append(key); sb.append(":"); sb.append(ZenPolicy.conversationTypeToString((int) diff.from())); @@ -428,6 +440,24 @@ public class ZenModeDiff { * Diff class representing a change between two ZenRules. */ public static class RuleDiff extends BaseDiff { + public static final String FIELD_ENABLED = "enabled"; + public static final String FIELD_SNOOZING = "snoozing"; + public static final String FIELD_NAME = "name"; + public static final String FIELD_ZEN_MODE = "zenMode"; + public static final String FIELD_CONDITION_ID = "conditionId"; + public static final String FIELD_CONDITION = "condition"; + public static final String FIELD_COMPONENT = "component"; + public static final String FIELD_CONFIGURATION_ACTIVITY = "configurationActivity"; + public static final String FIELD_ID = "id"; + public static final String FIELD_CREATION_TIME = "creationTime"; + public static final String FIELD_ENABLER = "enabler"; + public static final String FIELD_ZEN_POLICY = "zenPolicy"; + public static final String FIELD_MODIFIED = "modified"; + public static final String FIELD_PKG = "pkg"; + + // Special field to track whether this rule became active or inactive + FieldDiff<Boolean> mActiveDiff; + /** * Create a RuleDiff representing the difference between two ZenRule objects. * @param from previous ZenRule @@ -440,54 +470,64 @@ public class ZenModeDiff { if (from == null && to == null) { return; } + + // Even if added or removed, there may be a change in whether or not it was active. + // This only applies to automatic rules. + boolean fromActive = from != null ? from.isAutomaticActive() : false; + boolean toActive = to != null ? to.isAutomaticActive() : false; + if (fromActive != toActive) { + mActiveDiff = new FieldDiff<>(fromActive, toActive); + } + // Return if the diff was added or removed if (hasExistenceChange()) { return; } if (from.enabled != to.enabled) { - addField("enabled", new FieldDiff<>(from.enabled, to.enabled)); + addField(FIELD_ENABLED, new FieldDiff<>(from.enabled, to.enabled)); } if (from.snoozing != to.snoozing) { - addField("snoozing", new FieldDiff<>(from.snoozing, to.snoozing)); + addField(FIELD_SNOOZING, new FieldDiff<>(from.snoozing, to.snoozing)); } if (!Objects.equals(from.name, to.name)) { - addField("name", new FieldDiff<>(from.name, to.name)); + addField(FIELD_NAME, new FieldDiff<>(from.name, to.name)); } if (from.zenMode != to.zenMode) { - addField("zenMode", new FieldDiff<>(from.zenMode, to.zenMode)); + addField(FIELD_ZEN_MODE, new FieldDiff<>(from.zenMode, to.zenMode)); } if (!Objects.equals(from.conditionId, to.conditionId)) { - addField("conditionId", new FieldDiff<>(from.conditionId, to.conditionId)); + addField(FIELD_CONDITION_ID, new FieldDiff<>(from.conditionId, + to.conditionId)); } if (!Objects.equals(from.condition, to.condition)) { - addField("condition", new FieldDiff<>(from.condition, to.condition)); + addField(FIELD_CONDITION, new FieldDiff<>(from.condition, to.condition)); } if (!Objects.equals(from.component, to.component)) { - addField("component", new FieldDiff<>(from.component, to.component)); + addField(FIELD_COMPONENT, new FieldDiff<>(from.component, to.component)); } if (!Objects.equals(from.configurationActivity, to.configurationActivity)) { - addField("configurationActivity", new FieldDiff<>( + addField(FIELD_CONFIGURATION_ACTIVITY, new FieldDiff<>( from.configurationActivity, to.configurationActivity)); } if (!Objects.equals(from.id, to.id)) { - addField("id", new FieldDiff<>(from.id, to.id)); + addField(FIELD_ID, new FieldDiff<>(from.id, to.id)); } if (from.creationTime != to.creationTime) { - addField("creationTime", + addField(FIELD_CREATION_TIME, new FieldDiff<>(from.creationTime, to.creationTime)); } if (!Objects.equals(from.enabler, to.enabler)) { - addField("enabler", new FieldDiff<>(from.enabler, to.enabler)); + addField(FIELD_ENABLER, new FieldDiff<>(from.enabler, to.enabler)); } if (!Objects.equals(from.zenPolicy, to.zenPolicy)) { - addField("zenPolicy", new FieldDiff<>(from.zenPolicy, to.zenPolicy)); + addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy)); } if (from.modified != to.modified) { - addField("modified", new FieldDiff<>(from.modified, to.modified)); + addField(FIELD_MODIFIED, new FieldDiff<>(from.modified, to.modified)); } if (!Objects.equals(from.pkg, to.pkg)) { - addField("pkg", new FieldDiff<>(from.pkg, to.pkg)); + addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg)); } } @@ -536,7 +576,35 @@ public class ZenModeDiff { sb.append(diff); } + if (becameActive()) { + if (!first) { + sb.append(", "); + } + sb.append("(->active)"); + } else if (becameInactive()) { + if (!first) { + sb.append(", "); + } + sb.append("(->inactive)"); + } + return sb.append("}").toString(); } + + /** + * Returns whether this diff indicates that this (automatic) rule became active. + */ + public boolean becameActive() { + // if the "to" side is true, then it became active + return mActiveDiff != null && mActiveDiff.to(); + } + + /** + * Returns whether this diff indicates that this (automatic) rule became inactive. + */ + public boolean becameInactive() { + // if the "to" side is false, then it became inactive + return mActiveDiff != null && !mActiveDiff.to(); + } } } diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 14f050df2b49..708ebdf3627b 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -18,6 +18,7 @@ package android.service.voice; import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; import static android.Manifest.permission.RECORD_AUDIO; +import static android.service.voice.SoundTriggerFailure.ERROR_CODE_UNKNOWN; import static android.service.voice.VoiceInteractionService.MULTIPLE_ACTIVE_HOTWORD_DETECTORS; import android.annotation.ElapsedRealtimeLong; @@ -270,6 +271,15 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { static final long THROW_ON_INITIALIZE_IF_NO_DSP = 269165460L; /** + * Gates returning {@link Callback#onFailure} and {@link Callback#onUnknownFailure} + * when asynchronous exceptions are propagated to the client. If the change is not enabled, + * the existing behavior of delivering {@link #STATE_ERROR} is retained. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + static final long SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS = 280471513L; + + /** * Controls the sensitivity threshold adjustment factor for a given model. * Negative value corresponds to less sensitive model (high threshold) and * a positive value corresponds to a more sensitive model (low threshold). @@ -313,7 +323,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { private final Executor mExternalExecutor; private final Handler mHandler; private final IBinder mBinder = new Binder(); - private final int mTargetSdkVersion; private final boolean mSupportSandboxedDetectionService; @GuardedBy("mLock") @@ -856,7 +865,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { new Handler(Looper.myLooper())); mInternalCallback = new SoundTriggerListener(mHandler); mModelManagementService = modelManagementService; - mTargetSdkVersion = targetSdkVersion; mSupportSandboxedDetectionService = supportSandboxedDetectionService; } @@ -1382,6 +1390,7 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { * * @hide */ + // TODO(b/281608561): remove the enrollment flow from AlwaysOnHotwordDetector void onSoundModelsChanged() { synchronized (mLock) { if (mAvailability == STATE_INVALID @@ -1401,20 +1410,38 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { return; } - // Stop the recognition before proceeding. - // This is done because we want to stop the recognition on an older model if it changed - // or was deleted. - // The availability change callback should ensure that the client starts recognition - // again if needed. + // Stop the recognition before proceeding if we are in the enrolled state. + // The framework makes the guarantee that an actively used model is present in the + // system server's enrollment database. For this reason we much stop an actively running + // model when the underlying sound model in enrollment database no longer match. if (mAvailability == STATE_KEYPHRASE_ENROLLED) { + // A SoundTriggerFailure will be sent to the client if the model state was + // changed. This is an overloading of the onFailure usage because we are sending a + // callback even in the successful stop case. If stopRecognition is successful, + // suggested next action RESTART_RECOGNITION will be sent. + // TODO(b/281608561): This code path will be removed with other enrollment flows in + // this class. try { - stopRecognitionLocked(); - } catch (SecurityException e) { - Slog.w(TAG, "Failed to Stop the recognition", e); - if (mTargetSdkVersion <= Build.VERSION_CODES.R) { - throw e; + int result = stopRecognitionLocked(); + if (result == STATUS_OK) { + sendSoundTriggerFailure(new SoundTriggerFailure(ERROR_CODE_UNKNOWN, + "stopped recognition because of enrollment update", + FailureSuggestedAction.RESTART_RECOGNITION)); + } + // only log to logcat here because many failures can be false positives such as + // calling stopRecognition where there is no started session. + Log.w(TAG, "Failed to stop recognition after enrollment update: code=" + + result); + } catch (Exception e) { + Slog.w(TAG, "Failed to stop recognition after enrollment update", e); + if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) { + sendSoundTriggerFailure(new SoundTriggerFailure(ERROR_CODE_UNKNOWN, + "Failed to stop recognition after enrollment update: " + + Log.getStackTraceString(e), + FailureSuggestedAction.RECREATE_DETECTOR)); + } else { + updateAndNotifyStateChangedLocked(STATE_ERROR); } - updateAndNotifyStateChangedLocked(STATE_ERROR); return; } } @@ -1538,6 +1565,12 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { @GuardedBy("mLock") private void updateAndNotifyStateChangedLocked(int availability) { + updateAvailabilityLocked(availability); + notifyStateChangedLocked(); + } + + @GuardedBy("mLock") + private void updateAvailabilityLocked(int availability) { if (DBG) { Slog.d(TAG, "Hotword availability changed from " + mAvailability + " -> " + availability); @@ -1545,7 +1578,6 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { if (!mIsAvailabilityOverriddenByTestApi) { mAvailability = availability; } - notifyStateChangedLocked(); } @GuardedBy("mLock") @@ -1555,6 +1587,18 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { message.sendToTarget(); } + @GuardedBy("mLock") + private void sendUnknownFailure(String failureMessage) { + // update but do not call onAvailabilityChanged callback for STATE_ERROR + updateAvailabilityLocked(STATE_ERROR); + Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE, failureMessage).sendToTarget(); + } + + private void sendSoundTriggerFailure(@NonNull SoundTriggerFailure soundTriggerFailure) { + Message.obtain(mHandler, MSG_DETECTION_SOUND_TRIGGER_FAILURE, soundTriggerFailure) + .sendToTarget(); + } + /** @hide */ static final class SoundTriggerListener extends IHotwordRecognitionStatusCallback.Stub { private final Handler mHandler; @@ -1577,6 +1621,7 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { .build()) .sendToTarget(); } + @Override public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) { Slog.w(TAG, "Generic sound trigger event detected at AOHD: " + event); @@ -1726,6 +1771,7 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { } } + // TODO(b/267681692): remove the AsyncTask usage class RefreshAvailabilityTask extends AsyncTask<Void, Void, Void> { @Override @@ -1744,13 +1790,17 @@ public class AlwaysOnHotwordDetector extends AbstractDetector { } updateAndNotifyStateChangedLocked(availability); } - } catch (SecurityException e) { + } catch (Exception e) { + // Any exception here not caught will crash the process because AsyncTask does not + // bubble up the exceptions to the client app, so we must propagate it to the app. Slog.w(TAG, "Failed to refresh availability", e); - if (mTargetSdkVersion <= Build.VERSION_CODES.R) { - throw e; - } synchronized (mLock) { - updateAndNotifyStateChangedLocked(STATE_ERROR); + if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) { + sendUnknownFailure( + "Failed to refresh availability: " + Log.getStackTraceString(e)); + } else { + updateAndNotifyStateChangedLocked(STATE_ERROR); + } } } diff --git a/core/java/android/service/voice/SoundTriggerFailure.java b/core/java/android/service/voice/SoundTriggerFailure.java index 2ce5e5da4724..b4b0ffaf7c2a 100644 --- a/core/java/android/service/voice/SoundTriggerFailure.java +++ b/core/java/android/service/voice/SoundTriggerFailure.java @@ -74,14 +74,22 @@ public final class SoundTriggerFailure implements Parcelable { public @interface SoundTriggerErrorCode {} private final int mErrorCode; + private final int mSuggestedAction; private final String mErrorMessage; /** * @hide */ @TestApi - public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, - @NonNull String errorMessage) { + public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, @NonNull String errorMessage) { + this(errorCode, errorMessage, getSuggestedActionBasedOnErrorCode(errorCode)); + } + + /** + * @hide + */ + public SoundTriggerFailure(@SoundTriggerErrorCode int errorCode, @NonNull String errorMessage, + @FailureSuggestedAction.FailureSuggestedActionDef int suggestedAction) { if (TextUtils.isEmpty(errorMessage)) { throw new IllegalArgumentException("errorMessage is empty or null."); } @@ -95,7 +103,13 @@ public final class SoundTriggerFailure implements Parcelable { default: throw new IllegalArgumentException("Invalid ErrorCode: " + errorCode); } + if (suggestedAction != getSuggestedActionBasedOnErrorCode(errorCode) + && errorCode != ERROR_CODE_UNKNOWN) { + throw new IllegalArgumentException("Invalid suggested next action: " + + "errorCode=" + errorCode + ", suggestedAction=" + suggestedAction); + } mErrorMessage = errorMessage; + mSuggestedAction = suggestedAction; } /** @@ -119,7 +133,11 @@ public final class SoundTriggerFailure implements Parcelable { */ @FailureSuggestedAction.FailureSuggestedActionDef public int getSuggestedAction() { - switch (mErrorCode) { + return mSuggestedAction; + } + + private static int getSuggestedActionBasedOnErrorCode(@SoundTriggerErrorCode int errorCode) { + switch (errorCode) { case ERROR_CODE_UNKNOWN: case ERROR_CODE_MODULE_DIED: case ERROR_CODE_UNEXPECTED_PREEMPTION: @@ -144,8 +162,11 @@ public final class SoundTriggerFailure implements Parcelable { @Override public String toString() { - return "SoundTriggerFailure { errorCode = " + mErrorCode + ", errorMessage = " - + mErrorMessage + " }"; + return "SoundTriggerFailure {" + + " errorCode = " + mErrorCode + + ", errorMessage = " + mErrorMessage + + ", suggestedNextAction = " + mSuggestedAction + + " }"; } public static final @NonNull Parcelable.Creator<SoundTriggerFailure> CREATOR = diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 230f51191ee8..7f0a666da91f 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -2231,7 +2231,7 @@ public abstract class WallpaperService extends Service { mDestroyed = true; - if (mIWallpaperEngine.mDisplayManager != null) { + if (mIWallpaperEngine != null && mIWallpaperEngine.mDisplayManager != null) { mIWallpaperEngine.mDisplayManager.unregisterDisplayListener(mDisplayListener); } diff --git a/core/java/android/speech/OWNERS b/core/java/android/speech/OWNERS index 462d8bed743c..162e02904075 100644 --- a/core/java/android/speech/OWNERS +++ b/core/java/android/speech/OWNERS @@ -2,3 +2,4 @@ volnov@google.com eugeniom@google.com schfan@google.com andreaambu@google.com +hackz@google.com
\ No newline at end of file diff --git a/core/java/android/view/ContentRecordingSession.java b/core/java/android/view/ContentRecordingSession.java index fdecb8bc61ab..a89f79540c65 100644 --- a/core/java/android/view/ContentRecordingSession.java +++ b/core/java/android/view/ContentRecordingSession.java @@ -87,7 +87,7 @@ public final class ContentRecordingSession implements Parcelable { * * <p>Only set on the server side to sanitize any input from the client process. */ - private boolean mWaitingToRecord = false; + private boolean mWaitingForConsent = false; /** * Default instance, with recording the display. @@ -181,7 +181,7 @@ public final class ContentRecordingSession implements Parcelable { @RecordContent int contentToRecord, int displayToRecord, @Nullable IBinder tokenToRecord, - boolean waitingToRecord) { + boolean waitingForConsent) { this.mVirtualDisplayId = virtualDisplayId; this.mContentToRecord = contentToRecord; @@ -195,7 +195,7 @@ public final class ContentRecordingSession implements Parcelable { this.mDisplayToRecord = displayToRecord; this.mTokenToRecord = tokenToRecord; - this.mWaitingToRecord = waitingToRecord; + this.mWaitingForConsent = waitingForConsent; // onConstructed(); // You can define this method to get a callback } @@ -246,8 +246,8 @@ public final class ContentRecordingSession implements Parcelable { * <p>Only set on the server side to sanitize any input from the client process. */ @DataClass.Generated.Member - public boolean isWaitingToRecord() { - return mWaitingToRecord; + public boolean isWaitingForConsent() { + return mWaitingForConsent; } /** @@ -309,8 +309,8 @@ public final class ContentRecordingSession implements Parcelable { * <p>Only set on the server side to sanitize any input from the client process. */ @DataClass.Generated.Member - public @NonNull ContentRecordingSession setWaitingToRecord( boolean value) { - mWaitingToRecord = value; + public @NonNull ContentRecordingSession setWaitingForConsent( boolean value) { + mWaitingForConsent = value; return this; } @@ -325,7 +325,7 @@ public final class ContentRecordingSession implements Parcelable { "contentToRecord = " + recordContentToString(mContentToRecord) + ", " + "displayToRecord = " + mDisplayToRecord + ", " + "tokenToRecord = " + mTokenToRecord + ", " + - "waitingToRecord = " + mWaitingToRecord + + "waitingForConsent = " + mWaitingForConsent + " }"; } @@ -346,7 +346,7 @@ public final class ContentRecordingSession implements Parcelable { && mContentToRecord == that.mContentToRecord && mDisplayToRecord == that.mDisplayToRecord && java.util.Objects.equals(mTokenToRecord, that.mTokenToRecord) - && mWaitingToRecord == that.mWaitingToRecord; + && mWaitingForConsent == that.mWaitingForConsent; } @Override @@ -360,7 +360,7 @@ public final class ContentRecordingSession implements Parcelable { _hash = 31 * _hash + mContentToRecord; _hash = 31 * _hash + mDisplayToRecord; _hash = 31 * _hash + java.util.Objects.hashCode(mTokenToRecord); - _hash = 31 * _hash + Boolean.hashCode(mWaitingToRecord); + _hash = 31 * _hash + Boolean.hashCode(mWaitingForConsent); return _hash; } @@ -371,7 +371,7 @@ public final class ContentRecordingSession implements Parcelable { // void parcelFieldName(Parcel dest, int flags) { ... } byte flg = 0; - if (mWaitingToRecord) flg |= 0x10; + if (mWaitingForConsent) flg |= 0x10; if (mTokenToRecord != null) flg |= 0x8; dest.writeByte(flg); dest.writeInt(mVirtualDisplayId); @@ -392,7 +392,7 @@ public final class ContentRecordingSession implements Parcelable { // static FieldType unparcelFieldName(Parcel in) { ... } byte flg = in.readByte(); - boolean waitingToRecord = (flg & 0x10) != 0; + boolean waitingForConsent = (flg & 0x10) != 0; int virtualDisplayId = in.readInt(); int contentToRecord = in.readInt(); int displayToRecord = in.readInt(); @@ -411,7 +411,7 @@ public final class ContentRecordingSession implements Parcelable { this.mDisplayToRecord = displayToRecord; this.mTokenToRecord = tokenToRecord; - this.mWaitingToRecord = waitingToRecord; + this.mWaitingForConsent = waitingForConsent; // onConstructed(); // You can define this method to get a callback } @@ -441,7 +441,7 @@ public final class ContentRecordingSession implements Parcelable { private @RecordContent int mContentToRecord; private int mDisplayToRecord; private @Nullable IBinder mTokenToRecord; - private boolean mWaitingToRecord; + private boolean mWaitingForConsent; private long mBuilderFieldsSet = 0L; @@ -506,10 +506,10 @@ public final class ContentRecordingSession implements Parcelable { * <p>Only set on the server side to sanitize any input from the client process. */ @DataClass.Generated.Member - public @NonNull Builder setWaitingToRecord(boolean value) { + public @NonNull Builder setWaitingForConsent(boolean value) { checkNotUsed(); mBuilderFieldsSet |= 0x10; - mWaitingToRecord = value; + mWaitingForConsent = value; return this; } @@ -531,14 +531,14 @@ public final class ContentRecordingSession implements Parcelable { mTokenToRecord = null; } if ((mBuilderFieldsSet & 0x10) == 0) { - mWaitingToRecord = false; + mWaitingForConsent = false; } ContentRecordingSession o = new ContentRecordingSession( mVirtualDisplayId, mContentToRecord, mDisplayToRecord, mTokenToRecord, - mWaitingToRecord); + mWaitingForConsent); return o; } @@ -551,10 +551,10 @@ public final class ContentRecordingSession implements Parcelable { } @DataClass.Generated( - time = 1679855157534L, + time = 1683628463074L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/view/ContentRecordingSession.java", - inputSignatures = "public static final int RECORD_CONTENT_DISPLAY\npublic static final int RECORD_CONTENT_TASK\nprivate int mVirtualDisplayId\nprivate @android.view.ContentRecordingSession.RecordContent int mContentToRecord\nprivate int mDisplayToRecord\nprivate @android.annotation.Nullable android.os.IBinder mTokenToRecord\nprivate boolean mWaitingToRecord\npublic static android.view.ContentRecordingSession createDisplaySession(int)\npublic static android.view.ContentRecordingSession createTaskSession(android.os.IBinder)\npublic static boolean isValid(android.view.ContentRecordingSession)\npublic static boolean isProjectionOnSameDisplay(android.view.ContentRecordingSession,android.view.ContentRecordingSession)\nclass ContentRecordingSession extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genSetters=true, genEqualsHashCode=true)") + inputSignatures = "public static final int RECORD_CONTENT_DISPLAY\npublic static final int RECORD_CONTENT_TASK\nprivate int mVirtualDisplayId\nprivate @android.view.ContentRecordingSession.RecordContent int mContentToRecord\nprivate int mDisplayToRecord\nprivate @android.annotation.Nullable android.os.IBinder mTokenToRecord\nprivate boolean mWaitingForConsent\npublic static android.view.ContentRecordingSession createDisplaySession(int)\npublic static android.view.ContentRecordingSession createTaskSession(android.os.IBinder)\npublic static boolean isValid(android.view.ContentRecordingSession)\npublic static boolean isProjectionOnSameDisplay(android.view.ContentRecordingSession,android.view.ContentRecordingSession)\nclass ContentRecordingSession extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genSetters=true, genEqualsHashCode=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 60529c72afae..4b96d74b5687 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -1638,7 +1638,7 @@ public final class Display { * bounds of the window. * <p></p> * Handling multi-window mode correctly is necessary since applications are not always - * fullscreen. A user on a large screen device, such as a tablet or Chrome OS devices, is more + * fullscreen. A user on a large screen device, such as a tablet or ChromeOS devices, is more * likely to use multi-window modes. * <p></p> * For example, consider a device with a display partitioned into two halves. The user may have @@ -1708,7 +1708,7 @@ public final class Display { * bounds of the window. * <p></p> * Handling multi-window mode correctly is necessary since applications are not always - * fullscreen. A user on a large screen device, such as a tablet or Chrome OS devices, is more + * fullscreen. A user on a large screen device, such as a tablet or ChromeOS devices, is more * likely to use multi-window modes. * <p></p> * For example, consider a device with a display partitioned into two halves. The user may have diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 62fdfaea1303..ddaa71c6b1b5 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -461,6 +461,7 @@ public final class SurfaceControl implements Parcelable { private final CloseGuard mCloseGuard = CloseGuard.get(); private String mName; + private String mCallsite; /** * Note: do not rename, this field is used by native code. @@ -774,7 +775,6 @@ public final class SurfaceControl implements Parcelable { release(); } if (nativeObject != 0) { - mCloseGuard.openWithCallSite("release", callsite); mFreeNativeResources = sRegistry.registerNativeAllocation(this, nativeObject); } @@ -785,6 +785,8 @@ public final class SurfaceControl implements Parcelable { } else { mReleaseStack = null; } + setUnreleasedWarningCallSite(callsite); + addToRegistry(); } /** @@ -1322,6 +1324,23 @@ public final class SurfaceControl implements Parcelable { return; } mCloseGuard.openWithCallSite("release", callsite); + mCallsite = callsite; + } + + /** + * Returns the last provided call site when this SurfaceControl was created. + * @hide + */ + @Nullable String getCallsite() { + return mCallsite; + } + + /** + * Returns the name of this SurfaceControl, mainly for debugging purposes. + * @hide + */ + @NonNull String getName() { + return mName; } /** @@ -1435,6 +1454,7 @@ public final class SurfaceControl implements Parcelable { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } + removeFromRegistry(); } finally { super.finalize(); } @@ -1465,6 +1485,7 @@ public final class SurfaceControl implements Parcelable { mChoreographer = null; } } + removeFromRegistry(); } } @@ -4304,6 +4325,26 @@ public final class SurfaceControl implements Parcelable { return -1; } + /** + * Adds this surface control to the registry for this process if it is created. + */ + private void addToRegistry() { + final SurfaceControlRegistry registry = SurfaceControlRegistry.getProcessInstance(); + if (registry != null) { + registry.add(this); + } + } + + /** + * Removes this surface control from the registry for this process. + */ + private void removeFromRegistry() { + final SurfaceControlRegistry registry = SurfaceControlRegistry.getProcessInstance(); + if (registry != null) { + registry.remove(this); + } + } + // Called by native private static void invokeReleaseCallback(Consumer<SyncFence> callback, long nativeFencePtr) { SyncFence fence = new SyncFence(nativeFencePtr); diff --git a/core/java/android/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java new file mode 100644 index 000000000000..095189ad03a7 --- /dev/null +++ b/core/java/android/view/SurfaceControlRegistry.java @@ -0,0 +1,273 @@ +/* + * 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.view; + +import static android.Manifest.permission.READ_FRAME_BUFFER; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.content.Context; +import android.os.SystemClock; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.GcUtils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Map; +import java.util.WeakHashMap; + +/** + * A thread-safe registry used to track surface controls that are active (not yet released) within a + * process, to help debug and identify leaks. + * @hide + */ +public class SurfaceControlRegistry { + private static final String TAG = "SurfaceControlRegistry"; + + /** + * An interface for processing the registered SurfaceControls when the threshold is exceeded. + */ + public interface Reporter { + /** + * Called when the set of layers exceeds the max threshold. This can be called on any + * thread, and must be handled synchronously. + */ + void onMaxLayersExceeded(WeakHashMap<SurfaceControl, Long> surfaceControls, int limit, + PrintWriter pw); + } + + /** + * The default implementation of the reporter which logs the existing registered surfaces to + * logcat. + */ + private static class DefaultReporter implements Reporter { + public void onMaxLayersExceeded(WeakHashMap<SurfaceControl, Long> surfaceControls, + int limit, PrintWriter pw) { + final int size = Math.min(surfaceControls.size(), limit); + final long now = SystemClock.elapsedRealtime(); + final ArrayList<Map.Entry<SurfaceControl, Long>> entries = new ArrayList<>(); + for (Map.Entry<SurfaceControl, Long> entry : surfaceControls.entrySet()) { + entries.add(entry); + } + // Sort entries by time registered when dumping + // TODO: Or should it sort by name? + entries.sort((o1, o2) -> (int) (o1.getValue() - o2.getValue())); + + pw.println("SurfaceControlRegistry"); + pw.println("----------------------"); + pw.println("Listing oldest " + size + " of " + surfaceControls.size()); + for (int i = 0; i < size; i++) { + final Map.Entry<SurfaceControl, Long> entry = entries.get(i); + final SurfaceControl sc = entry.getKey(); + final long timeRegistered = entry.getValue(); + pw.print(" "); + pw.print(sc.getName()); + pw.print(" (" + sc.getCallsite() + ")"); + pw.println(" [" + ((now - timeRegistered) / 1000) + "s ago]"); + } + } + } + + // The threshold at which to dump information about all the known active SurfaceControls in the + // process when the number of layers exceeds a certain count. This should be significantly + // smaller than the MAX_LAYERS (currently 4096) defined in SurfaceFlinger.h + private static final int MAX_LAYERS_REPORTING_THRESHOLD = 1024; + + // The threshold at which to reset the dump state. Needs to be smaller than + // MAX_LAYERS_REPORTING_THRESHOLD + private static final int RESET_REPORTING_THRESHOLD = 256; + + // Number of surface controls to dump when the max threshold is exceeded + private static final int DUMP_LIMIT = 256; + + // Static lock, must be held for all registry operations + private static final Object sLock = new Object(); + + // The default reporter for printing out the registered surfaces + private static final DefaultReporter sDefaultReporter = new DefaultReporter(); + + // The registry for a given process + private static volatile SurfaceControlRegistry sProcessRegistry; + + // Mapping of the active SurfaceControls to the elapsed time when they were registered + @GuardedBy("sLock") + private final WeakHashMap<SurfaceControl, Long> mSurfaceControls; + + // The threshold at which we dump information about the current set of registered surfaces. + // Once this threshold is reached, we no longer report until the number of layers drops below + // mResetReportingThreshold to ensure that we don't spam logcat. + private int mMaxLayersReportingThreshold = MAX_LAYERS_REPORTING_THRESHOLD; + private int mResetReportingThreshold = RESET_REPORTING_THRESHOLD; + + // Whether the current set of layers has exceeded mMaxLayersReportingThreshold, and we have + // already reported the set of registered surfaces. + private boolean mHasReportedExceedingMaxThreshold = false; + + // The handler for when the registry exceeds the max threshold + private Reporter mReporter = sDefaultReporter; + + private SurfaceControlRegistry() { + mSurfaceControls = new WeakHashMap<>(256); + } + + /** + * Sets the thresholds at which the registry reports errors. + * @param maxLayersReportingThreshold The max threshold (inclusive) + * @param resetReportingThreshold The reset threshold (inclusive) + * @hide + */ + @VisibleForTesting + public void setReportingThresholds(int maxLayersReportingThreshold, int resetReportingThreshold, + Reporter reporter) { + synchronized (sLock) { + if (maxLayersReportingThreshold <= 0 + || resetReportingThreshold >= maxLayersReportingThreshold) { + throw new IllegalArgumentException("Expected maxLayersReportingThreshold (" + + maxLayersReportingThreshold + ") to be > 0 and resetReportingThreshold (" + + resetReportingThreshold + ") to be < maxLayersReportingThreshold"); + } + if (reporter == null) { + throw new IllegalArgumentException("Expected non-null reporter"); + } + mMaxLayersReportingThreshold = maxLayersReportingThreshold; + mResetReportingThreshold = resetReportingThreshold; + mHasReportedExceedingMaxThreshold = false; + mReporter = reporter; + } + } + + /** + * Creates and initializes the registry for all SurfaceControls in this process. The caller must + * hold the READ_FRAME_BUFFER permission. + * @hide + */ + @RequiresPermission(READ_FRAME_BUFFER) + @NonNull + public static void createProcessInstance(Context context) { + if (context.checkSelfPermission(READ_FRAME_BUFFER) != PERMISSION_GRANTED) { + throw new SecurityException("Expected caller to hold READ_FRAME_BUFFER"); + } + synchronized (sLock) { + if (sProcessRegistry == null) { + sProcessRegistry = new SurfaceControlRegistry(); + } + } + } + + /** + * Destroys the previously created registry this process. + * @hide + */ + public static void destroyProcessInstance() { + synchronized (sLock) { + if (sProcessRegistry == null) { + return; + } + sProcessRegistry = null; + } + } + + /** + * Returns the instance of the registry for this process, only non-null if + * createProcessInstance(Context) was previously called from a valid caller. + * @hide + */ + @Nullable + @VisibleForTesting + public static SurfaceControlRegistry getProcessInstance() { + synchronized (sLock) { + return sProcessRegistry; + } + } + + /** + * Adds a SurfaceControl to the registry. + */ + void add(SurfaceControl sc) { + synchronized (sLock) { + mSurfaceControls.put(sc, SystemClock.elapsedRealtime()); + if (!mHasReportedExceedingMaxThreshold + && mSurfaceControls.size() >= mMaxLayersReportingThreshold) { + // Dump existing info to logcat for debugging purposes (but don't close the + // System.out output stream otherwise we can't print to it after this call) + PrintWriter pw = new PrintWriter(System.out, true /* autoFlush */); + mReporter.onMaxLayersExceeded(mSurfaceControls, DUMP_LIMIT, pw); + mHasReportedExceedingMaxThreshold = true; + } + } + } + + /** + * Removes a SurfaceControl from the registry. + */ + void remove(SurfaceControl sc) { + synchronized (sLock) { + mSurfaceControls.remove(sc); + if (mHasReportedExceedingMaxThreshold + && mSurfaceControls.size() <= mResetReportingThreshold) { + mHasReportedExceedingMaxThreshold = false; + } + } + } + + /** + * Returns a hash of this registry and is a function of all the active surface controls. This + * is useful for testing to determine whether the registry has changed between creating and + * destroying new SurfaceControls. + */ + @Override + public int hashCode() { + synchronized (sLock) { + // Return a hash of the surface controls + return mSurfaceControls.keySet().hashCode(); + } + } + + /** + * Forces the gc and finalizers to run, used prior to dumping to ensure we only dump strongly + * referenced surface controls. + */ + private static void runGcAndFinalizers() { + long t = SystemClock.elapsedRealtime(); + GcUtils.runGcAndFinalizersSync(); + Log.i(TAG, "Ran gc and finalizers (" + (SystemClock.elapsedRealtime() - t) + "ms)"); + } + + /** + * Dumps information about the set of SurfaceControls in the registry. + * + * @param limit the number of layers to report + * @param runGc whether to run the GC and finalizers before dumping + * @hide + */ + public static void dump(int limit, boolean runGc, PrintWriter pw) { + if (runGc) { + // This needs to run outside the lock since finalization isn't synchronous + runGcAndFinalizers(); + } + synchronized (sLock) { + if (sProcessRegistry != null) { + sDefaultReporter.onMaxLayersExceeded(sProcessRegistry.mSurfaceControls, limit, pw); + } + } + } +} diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index f662c7372667..4cbb0409dafc 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6969,10 +6969,10 @@ public final class ViewRootImpl implements ViewParent, if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_TAB) { - if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) { + if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_CTRL_ON)) { groupNavigationDirection = View.FOCUS_FORWARD; } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(), - KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) { + KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) { groupNavigationDirection = View.FOCUS_BACKWARD; } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 5b6df1cb754e..e95ba797a985 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -1185,6 +1185,68 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE"; /** + * Application level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the app should be opted-out from the + * compatibility override that changes the min aspect ratio. + * + * <p>When this compat override is enabled the min aspect ratio given in the app's manifest can + * be overridden by the device manufacturer using their discretion to improve display + * compatibility unless the app's manifest value is higher. This treatment will also apply if + * no min aspect ratio value is provided in the manifest. These treatments can apply only in + * specific cases (e.g. device is in portrait) or each time the app is displayed on screen. + * + * <p>Setting this property to {@code false} informs the system that the app must be + * opted-out from the compatibility treatment even if the device manufacturer has opted the app + * into the treatment. + * + * <p>Not setting this property at all, or setting this property to {@code true} has no effect. + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE" + * android:value="true|false"/> + * </application> + * </pre> + * @hide + */ + // TODO(b/279428317): Make this public API. + String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = + "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE"; + + /** + * Application level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the app should be opted-out from the + * compatibility overrides that change the resizability of the app. + * + * <p>When these compat overrides are enabled they force the packages they are applied to to be + * resizable / unresizable. If the app is forced to be resizable this won't change whether + * the app can be put into multi-windowing mode, but allow the app to resize without going into + * size-compat mode when the window container resizes, such as display size change or screen + * rotation. + * + * <p>Setting this property to {@code false} informs the system that the app must be + * opted-out from the compatibility treatment even if the device manufacturer has opted the app + * into the treatment. + * + * <p>Not setting this property at all, or setting this property to {@code true} has no effect. + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES" + * android:value="true|false"/> + * </application> + * </pre> + * @hide + */ + // TODO(b/280052089): Make this public API. + String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = + "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"; + + /** * @hide */ public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array"; diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index b67969e5f81a..ba54686c169e 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -275,6 +275,14 @@ public class AutofillFeatureFlags { public static final boolean DEFAULT_AUTOFILL_PCC_CLASSIFICATION_ENABLED = false; // END AUTOFILL PCC CLASSIFICATION FLAGS DEFAULTS + // AUTOFILL FOR ALL APPS DEFAULTS + private static final boolean DEFAULT_AFAA_ON_UNIMPORTANT_VIEW_ENABLED = true; + private static final boolean DEFAULT_AFAA_ON_IMPORTANT_VIEW_ENABLED = true; + private static final String DEFAULT_AFAA_DENYLIST = ""; + private static final String DEFAULT_AFAA_ALLOWLIST = ""; + private static final String DEFAULT_AFAA_NON_AUTOFILLABLE_IME_ACTIONS = "2,3,4"; + private static final boolean DEFAULT_AFAA_SHOULD_ENABLE_AUTOFILL_ON_ALL_VIEW_TYPES = true; + private static final boolean DEFAULT_AFAA_SHOULD_ENABLE_MULTILINE_FILTER = true; private AutofillFeatureFlags() {}; @@ -330,7 +338,8 @@ public class AutofillFeatureFlags { public static boolean isTriggerFillRequestOnUnimportantViewEnabled() { return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_AUTOFILL, - DEVICE_CONFIG_TRIGGER_FILL_REQUEST_ON_UNIMPORTANT_VIEW, false); + DEVICE_CONFIG_TRIGGER_FILL_REQUEST_ON_UNIMPORTANT_VIEW, + DEFAULT_AFAA_ON_UNIMPORTANT_VIEW_ENABLED); } /** @@ -341,7 +350,8 @@ public class AutofillFeatureFlags { public static boolean isTriggerFillRequestOnFilteredImportantViewsEnabled() { return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_AUTOFILL, - DEVICE_CONFIG_TRIGGER_FILL_REQUEST_ON_FILTERED_IMPORTANT_VIEWS, false); + DEVICE_CONFIG_TRIGGER_FILL_REQUEST_ON_FILTERED_IMPORTANT_VIEWS, + DEFAULT_AFAA_ON_IMPORTANT_VIEW_ENABLED); } /** @@ -352,7 +362,8 @@ public class AutofillFeatureFlags { public static boolean shouldEnableAutofillOnAllViewTypes(){ return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_AUTOFILL, - DEVICE_CONFIG_SHOULD_ENABLE_AUTOFILL_ON_ALL_VIEW_TYPES, false); + DEVICE_CONFIG_SHOULD_ENABLE_AUTOFILL_ON_ALL_VIEW_TYPES, + DEFAULT_AFAA_SHOULD_ENABLE_AUTOFILL_ON_ALL_VIEW_TYPES); } /** @@ -363,7 +374,9 @@ public class AutofillFeatureFlags { */ public static Set<String> getNonAutofillableImeActionIdSetFromFlag() { final String mNonAutofillableImeActions = DeviceConfig.getString( - DeviceConfig.NAMESPACE_AUTOFILL, DEVICE_CONFIG_NON_AUTOFILLABLE_IME_ACTION_IDS, ""); + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_NON_AUTOFILLABLE_IME_ACTION_IDS, + DEFAULT_AFAA_NON_AUTOFILLABLE_IME_ACTIONS); return new ArraySet<>(Arrays.asList(mNonAutofillableImeActions.split(","))); } @@ -378,7 +391,8 @@ public class AutofillFeatureFlags { public static String getDenylistStringFromFlag() { return DeviceConfig.getString( DeviceConfig.NAMESPACE_AUTOFILL, - DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW, ""); + DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW, + DEFAULT_AFAA_DENYLIST); } /** @@ -389,7 +403,8 @@ public class AutofillFeatureFlags { public static String getAllowlistStringFromFlag() { return DeviceConfig.getString( DeviceConfig.NAMESPACE_AUTOFILL, - DEVICE_CONFIG_PACKAGE_AND_ACTIVITY_ALLOWLIST_FOR_TRIGGERING_FILL_REQUEST, ""); + DEVICE_CONFIG_PACKAGE_AND_ACTIVITY_ALLOWLIST_FOR_TRIGGERING_FILL_REQUEST, + DEFAULT_AFAA_ALLOWLIST); } /** * Whether include all views that have autofill type not none in assist structure. @@ -422,7 +437,8 @@ public class AutofillFeatureFlags { public static boolean shouldEnableMultilineFilter() { return DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_AUTOFILL, - DEVICE_CONFIG_MULTILINE_FILTER_ENABLED, false); + DEVICE_CONFIG_MULTILINE_FILTER_ENABLED, + DEFAULT_AFAA_SHOULD_ENABLE_MULTILINE_FILTER); } // START AUTOFILL PCC CLASSIFICATION FUNCTIONS diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 2ef7419ac1b1..ea750765799f 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -468,7 +468,8 @@ public final class AutofillManager { COMMIT_REASON_ACTIVITY_FINISHED, COMMIT_REASON_VIEW_COMMITTED, COMMIT_REASON_VIEW_CLICKED, - COMMIT_REASON_VIEW_CHANGED + COMMIT_REASON_VIEW_CHANGED, + COMMIT_REASON_SESSION_DESTROYED }) @Retention(RetentionPolicy.SOURCE) public @interface AutofillCommitReason {} @@ -507,6 +508,12 @@ public final class AutofillManager { * @hide */ public static final int COMMIT_REASON_VIEW_CHANGED = 4; + /** + * Autofill context was committed because of the session was destroyed. + * + * @hide + */ + public static final int COMMIT_REASON_SESSION_DESTROYED = 5; /** * Makes an authentication id from a request id and a dataset id. diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index 8cff06641847..efd50e7d2343 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -46,8 +46,6 @@ import android.os.IBinder.DeathRecipient; import android.os.RemoteException; import android.text.Selection; import android.text.Spannable; -import android.text.SpannableString; -import android.text.Spanned; import android.text.TextUtils; import android.util.LocalLog; import android.util.Log; @@ -740,7 +738,10 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // Since the same CharSequence instance may be reused in the TextView, we need to make // a copy of its content so that its value will not be changed by subsequent updates // in the TextView. - final CharSequence eventText = stringOrSpannedStringWithoutNoCopySpans(text); + CharSequence trimmed = TextUtils.trimToParcelableSize(text); + final CharSequence eventText = trimmed != null && trimmed == text + ? trimmed.toString() + : trimmed; final int composingStart; final int composingEnd; @@ -761,16 +762,6 @@ public final class MainContentCaptureSession extends ContentCaptureSession { .setSelectionIndex(startIndex, endIndex))); } - private CharSequence stringOrSpannedStringWithoutNoCopySpans(CharSequence source) { - if (source == null) { - return null; - } else if (source instanceof Spanned) { - return new SpannableString(source, /* ignoreNoCopySpan= */ true); - } else { - return source.toString(); - } - } - /** Public because is also used by ViewRootImpl */ public void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) { mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED) diff --git a/core/java/android/view/contentcapture/OWNERS b/core/java/android/view/contentcapture/OWNERS index 1a5cb1e4ca4a..d1eda9648520 100644 --- a/core/java/android/view/contentcapture/OWNERS +++ b/core/java/android/view/contentcapture/OWNERS @@ -5,3 +5,5 @@ joannechung@google.com markpun@google.com lpeter@google.com tymtsai@google.com +hackz@google.com +volnov@google.com
\ No newline at end of file diff --git a/core/java/android/view/contentcapture/ViewNode.java b/core/java/android/view/contentcapture/ViewNode.java index 1762a5817aaf..044a31f3b297 100644 --- a/core/java/android/view/contentcapture/ViewNode.java +++ b/core/java/android/view/contentcapture/ViewNode.java @@ -1052,14 +1052,21 @@ public final class ViewNode extends AssistStructure.ViewNode { } void writeToParcel(Parcel out, boolean simple) { - TextUtils.writeToParcel(mText, out, 0); + CharSequence text = TextUtils.trimToParcelableSize(mText); + TextUtils.writeToParcel(text, out, 0); out.writeFloat(mTextSize); out.writeInt(mTextStyle); out.writeInt(mTextColor); if (!simple) { + int selectionStart = text != null + ? Math.min(mTextSelectionStart, text.length()) + : mTextSelectionStart; + int selectionEnd = text != null + ? Math.min(mTextSelectionEnd, text.length()) + : mTextSelectionEnd; out.writeInt(mTextBackgroundColor); - out.writeInt(mTextSelectionStart); - out.writeInt(mTextSelectionEnd); + out.writeInt(selectionStart); + out.writeInt(selectionEnd); out.writeIntArray(mLineCharOffsets); out.writeIntArray(mLineBaselines); out.writeString(mHint); diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index c28950662fb3..34e6e49d390f 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -783,7 +783,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private class SetEmptyView extends Action { + private static class SetEmptyView extends Action { int emptyViewId; SetEmptyView(@IdRes int viewId, @IdRes int emptyViewId) { @@ -820,7 +820,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private class SetPendingIntentTemplate extends Action { + private static class SetPendingIntentTemplate extends Action { public SetPendingIntentTemplate(@IdRes int id, PendingIntent pendingIntentTemplate) { this.viewId = id; this.pendingIntentTemplate = pendingIntentTemplate; @@ -891,7 +891,7 @@ public class RemoteViews implements Parcelable, Filter { PendingIntent pendingIntentTemplate; } - private class SetRemoteViewsAdapterList extends Action { + private static class SetRemoteViewsAdapterList extends Action { public SetRemoteViewsAdapterList(@IdRes int id, ArrayList<RemoteViews> list, int viewTypeCount) { this.viewId = id; @@ -1354,7 +1354,7 @@ public class RemoteViews implements Parcelable, Filter { } @Nullable - private MethodHandle getMethod(View view, String methodName, Class<?> paramType, + private static MethodHandle getMethod(View view, String methodName, Class<?> paramType, boolean async) { MethodArgs result; Class<? extends View> klass = view.getClass(); @@ -1433,7 +1433,7 @@ public class RemoteViews implements Parcelable, Filter { * to {@link ImageView#getDrawable()}. * <p> */ - private class SetDrawableTint extends Action { + private static class SetDrawableTint extends Action { SetDrawableTint(@IdRes int id, boolean targetBackground, @ColorInt int colorFilter, @NonNull PorterDuff.Mode mode) { this.viewId = id; @@ -1697,7 +1697,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Base class for the reflection actions. */ - private abstract class BaseReflectionAction extends Action { + private abstract static class BaseReflectionAction extends Action { static final int BOOLEAN = 1; static final int BYTE = 2; static final int SHORT = 3; @@ -1860,7 +1860,7 @@ public class RemoteViews implements Parcelable, Filter { } /** Class for the reflection actions. */ - private final class ReflectionAction extends BaseReflectionAction { + private static final class ReflectionAction extends BaseReflectionAction { @UnsupportedAppUsage Object value; @@ -2006,7 +2006,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private final class ResourceReflectionAction extends BaseReflectionAction { + private static final class ResourceReflectionAction extends BaseReflectionAction { static final int DIMEN_RESOURCE = 1; static final int COLOR_RESOURCE = 2; @@ -2093,7 +2093,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private final class AttributeReflectionAction extends BaseReflectionAction { + private static final class AttributeReflectionAction extends BaseReflectionAction { static final int DIMEN_RESOURCE = 1; static final int COLOR_RESOURCE = 2; @@ -2187,7 +2187,7 @@ public class RemoteViews implements Parcelable, Filter { return ATTRIBUTE_REFLECTION_ACTION_TAG; } } - private final class ComplexUnitDimensionReflectionAction extends BaseReflectionAction { + private static final class ComplexUnitDimensionReflectionAction extends BaseReflectionAction { private final float mValue; @ComplexDimensionUnit @@ -2243,7 +2243,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private final class NightModeReflectionAction extends BaseReflectionAction { + private static final class NightModeReflectionAction extends BaseReflectionAction { private final Object mLightValue; private final Object mDarkValue; @@ -2603,7 +2603,7 @@ public class RemoteViews implements Parcelable, Filter { /** * ViewGroup methods related to removing child views. */ - private class ViewGroupActionRemove extends Action { + private static class ViewGroupActionRemove extends Action { /** * Id that indicates that all child views of the affected ViewGroup should be removed. * @@ -2725,7 +2725,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Action to remove a view from its parent. */ - private class RemoveFromParentAction extends Action { + private static class RemoveFromParentAction extends Action { RemoveFromParentAction(@IdRes int viewId) { this.viewId = viewId; @@ -2795,7 +2795,7 @@ public class RemoteViews implements Parcelable, Filter { * Helper action to set compound drawables on a TextView. Supports relative * (s/t/e/b) or cardinal (l/t/r/b) arrangement. */ - private class TextViewDrawableAction extends Action { + private static class TextViewDrawableAction extends Action { public TextViewDrawableAction(@IdRes int viewId, boolean isRelative, @DrawableRes int d1, @DrawableRes int d2, @DrawableRes int d3, @DrawableRes int d4) { this.viewId = viewId; @@ -2942,7 +2942,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Helper action to set text size on a TextView in any supported units. */ - private class TextViewSizeAction extends Action { + private static class TextViewSizeAction extends Action { TextViewSizeAction(@IdRes int viewId, @ComplexDimensionUnit int units, float size) { this.viewId = viewId; this.units = units; @@ -2980,7 +2980,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Helper action to set padding on a View. */ - private class ViewPaddingAction extends Action { + private static class ViewPaddingAction extends Action { public ViewPaddingAction(@IdRes int viewId, @Px int left, @Px int top, @Px int right, @Px int bottom) { this.viewId = viewId; @@ -3210,7 +3210,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Helper action to add a view tag with RemoteInputs. */ - private class SetRemoteInputsAction extends Action { + private static class SetRemoteInputsAction extends Action { public SetRemoteInputsAction(@IdRes int viewId, RemoteInput[] remoteInputs) { this.viewId = viewId; @@ -3246,7 +3246,7 @@ public class RemoteViews implements Parcelable, Filter { /** * Helper action to override all textViewColors */ - private class OverrideTextColorsAction extends Action { + private static class OverrideTextColorsAction extends Action { private final int textColor; @@ -3289,7 +3289,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private class SetIntTagAction extends Action { + private static class SetIntTagAction extends Action { @IdRes private final int mViewId; @IdRes private final int mKey; private final int mTag; diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java index 54d19eb79305..ee6ac1297b63 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -50,6 +50,7 @@ import com.android.internal.annotations.GuardedBy; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; @@ -205,7 +206,7 @@ public class Toast { INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; - tn.mNextView = mNextView; + tn.mNextView = new WeakReference<>(mNextView); final boolean isUiContext = mContext.isUiContext(); final int displayId = mContext.getDisplayId(); @@ -622,7 +623,7 @@ public class Toast { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) View mView; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) - View mNextView; + WeakReference<View> mNextView; int mDuration; WindowManager mWM; @@ -632,7 +633,7 @@ public class Toast { private final ToastPresenter mPresenter; @GuardedBy("mCallbacks") - private final List<Callback> mCallbacks; + private final WeakReference<List<Callback>> mCallbacks; /** * Creates a {@link ITransientNotification} object. @@ -649,7 +650,7 @@ public class Toast { mParams = mPresenter.getLayoutParams(); mPackageName = packageName; mToken = token; - mCallbacks = callbacks; + mCallbacks = new WeakReference<>(callbacks); mHandler = new Handler(looper, null) { @Override @@ -685,7 +686,11 @@ public class Toast { private List<Callback> getCallbacks() { synchronized (mCallbacks) { - return new ArrayList<>(mCallbacks); + if (mCallbacks.get() != null) { + return new ArrayList<>(mCallbacks.get()); + } else { + return new ArrayList<>(); + } } } @@ -721,13 +726,15 @@ public class Toast { if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) { return; } - if (mView != mNextView) { + if (mNextView != null && mView != mNextView.get()) { // remove the old view if necessary handleHide(); - mView = mNextView; - mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY, - mHorizontalMargin, mVerticalMargin, - new CallbackBinder(getCallbacks(), mHandler)); + mView = mNextView.get(); + if (mView != null) { + mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY, + mHorizontalMargin, mVerticalMargin, + new CallbackBinder(getCallbacks(), mHandler)); + } } } diff --git a/core/java/android/widget/ToastPresenter.java b/core/java/android/widget/ToastPresenter.java index 7467100838ec..7cb61fe597db 100644 --- a/core/java/android/widget/ToastPresenter.java +++ b/core/java/android/widget/ToastPresenter.java @@ -41,6 +41,8 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import java.lang.ref.WeakReference; + /** * Class responsible for toast presentation inside app's process and in system UI. * @@ -87,25 +89,33 @@ public class ToastPresenter { return view; } - private final Context mContext; private final Resources mResources; - private final WindowManager mWindowManager; - private final IAccessibilityManager mAccessibilityManager; + private final WeakReference<WindowManager> mWindowManager; + private final WeakReference<AccessibilityManager> mAccessibilityManager; private final INotificationManager mNotificationManager; private final String mPackageName; + private final String mContextPackageName; private final WindowManager.LayoutParams mParams; @Nullable private View mView; @Nullable private IBinder mToken; public ToastPresenter(Context context, IAccessibilityManager accessibilityManager, INotificationManager notificationManager, String packageName) { - mContext = context; mResources = context.getResources(); - mWindowManager = context.getSystemService(WindowManager.class); + mWindowManager = new WeakReference<>(context.getSystemService(WindowManager.class)); mNotificationManager = notificationManager; mPackageName = packageName; - mAccessibilityManager = accessibilityManager; + mContextPackageName = context.getPackageName(); mParams = createLayoutParams(); + + // We obtain AccessibilityManager manually via its constructor instead of using method + // AccessibilityManager.getInstance() for 2 reasons: + // 1. We want to be able to inject IAccessibilityManager in tests to verify behavior. + // 2. getInstance() caches the instance for the process even if we pass a different + // context to it. This is problematic for multi-user because callers can pass a context + // created via Context.createContextAsUser(). + mAccessibilityManager = new WeakReference<>( + new AccessibilityManager(context, accessibilityManager, context.getUserId())); } public String getPackageName() { @@ -173,7 +183,7 @@ public class ToastPresenter { params.y = yOffset; params.horizontalMargin = horizontalMargin; params.verticalMargin = verticalMargin; - params.packageName = mContext.getPackageName(); + params.packageName = mContextPackageName; params.hideTimeoutMilliseconds = (duration == Toast.LENGTH_LONG) ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; params.token = windowToken; @@ -270,8 +280,9 @@ public class ToastPresenter { public void hide(@Nullable ITransientNotificationCallback callback) { checkState(mView != null, "No toast to hide."); - if (mView.getParent() != null) { - mWindowManager.removeViewImmediate(mView); + final WindowManager windowManager = mWindowManager.get(); + if (mView.getParent() != null && windowManager != null) { + windowManager.removeViewImmediate(mView); } try { mNotificationManager.finishToken(mPackageName, mToken); @@ -295,14 +306,11 @@ public class ToastPresenter { * enabled. */ public void trySendAccessibilityEvent(View view, String packageName) { - // We obtain AccessibilityManager manually via its constructor instead of using method - // AccessibilityManager.getInstance() for 2 reasons: - // 1. We want to be able to inject IAccessibilityManager in tests to verify behavior. - // 2. getInstance() caches the instance for the process even if we pass a different - // context to it. This is problematic for multi-user because callers can pass a context - // created via Context.createContextAsUser(). - final AccessibilityManager accessibilityManager = - new AccessibilityManager(mContext, mAccessibilityManager, mContext.getUserId()); + final AccessibilityManager accessibilityManager = mAccessibilityManager.get(); + if (accessibilityManager == null) { + return; + } + if (!accessibilityManager.isEnabled()) { accessibilityManager.removeClient(); return; @@ -320,11 +328,15 @@ public class ToastPresenter { } private void addToastView() { + final WindowManager windowManager = mWindowManager.get(); + if (windowManager == null) { + return; + } if (mView.getParent() != null) { - mWindowManager.removeView(mView); + windowManager.removeView(mView); } try { - mWindowManager.addView(mView, mParams); + windowManager.addView(mView, mParams); } catch (WindowManager.BadTokenException e) { // Since the notification manager service cancels the token right after it notifies us // to cancel the toast there is an inherent race and we may attempt to add a window diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java index 618670a0a2c4..e58c044730ae 100644 --- a/core/java/android/window/BackProgressAnimator.java +++ b/core/java/android/window/BackProgressAnimator.java @@ -43,7 +43,7 @@ public class BackProgressAnimator { private ProgressCallback mCallback; private float mProgress = 0; private BackMotionEvent mLastBackEvent; - private boolean mStarted = false; + private boolean mBackAnimationInProgress = false; private void setProgress(float progress) { mProgress = progress; @@ -87,7 +87,7 @@ public class BackProgressAnimator { * @param event the {@link BackMotionEvent} containing the latest target progress. */ public void onBackProgressed(BackMotionEvent event) { - if (!mStarted) { + if (!mBackAnimationInProgress) { return; } mLastBackEvent = event; @@ -108,7 +108,7 @@ public class BackProgressAnimator { reset(); mLastBackEvent = event; mCallback = callback; - mStarted = true; + mBackAnimationInProgress = true; } /** @@ -122,7 +122,7 @@ public class BackProgressAnimator { // Should never happen. mSpring.cancel(); } - mStarted = false; + mBackAnimationInProgress = false; mLastBackEvent = null; mCallback = null; mProgress = 0; @@ -149,8 +149,13 @@ public class BackProgressAnimator { mSpring.animateToFinalPosition(0); } + /** Returns true if the back animation is in progress. */ + boolean isBackAnimationInProgress() { + return mBackAnimationInProgress; + } + private void updateProgressValue(float progress) { - if (mLastBackEvent == null || mCallback == null || !mStarted) { + if (mLastBackEvent == null || mCallback == null || !mBackAnimationInProgress) { return; } mCallback.onProgressUpdate( diff --git a/core/java/android/window/WindowInfosListenerForTest.java b/core/java/android/window/WindowInfosListenerForTest.java index 01e577fc5c6a..cfbeeffb91a6 100644 --- a/core/java/android/window/WindowInfosListenerForTest.java +++ b/core/java/android/window/WindowInfosListenerForTest.java @@ -68,12 +68,18 @@ public class WindowInfosListenerForTest { */ public final boolean isTrustedOverlay; + /** + * True if the window is visible. + */ + public final boolean isVisible; + WindowInfo(@NonNull IBinder windowToken, @NonNull String name, @NonNull Rect bounds, int inputConfig) { this.windowToken = windowToken; this.name = name; this.bounds = bounds; this.isTrustedOverlay = (inputConfig & InputConfig.TRUSTED_OVERLAY) != 0; + this.isVisible = (inputConfig & InputConfig.NOT_VISIBLE) == 0; } } @@ -131,9 +137,6 @@ public class WindowInfosListenerForTest { private static List<WindowInfo> buildWindowInfos(InputWindowHandle[] windowHandles) { var windowInfos = new ArrayList<WindowInfo>(windowHandles.length); for (var handle : windowHandles) { - if ((handle.inputConfig & InputConfig.NOT_VISIBLE) != 0) { - continue; - } var bounds = new Rect(handle.frameLeft, handle.frameTop, handle.frameRight, handle.frameBottom); windowInfos.add(new WindowInfo(handle.getWindowToken(), handle.name, bounds, diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 4d0132e46ba7..22b2ec09c034 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -158,6 +158,16 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { mAllCallbacks.remove(callback); // Re-populate the top callback to WM if the removed callback was previously the top one. if (previousTopCallback == callback) { + // We should call onBackCancelled() when an active callback is removed from dispatcher. + if (mProgressAnimator.isBackAnimationInProgress() + && callback instanceof OnBackAnimationCallback) { + // The ProgressAnimator will handle the new topCallback, so we don't want to call + // onBackCancelled() on it. We call immediately the callback instead. + OnBackAnimationCallback animatedCallback = (OnBackAnimationCallback) callback; + animatedCallback.onBackCancelled(); + Log.d(TAG, "The callback was removed while a back animation was in progress, " + + "an onBackCancelled() was dispatched."); + } setTopOnBackInvokedCallback(getTopCallback()); } } diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java index 7cf428ad97a5..56f633fbc6c9 100644 --- a/core/java/com/android/internal/app/AppLocaleCollector.java +++ b/core/java/com/android/internal/app/AppLocaleCollector.java @@ -157,13 +157,13 @@ public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollecto * Get a list of system locale that removes all extensions except for the numbering system. */ @VisibleForTesting - public List<LocaleStore.LocaleInfo> getSystemCurrentLocales() { - List<LocaleStore.LocaleInfo> sysLocales = LocaleStore.getSystemCurrentLocales(); + public Set<LocaleStore.LocaleInfo> getSystemCurrentLocales() { + Set<LocaleStore.LocaleInfo> sysLocales = LocaleStore.getSystemCurrentLocales(); return sysLocales.stream().filter( // For the locale to be added into the suggestion area, its country could not be // empty. info -> info.getLocale().getCountry().length() > 0).collect( - Collectors.toList()); + Collectors.toSet()); } @Override @@ -225,7 +225,10 @@ public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollecto // Add current system language into suggestion list if (!isForCountryMode) { boolean isCurrentLocale, existsInApp, existsInIme; - for (LocaleStore.LocaleInfo localeInfo : getSystemCurrentLocales()) { + // filter out the system locases that are supported by the application. + Set<LocaleStore.LocaleInfo> localeInfoSet = + filterSupportedLocales(getSystemCurrentLocales(), result.mAppSupportedLocales); + for (LocaleStore.LocaleInfo localeInfo : localeInfoSet) { isCurrentLocale = mAppCurrentLocale != null && localeInfo.getLocale().equals(mAppCurrentLocale.getLocale()); // Add the system suggestion flag if the localeInfo exists in mAllAppActiveLocales diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index ace8451722bb..2b39bb4eb7a5 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -165,9 +165,11 @@ import java.util.function.Supplier; import java.util.stream.Collectors; /** - * The Chooser Activity handles intent resolution specifically for sharing intents - - * for example, those generated by @see android.content.Intent#createChooser(Intent, CharSequence). + * This is the legacy ChooserActivity and is not expected to be invoked, it's only here because + * MediaAppSelectorActivity is still depending on it. The actual chooser used by the system is + * at packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java * + * The migration to the new package will be completed in a later release. */ public class ChooserActivity extends ResolverActivity implements ChooserListAdapter.ChooserListCommunicator, diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 88447daf7338..ff3c015cf66f 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -52,6 +52,8 @@ interface IAppOpsService { int checkAudioOperation(int code, int usage, int uid, String packageName); boolean shouldCollectNotes(int opCode); void setCameraAudioRestriction(int mode); + void startWatchingModeWithFlags(int op, String packageName, int flags, + IAppOpsCallback callback); // End of methods also called by native code. // Any new method exposed to native must be added after the last one, do not reorder @@ -110,8 +112,6 @@ interface IAppOpsService { void startWatchingStarted(in int[] ops, IAppOpsStartedCallback callback); void stopWatchingStarted(IAppOpsStartedCallback callback); - void startWatchingModeWithFlags(int op, String packageName, int flags, IAppOpsCallback callback); - void startWatchingNoted(in int[] ops, IAppOpsNotedCallback callback); void stopWatchingNoted(IAppOpsNotedCallback callback); diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index 65394bd87d64..d433cd652606 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -219,10 +219,6 @@ interface IBatteryStats { @EnforcePermission("BATTERY_STATS") long getAwakeTimePlugged(); - @EnforcePermission("BLUETOOTH_CONNECT") - void noteBluetoothOn(int uid, int reason, String packageName); - @EnforcePermission("BLUETOOTH_CONNECT") - void noteBluetoothOff(int uid, int reason, String packageName); @EnforcePermission("UPDATE_DEVICE_STATS") void noteBleScanStarted(in WorkSource ws, boolean isUnoptimized); @EnforcePermission("UPDATE_DEVICE_STATS") diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 904fb665335b..7452daa4908c 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -19,7 +19,11 @@ package com.android.internal.app; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL; import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_CALL_FROM_WORK; import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_OPEN_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_SWITCH_TO_WORK; +import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_WORK_TELEPHONY_CALL_BLOCKED_INFORMATION; +import static android.app.admin.DevicePolicyResources.Strings.Core.MINIRESOLVER_WORK_TELEPHONY_TEXT_BLOCKED_INFORMATION; import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -29,9 +33,11 @@ import static com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE; import android.annotation.Nullable; import android.annotation.TestApi; import android.app.Activity; +import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.AppGlobals; import android.app.admin.DevicePolicyManager; +import android.app.admin.ManagedSubscriptionsPolicy; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.ContentResolver; @@ -41,6 +47,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; +import android.graphics.drawable.Drawable; import android.metrics.LogMaker; import android.os.Build; import android.os.Bundle; @@ -48,6 +55,7 @@ import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.telecom.TelecomManager; import android.util.Slog; import android.view.View; import android.widget.Button; @@ -203,35 +211,124 @@ public class IntentForwarderActivity extends Activity { findViewById(R.id.title_container).setElevation(0); - ImageView icon = findViewById(R.id.icon); PackageManager packageManagerForTargetUser = createContextAsUser(UserHandle.of(targetUserId), /* flags= */ 0) .getPackageManager(); - icon.setImageDrawable(target.loadIcon(packageManagerForTargetUser)); + + ImageView icon = findViewById(R.id.icon); + icon.setImageDrawable( + getAppIcon(target, launchIntent, targetUserId, packageManagerForTargetUser)); View buttonContainer = findViewById(R.id.button_bar_container); buttonContainer.setPadding(0, 0, 0, buttonContainer.getPaddingBottom()); ((TextView) findViewById(R.id.open_cross_profile)).setText( - getOpenInWorkMessage(target.loadLabel(packageManagerForTargetUser))); + getOpenInWorkMessage(launchIntent, target.loadLabel(packageManagerForTargetUser))); // The mini-resolver's negative button is reused in this flow to cancel the intent ((Button) findViewById(R.id.use_same_profile_browser)).setText(R.string.cancel); findViewById(R.id.use_same_profile_browser).setOnClickListener(v -> finish()); + ((Button) findViewById(R.id.button_open)).setText(getOpenInWorkButtonString(launchIntent)); findViewById(R.id.button_open).setOnClickListener(v -> { - startActivityAsCaller(launchIntent, targetUserId); + startActivityAsCaller( + launchIntent, + ActivityOptions.makeCustomAnimation( + getApplicationContext(), + R.anim.activity_open_enter, + R.anim.push_down_out) + .toBundle(), + /* ignoreTargetSecurity= */ false, + targetUserId); finish(); }); + + + View telephonyInfo = findViewById(R.id.miniresolver_info_section); + DevicePolicyManager devicePolicyManager = + getSystemService(DevicePolicyManager.class); + // Additional information section is work telephony specific. Therefore, it is only shown + // for telephony related intents, when all sim subscriptions are in the work profile. + if ((isDialerIntent(launchIntent) || isTextMessageIntent(launchIntent)) + && devicePolicyManager.getManagedSubscriptionsPolicy().getPolicyType() + == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) { + telephonyInfo.setVisibility(View.VISIBLE); + ((TextView) findViewById(R.id.miniresolver_info_section_text)) + .setText(getWorkTelephonyInfoSectionMessage(launchIntent)); + } else { + telephonyInfo.setVisibility(View.GONE); + } + } + + private Drawable getAppIcon( + ResolveInfo target, + Intent launchIntent, + int targetUserId, + PackageManager packageManagerForTargetUser) { + if (isDialerIntent(launchIntent)) { + // The icon for the call intent will be a generic phone icon as the target will be + // the telecom call handler. From the user's perspective, they are being directed + // to the dialer app, so use the icon from that app instead. + TelecomManager telecomManager = + getApplicationContext().getSystemService(TelecomManager.class); + String defaultDialerPackageName = + telecomManager.getDefaultDialerPackage(UserHandle.of(targetUserId)); + try { + return packageManagerForTargetUser + .getApplicationInfo(defaultDialerPackageName, /* flags= */ 0) + .loadIcon(packageManagerForTargetUser); + } catch (PackageManager.NameNotFoundException e) { + // Allow to fall-through to the icon from the target if we can't find the default + // dialer icon. + Slog.w(TAG, "Cannot load icon for default dialer package"); + } + } + return target.loadIcon(packageManagerForTargetUser); } - private String getOpenInWorkMessage(CharSequence targetLabel) { + private int getOpenInWorkButtonString(Intent launchIntent) { + if (isDialerIntent(launchIntent)) { + return R.string.miniresolver_call; + } + if (isTextMessageIntent(launchIntent)) { + return R.string.miniresolver_switch; + } + return R.string.whichViewApplicationLabel; + } + + private String getOpenInWorkMessage(Intent launchIntent, CharSequence targetLabel) { + if (isDialerIntent(launchIntent)) { + return getSystemService(DevicePolicyManager.class).getResources().getString( + MINIRESOLVER_CALL_FROM_WORK, + () -> getString(R.string.miniresolver_call_in_work)); + } + if (isTextMessageIntent(launchIntent)) { + return getSystemService(DevicePolicyManager.class).getResources().getString( + MINIRESOLVER_SWITCH_TO_WORK, + () -> getString(R.string.miniresolver_switch_to_work)); + } return getSystemService(DevicePolicyManager.class).getResources().getString( MINIRESOLVER_OPEN_WORK, () -> getString(R.string.miniresolver_open_work, targetLabel), targetLabel); } + private String getWorkTelephonyInfoSectionMessage(Intent launchIntent) { + if (isDialerIntent(launchIntent)) { + return getSystemService(DevicePolicyManager.class).getResources().getString( + MINIRESOLVER_WORK_TELEPHONY_CALL_BLOCKED_INFORMATION, + () -> getString(R.string.miniresolver_call_information)); + } + if (isTextMessageIntent(launchIntent)) { + return getSystemService(DevicePolicyManager.class).getResources().getString( + MINIRESOLVER_WORK_TELEPHONY_TEXT_BLOCKED_INFORMATION, + () -> getString(R.string.miniresolver_sms_information)); + } + return ""; + } + + + private String getForwardToPersonalMessage() { return getSystemService(DevicePolicyManager.class).getResources().getString( FORWARD_INTENT_TO_PERSONAL, diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java index f4b858f87413..43d263bc8a6d 100644 --- a/core/java/com/android/internal/app/LocaleStore.java +++ b/core/java/com/android/internal/app/LocaleStore.java @@ -32,7 +32,6 @@ import com.android.internal.annotations.VisibleForTesting; import java.io.Serializable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -404,8 +403,8 @@ public class LocaleStore { /** * Returns a list of system locale that removes all extensions except for the numbering system. */ - public static List<LocaleInfo> getSystemCurrentLocales() { - List<LocaleInfo> localeList = new ArrayList<>(); + public static Set<LocaleInfo> getSystemCurrentLocales() { + Set<LocaleInfo> localeList = new HashSet<>(); LocaleList systemLangList = LocaleList.getDefault(); for(int i = 0; i < systemLangList.size(); i++) { Locale sysLocale = getLocaleWithOnlyNumberingSystem(systemLangList.get(i)); diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java index 80f540cca79b..506f19f7719a 100644 --- a/core/java/com/android/internal/jank/FrameTracker.java +++ b/core/java/com/android/internal/jank/FrameTracker.java @@ -275,7 +275,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener } } - @VisibleForTesting + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public Handler getHandler() { return mHandler; } diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 2b9db70c57bd..e530aec2119a 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -772,11 +772,12 @@ public class InteractionJankMonitor { return true; } + @UiThread private void putTracker(@CujType int cuj, @NonNull FrameTracker tracker) { synchronized (mLock) { mRunningTrackers.put(cuj, tracker); if (mDebugOverlay != null) { - mDebugOverlay.onTrackerAdded(cuj, tracker.getViewRoot()); + mDebugOverlay.onTrackerAdded(cuj, tracker); } if (DEBUG) { Log.d(TAG, "Added tracker for " + getNameOfCuj(cuj) @@ -791,6 +792,7 @@ public class InteractionJankMonitor { } } + @UiThread private void removeTracker(@CujType int cuj, int reason) { synchronized (mLock) { mRunningTrackers.remove(cuj); @@ -818,7 +820,7 @@ public class InteractionJankMonitor { SETTINGS_DEBUG_OVERLAY_ENABLED_KEY, DEFAULT_DEBUG_OVERLAY_ENABLED); if (debugOverlayEnabled && mDebugOverlay == null) { - mDebugOverlay = new InteractionMonitorDebugOverlay(mDebugBgColor, mDebugYOffset); + mDebugOverlay = new InteractionMonitorDebugOverlay(mLock, mDebugBgColor, mDebugYOffset); } else if (!debugOverlayEnabled && mDebugOverlay != null) { mDebugOverlay.dispose(); mDebugOverlay = null; diff --git a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java index 99b9f2f35fd4..ef7944c21ad2 100644 --- a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java +++ b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java @@ -19,25 +19,30 @@ package com.android.internal.jank; import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL; import android.annotation.ColorInt; +import android.annotation.UiThread; import android.app.ActivityThread; import android.content.Context; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RecordingCanvas; import android.graphics.Rect; +import android.os.Handler; import android.os.Trace; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.WindowCallbacks; +import com.android.internal.annotations.GuardedBy; import com.android.internal.jank.FrameTracker.Reasons; import com.android.internal.jank.InteractionJankMonitor.CujType; /** * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window * associated with one of the CUJs being tracked. There's no guarantee which window it will - * draw to. NOTE: sometimes the CUJ names will remain displayed on the screen longer than they - * are actually running. + * draw to. Traces that use the debug overlay should not be used for performance analysis. + * <p> + * To enable the overlay, run the following: <code>adb shell device_config put + * interaction_jank_monitor debug_overlay_enabled true</code> * <p> * CUJ names will be drawn as follows: * <ul> @@ -45,12 +50,16 @@ import com.android.internal.jank.InteractionJankMonitor.CujType; * <li> Grey text indicates the CUJ ended normally and is no longer running * <li> Red text with a strikethrough indicates the CUJ was canceled or ended abnormally * </ul> + * @hide */ class InteractionMonitorDebugOverlay implements WindowCallbacks { private static final int REASON_STILL_RUNNING = -1000; + private final Object mLock; // Sparse array where the key in the CUJ and the value is the session status, or null if // it's currently running + @GuardedBy("mLock") private final SparseIntArray mRunningCujs = new SparseIntArray(); + private Handler mHandler = null; private FrameTracker.ViewRootWrapper mViewRoot = null; private final Paint mDebugPaint; private final Paint.FontMetrics mDebugFontMetrics; @@ -59,8 +68,10 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { private final int mBgColor; private final double mYOffset; private final String mPackageName; + private static final String TRACK_NAME = "InteractionJankMonitor"; - InteractionMonitorDebugOverlay(@ColorInt int bgColor, double yOffset) { + InteractionMonitorDebugOverlay(Object lock, @ColorInt int bgColor, double yOffset) { + mLock = lock; mBgColor = bgColor; mYOffset = yOffset; mDebugPaint = new Paint(); @@ -70,18 +81,30 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { mPackageName = context.getPackageName(); } + @UiThread void dispose() { - if (mViewRoot != null) { - mViewRoot.removeWindowCallbacks(this); + if (mViewRoot != null && mHandler != null) { + mHandler.runWithScissors(() -> mViewRoot.removeWindowCallbacks(this), + InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); forceRedraw(); } + mHandler = null; mViewRoot = null; + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0); } - private boolean attachViewRootIfNeeded(FrameTracker.ViewRootWrapper viewRoot) { + @UiThread + private boolean attachViewRootIfNeeded(FrameTracker tracker) { + FrameTracker.ViewRootWrapper viewRoot = tracker.getViewRoot(); if (mViewRoot == null && viewRoot != null) { + // Add a trace marker so we can identify traces that were captured while the debug + // overlay was enabled. Traces that use the debug overlay should NOT be used for + // performance analysis. + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, "DEBUG_OVERLAY_DRAW", 0); + mHandler = tracker.getHandler(); mViewRoot = viewRoot; - viewRoot.addWindowCallbacks(this); + mHandler.runWithScissors(() -> viewRoot.addWindowCallbacks(this), + InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); forceRedraw(); return true; } @@ -115,52 +138,61 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { } } + @UiThread private void forceRedraw() { - if (mViewRoot != null) { - mViewRoot.requestInvalidateRootRenderNode(); - mViewRoot.getView().invalidate(); + if (mViewRoot != null && mHandler != null) { + mHandler.runWithScissors(() -> { + mViewRoot.requestInvalidateRootRenderNode(); + mViewRoot.getView().invalidate(); + }, InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); } } + @UiThread void onTrackerRemoved(@CujType int removedCuj, @Reasons int reason, SparseArray<FrameTracker> runningTrackers) { - mRunningCujs.put(removedCuj, reason); - // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended - if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { - mRunningCujs.clear(); - dispose(); - } else { - boolean needsNewViewRoot = true; - if (mViewRoot != null) { - // Check to see if this viewroot is still associated with one of the running - // trackers - for (int i = 0; i < runningTrackers.size(); i++) { - if (mViewRoot.equals( - runningTrackers.valueAt(i).getViewRoot())) { - needsNewViewRoot = false; - break; + synchronized (mLock) { + mRunningCujs.put(removedCuj, reason); + // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended + if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { + mRunningCujs.clear(); + dispose(); + } else { + boolean needsNewViewRoot = true; + if (mViewRoot != null) { + // Check to see if this viewroot is still associated with one of the running + // trackers + for (int i = 0; i < runningTrackers.size(); i++) { + if (mViewRoot.equals( + runningTrackers.valueAt(i).getViewRoot())) { + needsNewViewRoot = false; + break; + } } } - } - if (needsNewViewRoot) { - dispose(); - for (int i = 0; i < runningTrackers.size(); i++) { - if (attachViewRootIfNeeded(runningTrackers.valueAt(i).getViewRoot())) { - break; + if (needsNewViewRoot) { + dispose(); + for (int i = 0; i < runningTrackers.size(); i++) { + if (attachViewRootIfNeeded(runningTrackers.valueAt(i))) { + break; + } } + } else { + forceRedraw(); } - } else { - forceRedraw(); } } } - void onTrackerAdded(@CujType int addedCuj, FrameTracker.ViewRootWrapper viewRoot) { - // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ - // is still running - mRunningCujs.put(addedCuj, REASON_STILL_RUNNING); - attachViewRootIfNeeded(viewRoot); - forceRedraw(); + @UiThread + void onTrackerAdded(@CujType int addedCuj, FrameTracker tracker) { + synchronized (mLock) { + // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ + // is still running + mRunningCujs.put(addedCuj, REASON_STILL_RUNNING); + attachViewRootIfNeeded(tracker); + forceRedraw(); + } } @Override @@ -188,7 +220,6 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { @Override public void onPostDraw(RecordingCanvas canvas) { - Trace.beginSection("InteractionJankMonitor#drawDebug"); final int padding = dipToPx(5); final int h = canvas.getHeight(); final int w = canvas.getWidth(); @@ -235,6 +266,5 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { canvas.translate(0, cujNameTextHeight); canvas.drawText(cujName, 0, 0, mDebugPaint); } - Trace.endSection(); } } diff --git a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java index 6fa6fa5d37f3..3ba4ea55b5d3 100644 --- a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java +++ b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java @@ -118,6 +118,9 @@ public class AnrLatencyTracker implements AutoCloseable { private boolean mIsSkipped = false; private boolean mCopyingFirstPidSucceeded = false; + private long mPreDumpIfLockTooSlowStartUptime; + private long mPreDumpIfLockTooSlowDuration = 0; + private final int mAnrRecordPlacedOnQueueCookie = sNextAnrRecordPlacedOnQueueCookieGenerator.incrementAndGet(); @@ -401,6 +404,17 @@ public class AnrLatencyTracker implements AutoCloseable { Trace.traceCounter(TRACE_TAG_ACTIVITY_MANAGER, "anrRecordsQueueSize", queueSize); } + /** Records the start of AnrController#preDumpIfLockTooSlow. */ + public void preDumpIfLockTooSlowStarted() { + mPreDumpIfLockTooSlowStartUptime = getUptimeMillis(); + } + + /** Records the end of AnrController#preDumpIfLockTooSlow. */ + public void preDumpIfLockTooSlowEnded() { + mPreDumpIfLockTooSlowDuration += + getUptimeMillis() - mPreDumpIfLockTooSlowStartUptime; + } + /** Records a skipped ANR in ProcessErrorStateRecord#appNotResponding. */ public void anrSkippedProcessErrorStateRecordAppNotResponding() { anrSkipped("appNotResponding"); @@ -415,7 +429,7 @@ public class AnrLatencyTracker implements AutoCloseable { * Returns latency data as a comma separated value string for inclusion in ANR report. */ public String dumpAsCommaSeparatedArrayWithHeader() { - return "DurationsV3: " + mAnrTriggerUptime + return "DurationsV4: " + mAnrTriggerUptime /* triggering_to_app_not_responding_duration = */ + "," + (mAppNotRespondingStartUptime - mAnrTriggerUptime) /* app_not_responding_duration = */ @@ -464,6 +478,8 @@ public class AnrLatencyTracker implements AutoCloseable { + "," + mEarlyDumpStatus /* copying_first_pid_succeeded = */ + "," + (mCopyingFirstPidSucceeded ? 1 : 0) + /* preDumpIfLockTooSlow_duration = */ + + "," + mPreDumpIfLockTooSlowDuration + "\n\n"; } diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 21bdf099f18d..e5d567607199 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -652,9 +652,7 @@ int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool p char saveResolvedClassesDelayMsOptsBuf[ sizeof("-Xps-save-resolved-classes-delay-ms:")-1 + PROPERTY_VALUE_MAX]; char profileMinSavePeriodOptsBuf[sizeof("-Xps-min-save-period-ms:")-1 + PROPERTY_VALUE_MAX]; - char profileMinFirstSaveOptsBuf[ - sizeof("-Xps-min-first-save-ms:")-1 + PROPERTY_VALUE_MAX]; - char madviseRandomOptsBuf[sizeof("-XX:MadviseRandomAccess:")-1 + PROPERTY_VALUE_MAX]; + char profileMinFirstSaveOptsBuf[sizeof("-Xps-min-first-save-ms:") - 1 + PROPERTY_VALUE_MAX]; char madviseWillNeedFileSizeVdex[ sizeof("-XMadviseWillNeedVdexFileSize:")-1 + PROPERTY_VALUE_MAX]; char madviseWillNeedFileSizeOdex[ @@ -866,13 +864,8 @@ int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool p jitprithreadweightOptBuf, "-Xjitprithreadweight:"); - parseRuntimeOption("dalvik.vm.jittransitionweight", - jittransitionweightOptBuf, + parseRuntimeOption("dalvik.vm.jittransitionweight", jittransitionweightOptBuf, "-Xjittransitionweight:"); - /* - * Madvise related options. - */ - parseRuntimeOption("dalvik.vm.madvise-random", madviseRandomOptsBuf, "-XX:MadviseRandomAccess:"); /* * Use default platform configuration as limits for madvising, diff --git a/core/proto/android/companion/telecom.proto b/core/proto/android/companion/telecom.proto index b90067dae2a2..700baa1f4ee5 100644 --- a/core/proto/android/companion/telecom.proto +++ b/core/proto/android/companion/telecom.proto @@ -42,6 +42,9 @@ message Telecom { ONGOING = 2; ON_HOLD = 3; RINGING_SILENCED = 4; + AUDIO_PROCESSING = 5; + RINGING_SIMULATED = 6; + DISCONNECTED = 7; } Status status = 3; @@ -89,8 +92,6 @@ message Telecom { END = 6; PUT_ON_HOLD = 7; TAKE_OFF_HOLD = 8; - REJECT_AND_BLOCK = 9; - IGNORE = 10; } // The list of active calls. diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto index f87d9109e7f8..17ca7c80707c 100644 --- a/core/proto/android/service/notification.proto +++ b/core/proto/android/service/notification.proto @@ -360,3 +360,11 @@ message DNDPolicyProto { optional ConversationType allow_conversations_from = 19; } + +// Enum identifying the type of rule that changed; values set to match ones used in the +// DNDStateChanged proto. +enum RuleType { + RULE_TYPE_UNKNOWN = 0; + RULE_TYPE_MANUAL = 1; + RULE_TYPE_AUTOMATIC = 2; +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index af08e03f313f..2f9f6ae3f3c4 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -863,6 +863,7 @@ android:label="@string/permlab_readContacts" android:description="@string/permdesc_readContacts" android:protectionLevel="dangerous" /> + <uses-permission android:name="android.permission.READ_CONTACTS" /> <!-- Allows an application to write the user's contacts data. <p>Protection level: dangerous @@ -6039,6 +6040,7 @@ <p>Not for use by third-party applications. --> <permission android:name="android.permission.CALL_PRIVILEGED" android:protectionLevel="signature|privileged" /> + <uses-permission android:name="android.permission.CALL_PRIVILEGED" /> <!-- @SystemApi Allows an application to perform CDMA OTA provisioning @hide --> <permission android:name="android.permission.PERFORM_CDMA_PROVISIONING" @@ -7690,22 +7692,6 @@ android:defaultToDeviceProtectedStorage="true" android:forceQueryable="true" android:directBootAware="true"> - <activity android:name="com.android.internal.app.ChooserActivity" - android:theme="@style/Theme.DeviceDefault.Chooser" - android:finishOnCloseSystemDialogs="true" - android:excludeFromRecents="true" - android:documentLaunchMode="never" - android:relinquishTaskIdentity="true" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden" - android:process=":ui" - android:exported="true" - android:visibleToInstantApps="true"> - <intent-filter android:priority="100"> - <action android:name="android.intent.action.CHOOSER" /> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.VOICE" /> - </intent-filter> - </activity> <activity android:name="com.android.internal.accessibility.dialog.AccessibilityShortcutChooserActivity" android:exported="false" android:theme="@style/Theme.DeviceDefault.Dialog.Alert.DayNight" @@ -8231,7 +8217,7 @@ </service> <service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncConnectionService" - android:permission="android.permission.BIND_CONNECTION_SERVICE" + android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" android:exported="true"> <intent-filter> <action android:name="android.telecom.ConnectionService"/> diff --git a/core/res/res/layout/autofill_fill_dialog.xml b/core/res/res/layout/autofill_fill_dialog.xml index 2e65800d5abb..d1a4935633cb 100644 --- a/core/res/res/layout/autofill_fill_dialog.xml +++ b/core/res/res/layout/autofill_fill_dialog.xml @@ -35,10 +35,10 @@ <ImageView android:id="@+id/autofill_service_icon" - android:scaleType="fitStart" + android:scaleType="fitCenter" android:visibility="gone" - android:layout_width="@dimen/autofill_dialog_icon_size" - android:layout_height="@dimen/autofill_dialog_icon_size"/> + android:layout_height="@dimen/autofill_dialog_icon_max_height" + android:layout_width="fill_parent"/> <LinearLayout android:id="@+id/autofill_dialog_header" diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml index 3c0b789dbe6f..85529d6a4b3b 100644 --- a/core/res/res/layout/autofill_save.xml +++ b/core/res/res/layout/autofill_save.xml @@ -40,10 +40,10 @@ <ImageView android:id="@+id/autofill_save_icon" - android:scaleType="fitStart" + android:scaleType="fitCenter" android:layout_gravity="center" - android:layout_width="@dimen/autofill_save_icon_size" - android:layout_height="@dimen/autofill_save_icon_size"/> + android:layout_height="@dimen/autofill_save_icon_max_height" + android:layout_width="fill_parent"/> <TextView android:id="@+id/autofill_save_title" @@ -60,7 +60,6 @@ android:id="@+id/autofill_save_custom_subtitle" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/autofill_save_scroll_view_top_margin" android:visibility="gone"/> </LinearLayout> diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml index 1ad3acd7a3ea..db0ea547fbd5 100644 --- a/core/res/res/layout/miniresolver.xml +++ b/core/res/res/layout/miniresolver.xml @@ -63,6 +63,37 @@ android:textColor="?android:textColorPrimary" /> </RelativeLayout> + <!-- Additional information section, currently only shown when redirecting to Telephony apps --> + <LinearLayout + android:id="@+id/miniresolver_info_section" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingHorizontal="24dp" + android:paddingBottom="48dp" + android:visibility="gone" + android:background="?attr/colorBackground" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/miniresolver_info_section_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="8dp" + android:src="@drawable/ic_info_outline_24" + android:tint="?android:textColorSecondary" + /> + + <TextView + android:id="@+id/miniresolver_info_section_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:paddingEnd="8dp" + android:textSize="14sp" + android:textAllCaps="false" + android:textColor="?android:textColorSecondary" + /> + </LinearLayout> <LinearLayout android:id="@+id/button_bar_container" @@ -83,7 +114,7 @@ android:orientation="horizontal" android:layoutDirection="locale" android:measureWithLargestChild="true" - android:paddingHorizontal="16dp" + android:layout_marginHorizontal="24dp" android:paddingBottom="2dp" android:elevation="@dimen/resolver_elevation"> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 1bbe8eeaf37f..b7d088bd3d82 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1618,7 +1618,8 @@ {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground service with this type will require permission {@link android.Manifest.permission#FOREGROUND_SERVICE_PHONE_CALL} and - {@link android.Manifest.permission#MANAGE_OWN_CALLS}. + {@link android.Manifest.permission#MANAGE_OWN_CALLS} or holding the default + {@link android.app.role.RoleManager#ROLE_DIALER dialer role}. --> <flag name="phoneCall" value="0x04" /> <!-- GPS, map, navigation location update. @@ -1796,12 +1797,6 @@ --> <attr name="attributionTags" format="string" /> - <!-- Default value <code>true</code> allows an installer to enable update - ownership enforcement for this package via {@link - android.content.pm.PackageInstaller.SessionParams#setRequestUpdateOwnership} - during initial installation. This overrides the installer's use of {@link - android.content.pm.PackageInstaller.SessionParams#setRequestUpdateOwnership}. - --> <attr name="allowUpdateOwnership" format="boolean" /> <!-- The <code>manifest</code> tag is the root of an @@ -1841,7 +1836,6 @@ <attr name="isSplitRequired" /> <attr name="requiredSplitTypes" /> <attr name="splitTypes" /> - <attr name="allowUpdateOwnership" /> </declare-styleable> <!-- The <code>application</code> tag describes application-level components diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 23e3139c3d3a..00f8db0d9264 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3718,6 +3718,10 @@ magnification settings and adjust the default magnification capability. --> <bool name="config_magnification_area">true</bool> + <!-- The default value for always on magnification feature flag if the remote feature + flag does not exist --> + <bool name="config_magnification_always_on_enabled">true</bool> + <!-- If true, the display will be shifted around in ambient mode. --> <bool name="config_enableBurnInProtection">false</bool> @@ -5177,10 +5181,11 @@ <item>0,0,1.0,0,1</item> <item>1,1,1.0,0,1</item> <item>1,1,1.0,.4,1</item> + <item>1,1,1.0,.15,15</item> </string-array> <!-- The integer index of the selected option in config_udfps_touch_detection_options --> - <integer name="config_selected_udfps_touch_detection">2</integer> + <integer name="config_selected_udfps_touch_detection">3</integer> <!-- An array of arrays of side fingerprint sensor properties relative to each display. Note: this value is temporary and is expected to be queried directly diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index bc0af12e9569..24da59a378f8 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -874,6 +874,7 @@ <dimen name="autofill_elevation">32dp</dimen> <dimen name="autofill_save_inner_padding">16dp</dimen> <dimen name="autofill_save_icon_size">32dp</dimen> + <dimen name="autofill_save_icon_max_height">56dp</dimen> <dimen name="autofill_save_title_start_padding">8dp</dimen> <dimen name="autofill_save_scroll_view_top_margin">16dp</dimen> <dimen name="autofill_save_button_bar_padding">16dp</dimen> @@ -882,19 +883,18 @@ <!-- Max height of the the autofill save custom subtitle as a fraction of the screen width/height --> <dimen name="autofill_save_custom_subtitle_max_height">20%</dimen> - <!-- Max (absolute) dimensions (both width and height) of autofill service icon on autofill save affordance. - NOTE: the actual displayed size might is actually smaller than this and is hardcoded in the - autofill_save.xml layout; this dimension is just used to avoid a crash in the UI (if the icon provided - by the autofill service metadata is bigger than these dimentionsit will not be displayed). - --> - <dimen name="autofill_save_icon_max_size">300dp</dimen> - <!-- Maximum number of datasets that are visible in the UX picker without scrolling --> <integer name="autofill_max_visible_datasets">3</integer> - <!-- Size of an icon in the Autolfill fill dialog --> + <!-- Size of an icon in the Autofill fill dialog --> <dimen name="autofill_dialog_icon_size">32dp</dimen> + <!-- The max height of an icon in the Autofill fill dialog. --> + <dimen name="autofill_dialog_icon_max_height">56dp</dimen> + + <!-- The max width of the Autofill fill dialog. --> + <dimen name="autofill_dialog_max_width">640dp</dimen> + <!-- Size of a slice shortcut view --> <dimen name="slice_shortcut_size">56dp</dimen> <!-- Size of action icons in a slice --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index faaa2e8cc0f5..91fbf6bb9f06 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -861,10 +861,10 @@ <!-- "Switch" is a verb; it means to change user profile by tapping another user profile name. --> <string name="managed_profile_label">Switch to work profile</string> - <!-- "Switch" is a verb; it means to change user profile by tapping another user profile name. --> + <!-- "Switch" is a verb; it means to change user profile by switching to an app in the personal profile. --> <string name="user_owner_app_label">Switch to personal <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string> - <!-- "Switch" is a verb; it means to change user profile by tapping another user profile name. --> + <!-- "Switch" is a verb; it means to change user profile by switching to an app in the work profile. --> <string name="managed_profile_app_label">Switch to work <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string> <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. --> @@ -5392,11 +5392,11 @@ <string name="app_streaming_blocked_message" product="tablet">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your tablet instead.</string> <!-- Message shown when an app is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message" product="default">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your phone instead.</string> - <!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> + <!-- Message shown when an app being streamed to another device requests authentication such as via the biometrics API, and the user needs to complete the on their device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv">This app is requesting additional security. Try on your Android TV device instead.</string> - <!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> + <!-- Message shown when an app being streamed to another device requests authentication such as via the biometrics API, and the user needs to complete the on their device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet">This app is requesting additional security. Try on your tablet instead.</string> - <!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> + <!-- Message shown when an app being streamed to another device requests authentication such as via the biometrics API, and the user needs to complete the on their device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default">This app is requesting additional security. Try on your phone instead.</string> <!-- Message shown when the settings is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] --> <string name="app_streaming_blocked_message_for_settings_dialog" product="tv">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g>. Try on your Android TV device instead.</string> @@ -5907,10 +5907,24 @@ <string name="miniresolver_open_in_personal">Open in personal <xliff:g id="app" example="YouTube">%s</xliff:g>?</string> <!-- Dialog title. User must choose between opening content in a cross-profile app or same-profile browser. [CHAR LIMIT=NONE] --> <string name="miniresolver_open_in_work">Open in work <xliff:g id="app" example="YouTube">%s</xliff:g>?</string> + <!-- Dialog title. User must place the phone call in the other profile, or cancel. [CHAR LIMIT=NONE] --> + <string name="miniresolver_call_in_work">Call from work app?</string> + <!-- Dialog title. User much choose between opening content in a cross-profile app or cancelling. [CHAR LIMIT=NONE] --> + <string name="miniresolver_switch_to_work">Switch to work app?</string> + <!-- Dialog text. Shown when the user is unable to make a phone call from a personal app due to restrictions set + by their organization, and so must switch to a work app or cancel. [CHAR LIMIT=NONE] --> + <string name="miniresolver_call_information">Your organization only allows you to make calls from work apps</string> + <!-- Dialog text. Shown when the user is unable to send a text message from a personal app due to restrictions set + by their organization, and so must switch to a work app or cancel. [CHAR LIMIT=NONE] --> + <string name="miniresolver_sms_information">Your organization only allows you to send messages from work apps</string> <!-- Button option. Open the link in the personal browser. [CHAR LIMIT=NONE] --> <string name="miniresolver_use_personal_browser">Use personal browser</string> <!-- Button option. Open the link in the work browser. [CHAR LIMIT=NONE] --> <string name="miniresolver_use_work_browser">Use work browser</string> + <!-- Button option. Action to place a phone call. [CHAR LIMIT=NONE] --> + <string name="miniresolver_call">Call</string> + <!-- Button option. Action to open an app in the work profile. [CHAR LIMIT=NONE] --> + <string name="miniresolver_switch">Switch</string> <!-- Icc depersonalization related strings --> <!-- Label text for PIN entry widget on SIM Network Depersonalization panel [CHAR LIMIT=none] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 73e3b417f67a..f35e32b126ba 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -675,8 +675,6 @@ <java-symbol type="string" name="contentServiceSyncNotificationTitle" /> <java-symbol type="string" name="contentServiceTooManyDeletesNotificationDesc" /> <java-symbol type="string" name="csd_dose_reached_warning" /> - <java-symbol type="string" name="csd_dose_repeat_warning" /> - <java-symbol type="string" name="csd_entering_RS2_warning" /> <java-symbol type="string" name="csd_momentary_exposure_warning" /> <java-symbol type="string" name="date_and_time" /> <java-symbol type="string" name="date_picker_decrement_day_button" /> @@ -1579,6 +1577,14 @@ <java-symbol type="string" name="miniresolver_open_work" /> <java-symbol type="string" name="miniresolver_use_personal_browser" /> <java-symbol type="string" name="miniresolver_use_work_browser" /> + <java-symbol type="string" name="miniresolver_call_in_work" /> + <java-symbol type="string" name="miniresolver_switch_to_work" /> + <java-symbol type="string" name="miniresolver_call" /> + <java-symbol type="string" name="miniresolver_switch" /> + <java-symbol type="string" name="miniresolver_call_information" /> + <java-symbol type="string" name="miniresolver_sms_information" /> + <java-symbol type="id" name="miniresolver_info_section" /> + <java-symbol type="id" name="miniresolver_info_section_text" /> <java-symbol type="id" name="button_open" /> <java-symbol type="id" name="use_same_profile_browser" /> @@ -3654,8 +3660,8 @@ <java-symbol type="dimen" name="autofill_dataset_picker_max_width"/> <java-symbol type="dimen" name="autofill_dataset_picker_max_height"/> <java-symbol type="dimen" name="autofill_save_custom_subtitle_max_height"/> - <java-symbol type="dimen" name="autofill_save_icon_max_size"/> <java-symbol type="integer" name="autofill_max_visible_datasets" /> + <java-symbol type="dimen" name="autofill_dialog_max_width" /> <java-symbol type="style" name="Theme.DeviceDefault.Autofill" /> <java-symbol type="style" name="Theme.DeviceDefault.Light.Autofill" /> @@ -4530,6 +4536,7 @@ <java-symbol type="string" name="dismiss_action" /> <java-symbol type="bool" name="config_magnification_area" /> + <java-symbol type="bool" name="config_magnification_always_on_enabled" /> <java-symbol type="bool" name="config_trackerAppNeedsPermissions"/> <!-- FullScreenMagnification thumbnail --> @@ -5011,7 +5018,7 @@ <java-symbol type="bool" name="config_batteryStatsResetOnUnplugHighBatteryLevel" /> <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" /> - + <java-symbol name="materialColorOnSecondaryFixedVariant" type="attr"/> <java-symbol name="materialColorOnTertiaryFixedVariant" type="attr"/> <java-symbol name="materialColorSurfaceContainerLowest" type="attr"/> diff --git a/core/res/res/xml/irq_device_map.xml b/core/res/res/xml/irq_device_map.xml index 8b3667e9f2a9..2f894b99d603 100644 --- a/core/res/res/xml/irq_device_map.xml +++ b/core/res/res/xml/irq_device_map.xml @@ -28,6 +28,8 @@ - Wifi: Use this to denote network traffic that uses the wifi transport. - Sound_trigger: Use this to denote sound phrase detection, like the ones supported by SoundTriggerManager. + - Sensor: Use this to denote wakeups due to sensor events. + - Cellular_data: Use this to denote network traffic on the cellular transport. The overlay should use tags <device> and <subsystem> to describe this mapping in the following way: diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index 4cccf8e49890..c1deba3288e5 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -78,6 +78,7 @@ <uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.READ_WRITE_SYNC_DISABLED_MODE_CONFIG" /> <uses-permission android:name="android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG" /> + <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> diff --git a/core/tests/coretests/src/android/app/backup/BackupManagerMonitorWrapperTest.java b/core/tests/coretests/src/android/app/backup/BackupManagerMonitorWrapperTest.java new file mode 100644 index 000000000000..1f5e0cf52d5c --- /dev/null +++ b/core/tests/coretests/src/android/app/backup/BackupManagerMonitorWrapperTest.java @@ -0,0 +1,66 @@ +/* + * 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.app.backup; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.os.Bundle; +import android.platform.test.annotations.Presubmit; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class BackupManagerMonitorWrapperTest { + @Mock + private BackupManagerMonitor mMonitor; + private BackupManagerMonitorWrapper mMonitorWrapper; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testOnEvent_propagatesToMonitor() throws Exception { + mMonitorWrapper = new BackupManagerMonitorWrapper(mMonitor); + Bundle eventBundle = new Bundle(); + + mMonitorWrapper.onEvent(eventBundle); + + verify(mMonitor, times(/* wantedNumberOfInvocations */ 1)).onEvent(eq(eventBundle)); + } + + @Test + public void testOnEvent_nullMonitor_eventIsIgnored() throws Exception { + mMonitorWrapper = new BackupManagerMonitorWrapper(/* monitor */ null); + + mMonitorWrapper.onEvent(new Bundle()); + + verify(mMonitor, never()).onEvent(any()); + } +} diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java b/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java index c6bb07b17fd4..6d99e94a3656 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterActivityTest.java @@ -45,7 +45,6 @@ import com.android.frameworks.coretests.R; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.junit.After; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -67,22 +66,9 @@ public class FontScaleConverterActivityTest { @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); - private float mOriginalFontScale = Float.MIN_VALUE; - - @Before - public void setup() { - mOriginalFontScale = Settings.System.getFloat( - InstrumentationRegistry.getInstrumentation().getContext().getContentResolver(), - Settings.System.FONT_SCALE, - Float.MIN_VALUE - ); - } - @After public void teardown() { - if (mOriginalFontScale != Float.MIN_VALUE) { - setSystemFontScale(mOriginalFontScale); - } + restoreSystemFontScaleToDefault(); } @IwTest(focusArea = "accessibility") @@ -160,6 +146,28 @@ public class FontScaleConverterActivityTest { ); } + private static void restoreSystemFontScaleToDefault() { + ShellIdentityUtils.invokeWithShellPermissions(() -> { + // TODO(b/279083734): would use Settings.System.resetToDefaults() if it existed + Settings.System.putString( + InstrumentationRegistry.getInstrumentation() + .getContext() + .getContentResolver(), + Settings.System.FONT_SCALE, + null, + /* overrideableByRestore= */ true); + }); + + PollingCheck.waitFor( + /* timeout= */ 5000, + () -> InstrumentationRegistry.getInstrumentation() + .getContext() + .getResources() + .getConfiguration() + .fontScale == 1 + ); + } + private Matcher<View> withTextSizeInRange(float sizeStartPx, float sizeEndPx) { return new BoundedMatcher<>(TextView.class) { private static final float TOLERANCE = 0.05f; diff --git a/core/tests/coretests/src/android/view/InsetsSourceTest.java b/core/tests/coretests/src/android/view/InsetsSourceTest.java index 55680abb159d..9595332afc6c 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceTest.java @@ -229,20 +229,36 @@ public class InsetsSourceTest { @Test public void testGetIndex() { - for (int index = 0; index < 2048; index++) { - for (int type = FIRST; type <= LAST; type = type << 1) { - final int id = InsetsSource.createId(null, index, type); - assertEquals(index, InsetsSource.getIndex(id)); + // Here doesn't iterate all the owners, or the test cannot be done before timeout. + for (int owner = 0; owner < 100; owner++) { + for (int index = 0; index < 2048; index++) { + for (int type = FIRST; type <= LAST; type = type << 1) { + final int id = InsetsSource.createId(owner, index, type); + final int indexFromId = InsetsSource.getIndex(id); + assertEquals("index and indexFromId must be the same. id=" + id + + ", owner=" + owner + + ", index=" + index + + ", type=" + type + + ", indexFromId=" + indexFromId + ".", index, indexFromId); + } } } } @Test public void testGetType() { - for (int index = 0; index < 2048; index++) { - for (int type = FIRST; type <= LAST; type = type << 1) { - final int id = InsetsSource.createId(null, index, type); - assertEquals(type, InsetsSource.getType(id)); + // Here doesn't iterate all the owners, or the test cannot be done before timeout. + for (int owner = 0; owner < 100; owner++) { + for (int index = 0; index < 2048; index++) { + for (int type = FIRST; type <= LAST; type = type << 1) { + final int id = InsetsSource.createId(owner, index, type); + final int typeFromId = InsetsSource.getType(id); + assertEquals("type and typeFromId must be the same. id=" + id + + ", owner=" + owner + + ", index=" + index + + ", type=" + type + + ", typeFromId=" + typeFromId + ".", type, typeFromId); + } } } } diff --git a/core/tests/coretests/src/android/view/SurfaceControlRegistryTests.java b/core/tests/coretests/src/android/view/SurfaceControlRegistryTests.java new file mode 100644 index 000000000000..d10ba7ccbac4 --- /dev/null +++ b/core/tests/coretests/src/android/view/SurfaceControlRegistryTests.java @@ -0,0 +1,173 @@ +/* + * 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.view; + +import static android.Manifest.permission.READ_FRAME_BUFFER; +import static android.content.pm.PackageManager.PERMISSION_DENIED; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.platform.test.annotations.Presubmit; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.PrintWriter; +import java.util.WeakHashMap; + +/** + * Class for testing {@link SurfaceControlRegistry}. + * + * Build/Install/Run: + * atest FrameworksCoreTests:android.view.SurfaceControlRegistryTests + */ +@Presubmit +@RunWith(AndroidJUnit4.class) +public class SurfaceControlRegistryTests { + + @BeforeClass + public static void setUpOnce() { + SurfaceControlRegistry.createProcessInstance(getInstrumentation().getTargetContext()); + } + + @AfterClass + public static void tearDownOnce() { + SurfaceControlRegistry.destroyProcessInstance(); + } + + @Test + public void testRequiresPermissionToCreateProcessInstance() { + try { + Context ctx = mock(Context.class); + doReturn(PERMISSION_DENIED).when(ctx).checkSelfPermission(eq(READ_FRAME_BUFFER)); + SurfaceControlRegistry.createProcessInstance(ctx); + fail("Expected SecurityException due to missing permission"); + } catch (SecurityException se) { + // Expected failure + } catch (Exception e) { + fail("Unexpected exception: " + e); + } + } + + @Test + public void testCreateReleaseSurfaceControl() { + int hash0 = SurfaceControlRegistry.getProcessInstance().hashCode(); + SurfaceControl sc = buildTestSurface(); + assertNotEquals(hash0, SurfaceControlRegistry.getProcessInstance().hashCode()); + sc.release(); + assertEquals(hash0, SurfaceControlRegistry.getProcessInstance().hashCode()); + } + + @Test + public void testCreateReleaseMultipleSurfaceControls() { + int hash0 = SurfaceControlRegistry.getProcessInstance().hashCode(); + SurfaceControl sc1 = buildTestSurface(); + int hash1 = SurfaceControlRegistry.getProcessInstance().hashCode(); + assertNotEquals(hash0, hash1); + SurfaceControl sc2 = buildTestSurface(); + int hash1_2 = SurfaceControlRegistry.getProcessInstance().hashCode(); + assertNotEquals(hash0, hash1_2); + assertNotEquals(hash1, hash1_2); + // Release in inverse order to verify hashes still differ + sc1.release(); + int hash2 = SurfaceControlRegistry.getProcessInstance().hashCode(); + assertNotEquals(hash0, hash2); + sc2.release(); + assertEquals(hash0, SurfaceControlRegistry.getProcessInstance().hashCode()); + } + + @Test + public void testThresholds() { + SurfaceControlRegistry registry = SurfaceControlRegistry.getProcessInstance(); + TestReporter reporter = new TestReporter(); + registry.setReportingThresholds(4 /* max */, 2 /* reset */, reporter); + + // Exceed the threshold ensure the callback is made + SurfaceControl sc1 = buildTestSurface(); + SurfaceControl sc2 = buildTestSurface(); + SurfaceControl sc3 = buildTestSurface(); + SurfaceControl sc4 = buildTestSurface(); + reporter.assertMaxThresholdExceededCallCount(1); + reporter.assertLastReportedSetEquals(sc1, sc2, sc3, sc4); + + // Create a few more, ensure we don't report again for the time being + SurfaceControl sc5 = buildTestSurface(); + SurfaceControl sc6 = buildTestSurface(); + reporter.assertMaxThresholdExceededCallCount(1); + reporter.assertLastReportedSetEquals(sc1, sc2, sc3, sc4); + + // Release down to the reset threshold + sc1.release(); + sc2.release(); + sc3.release(); + sc4.release(); + + // Create a few more to hit the max threshold again + SurfaceControl sc7 = buildTestSurface(); + SurfaceControl sc8 = buildTestSurface(); + reporter.assertMaxThresholdExceededCallCount(2); + reporter.assertLastReportedSetEquals(sc5, sc6, sc7, sc8); + } + + private SurfaceControl buildTestSurface() { + return new SurfaceControl.Builder() + .setContainerLayer() + .setName("SurfaceControlRegistryTests") + .setCallsite("SurfaceControlRegistryTests") + .build(); + } + + private static class TestReporter implements SurfaceControlRegistry.Reporter { + WeakHashMap<SurfaceControl, Long> lastSurfaceControls = new WeakHashMap<>(); + int callCount = 0; + + @Override + public void onMaxLayersExceeded(WeakHashMap<SurfaceControl, Long> surfaceControls, + int limit, PrintWriter pw) { + lastSurfaceControls.clear(); + lastSurfaceControls.putAll(surfaceControls); + callCount++; + } + + public void assertMaxThresholdExceededCallCount(int count) { + assertTrue("Expected " + count + " got " + callCount, count == callCount); + } + + public void assertLastReportedSetEquals(SurfaceControl... surfaces) { + WeakHashMap<SurfaceControl, Long> last = new WeakHashMap<>(lastSurfaceControls); + for (int i = 0; i < surfaces.length; i++) { + last.remove(surfaces[i]); + } + if (!last.isEmpty()) { + fail("Sets differ"); + } + } + } +} diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java index 963014e0bb50..467222696dfb 100644 --- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java +++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java @@ -22,7 +22,6 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; @@ -34,9 +33,6 @@ import android.app.PendingIntent; import android.appwidget.AppWidgetHostView; import android.content.Context; import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.AsyncTask; @@ -91,19 +87,6 @@ public class RemoteViewsTest { } @Test - public void clone_doesNotCopyBitmap() { - RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test); - Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); - - original.setImageViewBitmap(R.id.image, bitmap); - RemoteViews clone = original.clone(); - View inflated = clone.apply(mContext, mContainer); - - Drawable drawable = ((ImageView) inflated.findViewById(R.id.image)).getDrawable(); - assertSame(bitmap, ((BitmapDrawable)drawable).getBitmap()); - } - - @Test public void clone_originalCanStillBeApplied() { RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test); diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index cde100cc20aa..8e772a2d1845 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -173,4 +173,25 @@ public class WindowOnBackInvokedDispatcherTest { waitForIdle(); verify(mCallback2).onBackStarted(any(BackEvent.class)); } + + @Test + public void onUnregisterWhileBackInProgress_callOnBackCancelled() throws RemoteException { + ArgumentCaptor<OnBackInvokedCallbackInfo> captor = + ArgumentCaptor.forClass(OnBackInvokedCallbackInfo.class); + + mDispatcher.registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, mCallback1); + + verify(mWindowSession).setOnBackInvokedCallbackInfo( + Mockito.eq(mWindow), + captor.capture()); + IOnBackInvokedCallback iOnBackInvokedCallback = captor.getValue().getCallback(); + iOnBackInvokedCallback.onBackStarted(mBackEvent); + waitForIdle(); + verify(mCallback1).onBackStarted(any(BackEvent.class)); + + mDispatcher.unregisterOnBackInvokedCallback(mCallback1); + verify(mCallback1).onBackCancelled(); + verifyNoMoreInteractions(mCallback1); + } } diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index ead5fd49c508..a044602f7ec0 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -525,6 +525,8 @@ applications that come with the platform <permission name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" /> <!-- Permission required for GTS test - GtsCredentialsTestCases --> <permission name="android.permission.LAUNCH_CREDENTIAL_SELECTOR"/> + <!-- Permission required for CTS test IntentRedirectionTest --> + <permission name="android.permission.QUERY_CLONED_APPS"/> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 7c2759af156f..4b4e7220cb29 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1675,6 +1675,12 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/am\/ActivityManagerService.java" }, + "-584061725": { + "message": "Content Recording: Accept session updating same display %d with granted consent, with an existing session %s", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecordingController.java" + }, "-583031528": { "message": "%s", "level": "INFO", @@ -2059,6 +2065,12 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-233530384": { + "message": "Content Recording: Incoming session on display %d can't be set since it is already null; the corresponding VirtualDisplay must have already been removed.", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecordingController.java" + }, "-230587670": { "message": "SyncGroup %d: Unfinished container: %s", "level": "VERBOSE", @@ -2161,6 +2173,12 @@ "group": "WM_DEBUG_BACK_PREVIEW", "at": "com\/android\/server\/wm\/TaskFragment.java" }, + "-125383273": { + "message": "Content Recording: waiting to record, so do nothing", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-124316973": { "message": "Translucent=%s Floating=%s ShowWallpaper=%s Disable=%s", "level": "VERBOSE", @@ -3865,6 +3883,12 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/WindowState.java" }, + "1511273241": { + "message": "Refreshing activity for camera compatibility treatment, activityRecord=%s", + "level": "VERBOSE", + "group": "WM_DEBUG_STATES", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "1518495446": { "message": "removeWindowToken: Attempted to remove non-existing token: %s", "level": "WARN", @@ -4297,12 +4321,6 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, - "1967643923": { - "message": "Refershing activity for camera compatibility treatment, activityRecord=%s", - "level": "VERBOSE", - "group": "WM_DEBUG_STATES", - "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" - }, "1967975839": { "message": "Changing app %s visible=%b performLayout=%b", "level": "VERBOSE", diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java index 658d92cc7489..96190c4bc00a 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java @@ -31,12 +31,14 @@ import android.view.DisplayAddress; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.window.extensions.WindowExtensions; import androidx.window.extensions.core.util.function.Consumer; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -51,6 +53,7 @@ import java.util.concurrent.Executor; public class WindowAreaComponentImpl implements WindowAreaComponent, DeviceStateManager.DeviceStateCallback { + private static final int INVALID_DISPLAY_ADDRESS = -1; private final Object mLock = new Object(); @NonNull @@ -69,8 +72,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, private final int mConcurrentDisplayState; @NonNull private final int[] mFoldedDeviceStates; - @NonNull - private long mRearDisplayAddress = 0; + private long mRearDisplayAddress = INVALID_DISPLAY_ADDRESS; @WindowAreaSessionState private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE; @@ -109,10 +111,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, R.integer.config_deviceStateConcurrentRearDisplay); mDeviceStateManager.registerCallback(mExecutor, this); - if (mConcurrentDisplayState != INVALID_DEVICE_STATE) { - mRearDisplayAddress = Long.parseLong(context.getResources().getString( - R.string.config_rearDisplayPhysicalAddress)); - } + mRearDisplayAddress = getRearDisplayAddress(context); } /** @@ -220,6 +219,44 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, } /** + * Returns the{@link DisplayMetrics} associated with the rear facing display. If the rear facing + * display was not found in the display list, but we have already computed the + * {@link DisplayMetrics} for that display, we return the cached value. If no display has been + * found, then we return an empty {@link DisplayMetrics} value. + * + * TODO(b/267563768): Update with guidance from Display team for missing displays. + * + * @since {@link WindowExtensions#VENDOR_API_LEVEL_3} + */ + @Override + public DisplayMetrics getRearDisplayMetrics() { + DisplayMetrics metrics = null; + + // DISPLAY_CATEGORY_REAR displays are only available when you are in the concurrent + // display state, so we have to look through all displays to match the address + Display[] displays = mDisplayManager.getDisplays( + DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED); + for (int i = 0; i < displays.length; i++) { + DisplayAddress.Physical address = + (DisplayAddress.Physical) displays[i].getAddress(); + if (mRearDisplayAddress == address.getPhysicalDisplayId()) { + metrics = new DisplayMetrics(); + displays[i].getRealMetrics(metrics); + break; + } + } + + synchronized (mLock) { + // Update the rear display metrics with our latest value if one was received + if (metrics != null) { + mRearDisplayMetrics = metrics; + } + + return Objects.requireNonNullElseGet(mRearDisplayMetrics, DisplayMetrics::new); + } + } + + /** * Adds a listener interested in receiving updates on the RearDisplayPresentationStatus * of the device. Because this is being called from the OEM provided * extensions, the result of the listener will be posted on the executor @@ -260,8 +297,8 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, return; } @WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus(); - DisplayMetrics metrics = - currentStatus == STATUS_UNSUPPORTED ? null : getRearDisplayMetrics(); + DisplayMetrics metrics = currentStatus == STATUS_UNSUPPORTED ? new DisplayMetrics() + : getRearDisplayMetrics(); consumer.accept( new RearDisplayPresentationStatus(currentStatus, metrics)); } @@ -342,11 +379,22 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, mRearDisplayPresentationController); DeviceStateRequest concurrentDisplayStateRequest = DeviceStateRequest.newBuilder( mConcurrentDisplayState).build(); - mDeviceStateManager.requestState( - concurrentDisplayStateRequest, - mExecutor, - deviceStateCallback - ); + + try { + mDeviceStateManager.requestState( + concurrentDisplayStateRequest, + mExecutor, + deviceStateCallback + ); + } catch (SecurityException e) { + // If a SecurityException occurs when invoking DeviceStateManager#requestState + // (e.g. if the caller is not in the foreground, or if it does not have the required + // permissions), we should first clean up our local state before re-throwing the + // SecurityException to the caller. Otherwise, subsequent attempts to + // startRearDisplayPresentationSession will always fail. + mRearDisplayPresentationController = null; + throw e; + } } } @@ -480,37 +528,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, } } - /** - * Returns the{@link DisplayMetrics} associated with the rear facing display. If the rear facing - * display was not found in the display list, but we have already computed the - * {@link DisplayMetrics} for that display, we return the cached value. - * - * TODO(b/267563768): Update with guidance from Display team for missing displays. - * - * @throws IllegalArgumentException if the display is not found and there is no cached - * {@link DisplayMetrics} for this display. - */ - @GuardedBy("mLock") - private DisplayMetrics getRearDisplayMetrics() { - Display[] displays = mDisplayManager.getDisplays( - DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED); - for (int i = 0; i < displays.length; i++) { - DisplayAddress.Physical address = - (DisplayAddress.Physical) displays[i].getAddress(); - if (mRearDisplayAddress == address.getPhysicalDisplayId()) { - if (mRearDisplayMetrics == null) { - mRearDisplayMetrics = new DisplayMetrics(); - } - displays[i].getRealMetrics(mRearDisplayMetrics); - return mRearDisplayMetrics; - } - } - if (mRearDisplayMetrics != null) { - return mRearDisplayMetrics; - } else { - throw new IllegalArgumentException( - "No display found with the provided display address"); - } + private long getRearDisplayAddress(Context context) { + String address = context.getResources().getString( + R.string.config_rearDisplayPhysicalAddress); + return address.isEmpty() ? INVALID_DISPLAY_ADDRESS : Long.parseLong(address); } @GuardedBy("mLock") diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml deleted file mode 100644 index d8f356164358..000000000000 --- a/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2021 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:fillColor="@color/tv_pip_menu_focus_border" - android:pathData="M7,10l5,5 5,-5H7z"/> -</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml deleted file mode 100644 index 3e0011c65942..000000000000 --- a/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2021 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:fillColor="@color/tv_pip_menu_focus_border" - android:pathData="M14,7l-5,5 5,5V7z"/> -</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml deleted file mode 100644 index f6b3c72e3cb5..000000000000 --- a/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2021 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:fillColor="@color/tv_pip_menu_focus_border" - android:pathData="M10,17l5,-5 -5,-5v10z"/> -</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml deleted file mode 100644 index 1a3446249573..000000000000 --- a/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2021 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:fillColor="@color/tv_pip_menu_focus_border" - android:pathData="M7,14l5,-5 5,5H7z"/> -</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml index 49491a7b572c..69b339ad77fb 100644 --- a/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml +++ b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml @@ -20,47 +20,48 @@ android:focusable="false" android:focusableInTouchMode="false" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:theme="@style/ReachabilityEduHandLayout"> <androidx.appcompat.widget.AppCompatTextView - style="@style/ReachabilityEduHandLayout" android:text="@string/letterbox_reachability_reposition_text" app:drawableTopCompat="@drawable/reachability_education_ic_right_hand" android:layout_gravity="center_horizontal|top" android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" android:id="@+id/reachability_move_up_button" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:visibility="invisible"/> <androidx.appcompat.widget.AppCompatTextView - style="@style/ReachabilityEduHandLayout" android:text="@string/letterbox_reachability_reposition_text" app:drawableTopCompat="@drawable/reachability_education_ic_right_hand" android:layout_gravity="center_vertical|right" android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" android:id="@+id/reachability_move_right_button" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:visibility="invisible"/> <androidx.appcompat.widget.AppCompatTextView - style="@style/ReachabilityEduHandLayout" android:text="@string/letterbox_reachability_reposition_text" app:drawableTopCompat="@drawable/reachability_education_ic_left_hand" android:layout_gravity="center_vertical|left" android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" android:id="@+id/reachability_move_left_button" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:visibility="invisible"/> <androidx.appcompat.widget.AppCompatTextView - style="@style/ReachabilityEduHandLayout" android:text="@string/letterbox_reachability_reposition_text" app:drawableTopCompat="@drawable/reachability_education_ic_right_hand" android:layout_gravity="center_horizontal|bottom" android:layout_marginTop="@dimen/letterbox_reachability_education_dialog_margin" android:id="@+id/reachability_move_down_button" android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_height="wrap_content" + android:visibility="invisible"/> </com.android.wm.shell.compatui.ReachabilityEduLayout> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index ab64f9e359b0..82a358cf68d6 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -104,9 +104,7 @@ android:layout_centerHorizontal="true" android:layout_alignParentTop="true" android:alpha="0" - android:contentDescription="@string/a11y_action_pip_move_up" - android:elevation="@dimen/pip_menu_arrow_elevation" - android:src="@drawable/pip_ic_move_up" /> + android:contentDescription="@string/a11y_action_pip_move_up"/> <ImageView android:id="@+id/tv_pip_menu_arrow_right" @@ -115,9 +113,7 @@ android:layout_centerVertical="true" android:layout_alignParentRight="true" android:alpha="0" - android:contentDescription="@string/a11y_action_pip_move_right" - android:elevation="@dimen/pip_menu_arrow_elevation" - android:src="@drawable/pip_ic_move_right" /> + android:contentDescription="@string/a11y_action_pip_move_right"/> <ImageView android:id="@+id/tv_pip_menu_arrow_down" @@ -126,9 +122,7 @@ android:layout_centerHorizontal="true" android:layout_alignParentBottom="true" android:alpha="0" - android:contentDescription="@string/a11y_action_pip_move_down" - android:elevation="@dimen/pip_menu_arrow_elevation" - android:src="@drawable/pip_ic_move_down" /> + android:contentDescription="@string/a11y_action_pip_move_down"/> <ImageView android:id="@+id/tv_pip_menu_arrow_left" @@ -137,7 +131,5 @@ android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:alpha="0" - android:contentDescription="@string/a11y_action_pip_move_left" - android:elevation="@dimen/pip_menu_arrow_elevation" - android:src="@drawable/pip_ic_move_left" /> + android:contentDescription="@string/a11y_action_pip_move_left"/> </RelativeLayout> diff --git a/libs/WindowManager/Shell/res/values-night/styles.xml b/libs/WindowManager/Shell/res/values-night/styles.xml index 758c99db7bb4..4871f7ada627 100644 --- a/libs/WindowManager/Shell/res/values-night/styles.xml +++ b/libs/WindowManager/Shell/res/values-night/styles.xml @@ -17,12 +17,10 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android"> - <style name="ReachabilityEduHandLayout"> + <style name="ReachabilityEduHandLayout" parent="Theme.AppCompat"> <item name="android:focusable">false</item> <item name="android:focusableInTouchMode">false</item> <item name="android:background">@android:color/transparent</item> - <item name="android:contentDescription">@string/restart_button_description</item> - <item name="android:visibility">invisible</item> <item name="android:lineSpacingExtra">-1sp</item> <item name="android:textSize">12sp</item> <item name="android:textAlignment">center</item> diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index adbf65648dd1..fd825639f1e8 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -33,8 +33,8 @@ <!-- outer space minus border width --> <dimen name="pip_menu_outer_space_frame">20dp</dimen> - <dimen name="pip_menu_arrow_size">24dp</dimen> - <dimen name="pip_menu_arrow_elevation">5dp</dimen> + <dimen name="pip_menu_arrow_size">12dp</dimen> + <dimen name="pip_menu_arrow_elevation">1dp</dimen> <dimen name="pip_menu_elevation_no_menu">1dp</dimen> <dimen name="pip_menu_elevation_move_menu">7dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml index e6933ca3fce6..5f7fb12c3002 100644 --- a/libs/WindowManager/Shell/res/values/colors_tv.xml +++ b/libs/WindowManager/Shell/res/values/colors_tv.xml @@ -27,6 +27,10 @@ <color name="tv_pip_menu_focus_border">#E8EAED</color> <color name="tv_pip_menu_dim_layer">#990E0E0F</color> <color name="tv_pip_menu_background">#1E232C</color> + <!-- Normally, the arrow color would be the same as the focus border color. But due to + optical illusion that looks too dark on the screen. That's why we define a separate + (lighter) arrow color. --> + <color name="tv_pip_menu_arrow_color">#F1F3F4</color> <color name="tv_pip_edu_text">#99D2E3FC</color> <color name="tv_pip_edu_text_home_icon">#D2E3FC</color> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 2b3888854d5a..8635c56b7bc6 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -151,12 +151,10 @@ </item> </style> - <style name="ReachabilityEduHandLayout"> + <style name="ReachabilityEduHandLayout" parent="Theme.AppCompat.Light"> <item name="android:focusable">false</item> <item name="android:focusableInTouchMode">false</item> <item name="android:background">@android:color/transparent</item> - <item name="android:contentDescription">@string/restart_button_description</item> - <item name="android:visibility">invisible</item> <item name="android:lineSpacingExtra">-1sp</item> <item name="android:textSize">12sp</item> <item name="android:textAlignment">center</item> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index 59f120deeb94..4d87c9583f64 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -61,6 +61,9 @@ class ActivityEmbeddingAnimationRunner { @VisibleForTesting final ActivityEmbeddingAnimationSpec mAnimationSpec; + @Nullable + private Animator mActiveAnimator; + ActivityEmbeddingAnimationRunner(@NonNull Context context, @NonNull ActivityEmbeddingController controller) { mController = controller; @@ -75,8 +78,10 @@ class ActivityEmbeddingAnimationRunner { // applied to make sure the surface is ready. final List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks = new ArrayList<>(); - final Animator animator = createAnimator(info, startTransaction, finishTransaction, + final Animator animator = createAnimator(info, startTransaction, + finishTransaction, () -> mController.onAnimationFinished(transition), postStartTransactionCallbacks); + mActiveAnimator = animator; // Start the animation. if (!postStartTransactionCallbacks.isEmpty()) { @@ -98,6 +103,17 @@ class ActivityEmbeddingAnimationRunner { } } + void cancelAnimationFromMerge() { + if (mActiveAnimator == null) { + Log.e(TAG, + "No active ActivityEmbedding animator running but mergeAnimation is " + + "trying to cancel one." + ); + return; + } + mActiveAnimator.end(); + } + /** * Sets transition animation scale settings value. * @param scale The setting value of transition animation scale. @@ -153,6 +169,7 @@ class ActivityEmbeddingAnimationRunner { adapter.onAnimationEnd(t); } t.apply(); + mActiveAnimator = null; animationFinishCallback.run(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java index bfbddbbe4aa0..fbdbd3e61d92 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java @@ -118,6 +118,13 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle return true; } + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + mAnimationRunner.cancelAnimationFromMerge(); + } + private boolean handleNonEmbeddedChanges(List<TransitionInfo.Change> changes) { final Rect nonClosingEmbeddedArea = new Rect(); for (int i = changes.size() - 1; i >= 0; i--) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java index 53a438ec2fde..c98090638010 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java @@ -54,10 +54,42 @@ public interface BackAnimation { void setTriggerBack(boolean triggerBack); /** - * Sets the threshold values that defining edge swipe behavior. - * @param progressThreshold the max threshold to keep linear progressing back animation. + * Sets the threshold values that define edge swipe behavior.<br> + * <br> + * <h1>How does {@code nonLinearFactor} work?</h1> + * <pre> + * screen screen screen + * width width width + * |——————| |————————————| |————————————————————| + * A B A B C A + * 1 +——————+—————+ 1 +————————————+ 1 +————————————+———————+ + * | / | | —/| | | —————/| + * | / | | —/ | | ——/ | + * | / | | —/ | | ——/ | | + * | / | | —/ | | ——/ | | + * | / | | —/ | | ——/ | | + * |/ | |—/ | |—/ | | + * 0 +————————————+ 0 +————————————+ 0 +————————————+———————+ + * B B B + * </pre> + * Three devices with different widths (smaller, equal, and wider) relative to the progress + * threshold are shown in the graphs.<br> + * - A is the width of the screen<br> + * - B is the progress threshold (horizontal swipe distance where progress is linear)<br> + * - C equals B + (A - B) * nonLinearFactor<br> + * <br> + * If A is less than or equal to B, {@code progress} for the swipe distance between:<br> + * - [0, A] will scale linearly between [0, 1].<br> + * If A is greater than B, {@code progress} for swipe distance between:<br> + * - [0, B] will scale linearly between [0, B / C]<br> + * - (B, A] will scale non-linearly and reach 1. + * + * @param linearDistance up to this distance progress continues linearly. B in the graph above. + * @param maxDistance distance at which the progress will be 1f. A in the graph above. + * @param nonLinearFactor This value is used to calculate the target if the screen is wider + * than the progress threshold. */ - void setSwipeThresholds(float progressThreshold); + void setSwipeThresholds(float linearDistance, float maxDistance, float nonLinearFactor); /** * Sets the system bar listener to control the system bar color. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 6d879b830e2e..bb543f24a8ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -301,9 +301,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } @Override - public void setSwipeThresholds(float progressThreshold) { + public void setSwipeThresholds( + float linearDistance, + float maxDistance, + float nonLinearFactor) { mShellExecutor.execute(() -> BackAnimationController.this.setSwipeThresholds( - progressThreshold)); + linearDistance, maxDistance, nonLinearFactor)); } @Override @@ -509,7 +512,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // Constraints - absolute values float minVelocity = mFlingAnimationUtils.getMinVelocityPxPerSecond(); float maxVelocity = mFlingAnimationUtils.getHighVelocityPxPerSecond(); - float maxX = mTouchTracker.getMaxX(); // px + float maxX = mTouchTracker.getMaxDistance(); // px float maxFlingDistance = maxX * MAX_FLING_PROGRESS; // px // Current state @@ -605,8 +608,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTouchTracker.setTriggerBack(triggerBack); } - private void setSwipeThresholds(float progressThreshold) { - mTouchTracker.setProgressThreshold(progressThreshold); + private void setSwipeThresholds( + float linearDistance, + float maxDistance, + float nonLinearFactor) { + mTouchTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor); } private void invokeOrCancelBack() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java index 7a00f5b9bab4..a0ada39b459e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -28,11 +28,13 @@ import android.window.BackMotionEvent; * Helper class to record the touch location for gesture and generate back events. */ class TouchTracker { - private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP = - "persist.wm.debug.predictive_back_progress_threshold"; - private static final int PROGRESS_THRESHOLD = SystemProperties - .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); - private float mProgressThreshold; + private static final String PREDICTIVE_BACK_LINEAR_DISTANCE_PROP = + "persist.wm.debug.predictive_back_linear_distance"; + private static final int LINEAR_DISTANCE = SystemProperties + .getInt(PREDICTIVE_BACK_LINEAR_DISTANCE_PROP, -1); + private float mLinearDistance = LINEAR_DISTANCE; + private float mMaxDistance; + private float mNonLinearFactor; /** * Location of the latest touch event */ @@ -125,17 +127,42 @@ class TouchTracker { // the location everytime back is restarted after being cancelled. float startX = mTriggerBack ? mInitTouchX : mStartThresholdX; float deltaX = Math.abs(startX - touchX); - float maxX = getMaxX(); - maxX = maxX == 0 ? 1 : maxX; - return MathUtils.constrain(deltaX / maxX, 0, 1); + float linearDistance = mLinearDistance; + float maxDistance = getMaxDistance(); + maxDistance = maxDistance == 0 ? 1 : maxDistance; + float progress; + if (linearDistance < maxDistance) { + // Up to linearDistance it behaves linearly, then slowly reaches 1f. + + // maxDistance is composed of linearDistance + nonLinearDistance + float nonLinearDistance = maxDistance - linearDistance; + float initialTarget = linearDistance + nonLinearDistance * mNonLinearFactor; + + boolean isLinear = deltaX <= linearDistance; + if (isLinear) { + progress = deltaX / initialTarget; + } else { + float nonLinearDeltaX = deltaX - linearDistance; + float nonLinearProgress = nonLinearDeltaX / nonLinearDistance; + float currentTarget = MathUtils.lerp( + /* start = */ initialTarget, + /* stop = */ maxDistance, + /* amount = */ nonLinearProgress); + progress = deltaX / currentTarget; + } + } else { + // Always linear behavior. + progress = deltaX / maxDistance; + } + return MathUtils.constrain(progress, 0, 1); } /** - * Maximum X value (in pixels). + * Maximum distance in pixels. * Progress is considered to be completed (1f) when this limit is exceeded. */ - float getMaxX() { - return PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold; + float getMaxDistance() { + return mMaxDistance; } BackMotionEvent createProgressEvent(float progress) { @@ -149,7 +176,14 @@ class TouchTracker { /* departingAnimationTarget = */ null); } - public void setProgressThreshold(float progressThreshold) { - mProgressThreshold = progressThreshold; + public void setProgressThresholds(float linearDistance, float maxDistance, + float nonLinearFactor) { + if (LINEAR_DISTANCE >= 0) { + mLinearDistance = LINEAR_DISTANCE; + } else { + mLinearDistance = linearDistance; + } + mMaxDistance = maxDistance; + mNonLinearFactor = nonLinearFactor; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index adc0c9c4322a..9fcd207dc370 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -46,6 +46,7 @@ import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Picture; import android.graphics.PointF; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.ShapeDrawable; import android.os.RemoteException; @@ -479,13 +480,18 @@ public class BubbleExpandedView extends LinearLayout { void applyThemeAttrs() { final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ android.R.attr.dialogCornerRadius, - com.android.internal.R.attr.materialColorSurfaceBright}); + com.android.internal.R.attr.materialColorSurfaceBright, + com.android.internal.R.attr.materialColorSurfaceContainerHigh}); boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( mContext.getResources()); mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0; mBackgroundColorFloating = ta.getColor(1, Color.WHITE); mExpandedViewContainer.setBackgroundColor(mBackgroundColorFloating); + final int manageMenuBg = ta.getColor(2, Color.WHITE); ta.recycle(); + if (mManageButton != null) { + mManageButton.getBackground().setColorFilter(manageMenuBg, PorterDuff.Mode.SRC_IN); + } if (mTaskView != null) { mTaskView.setCornerRadius(mCornerRadius); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index e7ec7aa164e9..a48be5ed5ad5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -38,8 +38,10 @@ import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Color; import android.graphics.Outline; import android.graphics.PointF; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; @@ -1203,6 +1205,12 @@ public class BubbleStackView extends FrameLayout R.layout.bubble_manage_menu, this, false); mManageMenu.setVisibility(View.INVISIBLE); + final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ + com.android.internal.R.attr.materialColorSurfaceBright}); + final int menuBackgroundColor = ta.getColor(0, Color.WHITE); + ta.recycle(); + mManageMenu.getBackground().setColorFilter(menuBackgroundColor, PorterDuff.Mode.SRC_IN); + PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig); mManageMenu.setOutlineProvider(new ViewOutlineProvider() { @@ -1784,13 +1792,17 @@ public class BubbleStackView extends FrameLayout // We're expanded while the last bubble is being removed. Let the scrim animate away // and then remove our views (removing the icon view triggers the removal of the // bubble window so do that at the end of the animation so we see the scrim animate). + BadgedImageView iconView = bubble.getIconView(); showScrim(false, () -> { mRemovingLastBubbleWhileExpanded = false; bubble.cleanupExpandedView(); - mBubbleContainer.removeView(bubble.getIconView()); + if (iconView != null) { + mBubbleContainer.removeView(iconView); + } bubble.cleanupViews(); // cleans up the icon view updateExpandedView(); // resets state for no expanded bubble }); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; } // Remove it from the views @@ -1816,6 +1828,7 @@ public class BubbleStackView extends FrameLayout // If a bubble is suppressed, it is not attached to the container. Clean it up. if (bubble.isSuppressed()) { bubble.cleanupViews(); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); } else { Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 56616cb6bd88..753dfa7396f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -22,6 +22,8 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -51,6 +53,8 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.SurfaceUtils; +import java.util.function.Consumer; + /** * Handles split decor like showing resizing hint for a specific split. */ @@ -58,7 +62,6 @@ public class SplitDecorManager extends WindowlessWindowManager { private static final String TAG = SplitDecorManager.class.getSimpleName(); private static final String RESIZING_BACKGROUND_SURFACE_NAME = "ResizingBackground"; private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground"; - private static final long FADE_DURATION = 133; private final IconProvider mIconProvider; private final SurfaceSession mSurfaceSession; @@ -251,7 +254,7 @@ public class SplitDecorManager extends WindowlessWindowManager { } /** Stops showing resizing hint. */ - public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) { + public void onResized(SurfaceControl.Transaction t, Consumer<Boolean> animFinishedCallback) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } @@ -261,6 +264,7 @@ public class SplitDecorManager extends WindowlessWindowManager { final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); mScreenshotAnimator = ValueAnimator.ofFloat(1, 0); + mScreenshotAnimator.setDuration(FADE_DURATION); mScreenshotAnimator.addUpdateListener(valueAnimator -> { final float progress = (float) valueAnimator.getAnimatedValue(); animT.setAlpha(mScreenshot, progress); @@ -281,7 +285,7 @@ public class SplitDecorManager extends WindowlessWindowManager { mScreenshot = null; if (mRunningAnimationCount == 0 && animFinishedCallback != null) { - animFinishedCallback.run(); + animFinishedCallback.accept(true); } } }); @@ -313,12 +317,12 @@ public class SplitDecorManager extends WindowlessWindowManager { } } if (mShown) { - fadeOutDecor(animFinishedCallback); + fadeOutDecor(()-> animFinishedCallback.accept(true)); } else { // Decor surface is hidden so release it directly. releaseDecor(t); if (mRunningAnimationCount == 0 && animFinishedCallback != null) { - animFinishedCallback.run(); + animFinishedCallback.accept(false); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 2832c553c20c..9a2ec15238a7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -717,6 +717,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return bounds.width() > bounds.height(); } + public boolean isDensityChanged(int densityDpi) { + return mDensity != densityDpi; + } + /** * Return if this layout is landscape. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java index 0bcafe513b4f..ef93a336305f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java @@ -43,6 +43,11 @@ public class SplitScreenConstants { */ public static final int SPLIT_POSITION_BOTTOM_OR_RIGHT = 1; + /** + * Duration used for every split fade-in or fade-out. + */ + public static final int FADE_DURATION = 133; + @IntDef(prefix = {"SPLIT_POSITION_"}, value = { SPLIT_POSITION_UNDEFINED, SPLIT_POSITION_TOP_OR_LEFT, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java index 042721c97053..0289da916937 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -78,8 +78,15 @@ public class SplitScreenUtils { return taskInfo != null ? getPackageName(taskInfo.baseIntent) : null; } - /** Returns true if they are the same package. */ - public static boolean samePackage(String packageName1, String packageName2) { - return packageName1 != null && packageName1.equals(packageName2); + /** Retrieve user id from a taskId */ + public static int getUserId(int taskId, ShellTaskOrganizer taskOrganizer) { + final ActivityManager.RunningTaskInfo taskInfo = taskOrganizer.getRunningTaskInfo(taskId); + return taskInfo != null ? taskInfo.userId : -1; + } + + /** Returns true if package names and user ids match. */ + public static boolean samePackage(String packageName1, String packageName2, + int userId1, int userId2) { + return (packageName1 != null && packageName1.equals(packageName2)) && (userId1 == userId2); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 28368ef37061..74ef57e4baae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -77,6 +77,8 @@ import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.phone.PipTouchHandler; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; @@ -562,6 +564,28 @@ public abstract class WMShellBaseModule { } // + // Keyguard transitions (optional feature) + // + + @WMSingleton + @Provides + static KeyguardTransitionHandler provideKeyguardTransitionHandler( + ShellInit shellInit, + Transitions transitions, + @ShellMainThread Handler mainHandler, + @ShellMainThread ShellExecutor mainExecutor) { + return new KeyguardTransitionHandler( + shellInit, transitions, mainHandler, mainExecutor); + } + + @WMSingleton + @Provides + static KeyguardTransitions provideKeyguardTransitions( + KeyguardTransitionHandler handler) { + return handler.asKeyguardTransitions(); + } + + // // Display areas // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index f3130d358ec1..4980e49a6fc3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -60,6 +60,7 @@ import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; import com.android.wm.shell.freeform.FreeformTaskTransitionHandler; import com.android.wm.shell.freeform.FreeformTaskTransitionObserver; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; @@ -200,6 +201,7 @@ public abstract class WMShellModule { ShellTaskOrganizer taskOrganizer, DisplayController displayController, SyncTransactionQueue syncQueue, + Transitions transitions, Optional<DesktopModeController> desktopModeController, Optional<DesktopTasksController> desktopTasksController, Optional<SplitScreenController> splitScreenController) { @@ -211,6 +213,7 @@ public abstract class WMShellModule { taskOrganizer, displayController, syncQueue, + transitions, desktopModeController, desktopTasksController, splitScreenController); @@ -532,9 +535,10 @@ public abstract class WMShellModule { Optional<SplitScreenController> splitScreenOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, Optional<RecentsTransitionHandler> recentsTransitionHandler, + KeyguardTransitionHandler keyguardTransitionHandler, Transitions transitions) { return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional, - pipTouchHandlerOptional, recentsTransitionHandler); + pipTouchHandlerOptional, recentsTransitionHandler, keyguardTransitionHandler); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index df94b414c092..fb08c878837a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -266,6 +266,7 @@ public class DragAndDropPolicy { final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); final Bundle opts = intent.hasExtra(EXTRA_ACTIVITY_OPTIONS) ? intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS) : new Bundle(); + final UserHandle user = intent.getParcelableExtra(EXTRA_USER); if (isTask) { final int taskId = intent.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); @@ -273,14 +274,14 @@ public class DragAndDropPolicy { } else if (isShortcut) { final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); final String id = intent.getStringExtra(EXTRA_SHORTCUT_ID); - final UserHandle user = intent.getParcelableExtra(EXTRA_USER); mStarter.startShortcut(packageName, id, position, opts, user); } else { final PendingIntent launchIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); // Put BAL flags to avoid activity start aborted. opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); - mStarter.startIntent(launchIntent, null /* fillIntent */, position, opts); + mStarter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */, + position, opts); } } @@ -334,8 +335,8 @@ public class DragAndDropPolicy { void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options); void startShortcut(String packageName, String shortcutId, @SplitPosition int position, @Nullable Bundle options, UserHandle user); - void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, - @Nullable Bundle options); + void startIntent(PendingIntent intent, int userId, Intent fillInIntent, + @SplitPosition int position, @Nullable Bundle options); void enterSplitScreen(int taskId, boolean leftOrTop); /** @@ -379,8 +380,8 @@ public class DragAndDropPolicy { } @Override - public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, int position, - @Nullable Bundle options) { + public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent, + int position, @Nullable Bundle options) { try { intent.send(mContext, 0, fillInIntent, null, null, null, options); } catch (PendingIntent.CanceledException e) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java new file mode 100644 index 000000000000..4d8075a9b56c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -0,0 +1,274 @@ +/* + * 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.wm.shell.keyguard; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; +import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; +import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; +import static android.view.WindowManager.TRANSIT_NONE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_SLEEP; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; +import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD; + +import static com.android.wm.shell.util.TransitionUtil.isOpeningType; +import static com.android.wm.shell.util.TransitionUtil.isClosingType; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.RemoteException; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.util.ArrayMap; +import android.util.Log; +import android.view.SurfaceControl; +import android.window.IRemoteTransition; +import android.window.IRemoteTransitionFinishedCallback; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerTransaction; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback; + +import java.util.Map; + +/** + * The handler for Keyguard enter/exit and occlude/unocclude animations. + * + * <p>This takes the highest priority. + */ +public class KeyguardTransitionHandler implements Transitions.TransitionHandler { + private static final String TAG = "KeyguardTransition"; + + private final Transitions mTransitions; + private final Handler mMainHandler; + private final ShellExecutor mMainExecutor; + + private final Map<IBinder, IRemoteTransition> mStartedTransitions = new ArrayMap<>(); + + /** + * Local IRemoteTransition implementations registered by the keyguard service. + * @see KeyguardTransitions + */ + private IRemoteTransition mExitTransition = null; + private IRemoteTransition mOccludeTransition = null; + private IRemoteTransition mOccludeByDreamTransition = null; + private IRemoteTransition mUnoccludeTransition = null; + + public KeyguardTransitionHandler( + @NonNull ShellInit shellInit, + @NonNull Transitions transitions, + @NonNull Handler mainHandler, + @NonNull ShellExecutor mainExecutor) { + mTransitions = transitions; + mMainHandler = mainHandler; + mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mTransitions.addHandler(this); + } + + /** + * Interface for SystemUI implementations to set custom Keyguard exit/occlude handlers. + */ + @ExternalThread + public KeyguardTransitions asKeyguardTransitions() { + return new KeyguardTransitionsImpl(); + } + + public static boolean handles(TransitionInfo info) { + return (info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0 + || (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0 + || info.getType() == TRANSIT_KEYGUARD_OCCLUDE + || info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull TransitionFinishCallback finishCallback) { + if (!handles(info)) { + return false; + } + + boolean hasOpeningOcclude = false; + boolean hasOpeningDream = false; + boolean hasClosingApp = false; + + // Check for occluding/dream/closing apps + for (int i = info.getChanges().size() - 1; i >= 0; i--) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (isOpeningType(change.getMode())) { + if (change.hasFlags(FLAG_OCCLUDES_KEYGUARD)) { + hasOpeningOcclude = true; + } + if (change.getTaskInfo() != null + && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_DREAM) { + hasOpeningDream = true; + } + } else if (isClosingType(change.getMode())) { + hasClosingApp = true; + } + } + + // Choose a transition applicable for the changes and keyguard state. + if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) { + return startAnimation(mExitTransition, + "going-away", + transition, info, startTransaction, finishTransaction, finishCallback); + } + if (hasOpeningOcclude || info.getType() == TRANSIT_KEYGUARD_OCCLUDE) { + if (hasOpeningDream) { + return startAnimation(mOccludeByDreamTransition, + "occlude-by-dream", + transition, info, startTransaction, finishTransaction, finishCallback); + } else { + return startAnimation(mOccludeTransition, + "occlude", + transition, info, startTransaction, finishTransaction, finishCallback); + } + } else if (hasClosingApp || info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE) { + return startAnimation(mUnoccludeTransition, + "unocclude", + transition, info, startTransaction, finishTransaction, finishCallback); + } else { + Log.wtf(TAG, "Failed to play: " + info); + return false; + } + } + + private boolean startAnimation(IRemoteTransition remoteHandler, String description, + @NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "start keyguard %s transition, info = %s", description, info); + + try { + remoteHandler.startAnimation(transition, info, startTransaction, + new IRemoteTransitionFinishedCallback.Stub() { + @Override + public void onTransitionFinished( + WindowContainerTransaction wct, SurfaceControl.Transaction sct) { + mMainExecutor.execute(() -> { + finishCallback.onTransitionFinished(wct, null); + }); + } + }); + mStartedTransitions.put(transition, remoteHandler); + } catch (RemoteException e) { + Log.wtf(TAG, "RemoteException thrown from local IRemoteTransition", e); + return false; + } + startTransaction.clear(); + return true; + } + + @Override + public void mergeAnimation(@NonNull IBinder nextTransition, @NonNull TransitionInfo nextInfo, + @NonNull SurfaceControl.Transaction nextT, @NonNull IBinder currentTransition, + @NonNull TransitionFinishCallback nextFinishCallback) { + final IRemoteTransition playing = mStartedTransitions.get(currentTransition); + + if (playing == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "unknown keyguard transition %s", currentTransition); + return; + } + + if (nextInfo.getType() == TRANSIT_SLEEP) { + // An empty SLEEP transition comes in as a signal to abort transitions whenever a sleep + // token is held. In cases where keyguard is showing, we are running the animation for + // the device sleeping/waking, so it's best to ignore this and keep playing anyway. + return; + } else { + finishAnimationImmediately(currentTransition); + } + } + + @Override + public void onTransitionConsumed(IBinder transition, boolean aborted, + SurfaceControl.Transaction finishTransaction) { + finishAnimationImmediately(transition); + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } + + private void finishAnimationImmediately(IBinder transition) { + final IRemoteTransition playing = mStartedTransitions.get(transition); + + if (playing != null) { + final IBinder fakeTransition = new Binder(); + final TransitionInfo fakeInfo = new TransitionInfo(TRANSIT_SLEEP, 0x0); + final SurfaceControl.Transaction fakeT = new SurfaceControl.Transaction(); + final FakeFinishCallback fakeFinishCb = new FakeFinishCallback(); + try { + playing.mergeAnimation(fakeTransition, fakeInfo, fakeT, transition, fakeFinishCb); + } catch (RemoteException e) { + // There is no good reason for this to happen because the player is a local object + // implementing an AIDL interface. + Log.wtf(TAG, "RemoteException thrown from KeyguardService transition", e); + } + } + } + + private static class FakeFinishCallback extends IRemoteTransitionFinishedCallback.Stub { + @Override + public void onTransitionFinished( + WindowContainerTransaction wct, SurfaceControl.Transaction t) { + return; + } + } + + @ExternalThread + private final class KeyguardTransitionsImpl implements KeyguardTransitions { + @Override + public void register( + IRemoteTransition exitTransition, + IRemoteTransition occludeTransition, + IRemoteTransition occludeByDreamTransition, + IRemoteTransition unoccludeTransition) { + mMainExecutor.execute(() -> { + mExitTransition = exitTransition; + mOccludeTransition = occludeTransition; + mOccludeByDreamTransition = occludeByDreamTransition; + mUnoccludeTransition = unoccludeTransition; + }); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java new file mode 100644 index 000000000000..b4b327f0eff2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.keyguard; + +import android.annotation.NonNull; +import android.window.IRemoteTransition; + +import com.android.wm.shell.common.annotations.ExternalThread; + +/** + * Interface exposed to SystemUI Keyguard to register handlers for running + * animations on keyguard visibility changes. + * + * TODO(b/274954192): Merge the occludeTransition and occludeByDream handlers and just let the + * keyguard handler make the decision on which version it wants to play. + */ +@ExternalThread +public interface KeyguardTransitions { + /** + * Registers a set of remote transitions for Keyguard. + */ + default void register( + @NonNull IRemoteTransition unlockTransition, + @NonNull IRemoteTransition occludeTransition, + @NonNull IRemoteTransition occludeByDreamTransition, + @NonNull IRemoteTransition unoccludeTransition) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java index 9fa57cacb11f..c701b9581ca2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java @@ -206,6 +206,7 @@ public abstract class PipContentOverlay { tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); tx.setBuffer(mLeash, mBitmap.getHardwareBuffer()); + tx.setAlpha(mLeash, 0f); tx.reparent(mLeash, parentLeash); tx.apply(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 8709eaba0272..58bc81dd3007 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -64,7 +64,6 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemProperties; -import android.util.Log; import android.view.Choreographer; import android.view.Display; import android.view.Surface; @@ -109,7 +108,6 @@ import java.util.function.IntConsumer; public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, DisplayController.OnDisplaysChangedListener, ShellTaskOrganizer.FocusListener { private static final String TAG = PipTaskOrganizer.class.getSimpleName(); - private static final boolean DEBUG = false; /** * The fixed start delay in ms when fading out the content overlay from bounds animation. @@ -1045,7 +1043,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, void onExitPipFinished(TaskInfo info) { if (mLeash == null) { // TODO(239461594): Remove once the double call to onExitPipFinished() is fixed - Log.w(TAG, "Warning, onExitPipFinished() called multiple times in the same sessino"); + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "Warning, onExitPipFinished() called multiple times in the same session"); return; } @@ -1134,15 +1133,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, && (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP); if ((mPipTransitionState.getInSwipePipToHomeTransition() || waitForFixedRotationOnEnteringPip) && fromRotation) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Skip onMovementBoundsChanged on rotation change" - + " InSwipePipToHomeTransition=%b" - + " mWaitForFixedRotation=%b" - + " getTransitionState=%d", TAG, - mPipTransitionState.getInSwipePipToHomeTransition(), mWaitForFixedRotation, - mPipTransitionState.getTransitionState()); - } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Skip onMovementBoundsChanged on rotation change" + + " InSwipePipToHomeTransition=%b" + + " mWaitForFixedRotation=%b" + + " getTransitionState=%d", TAG, + mPipTransitionState.getInSwipePipToHomeTransition(), mWaitForFixedRotation, + mPipTransitionState.getTransitionState()); return; } final PipAnimationController.PipTransitionAnimator animator = @@ -1437,8 +1434,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } if (mLeash == null || !mLeash.isValid()) { - Log.e(TAG, String.format("scheduleFinishResizePip with null leash! mState=%d", - mPipTransitionState.getTransitionState())); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: scheduleFinishResizePip with null leash! mState=%d", + TAG, mPipTransitionState.getTransitionState()); return; } 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 9677728d1d18..fa21db5f22ee 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 @@ -1015,6 +1015,16 @@ public class PipTransition extends PipTransitionController { mPipOrganizer.onExitPipFinished(prevPipTaskChange.getTaskInfo()); } + @Override + public boolean syncPipSurfaceState(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + final TransitionInfo.Change pipChange = findCurrentPipTaskChange(info); + if (pipChange == null) return false; + updatePipForUnhandledTransition(pipChange, startTransaction, finishTransaction); + return true; + } + private void updatePipForUnhandledTransition(@NonNull TransitionInfo.Change pipChange, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction) { @@ -1025,10 +1035,12 @@ public class PipTransition extends PipTransitionController { final boolean isInPip = mPipTransitionState.isInPip(); mSurfaceTransactionHelper .crop(startTransaction, leash, destBounds) - .round(startTransaction, leash, isInPip); + .round(startTransaction, leash, isInPip) + .shadow(startTransaction, leash, isInPip); mSurfaceTransactionHelper .crop(finishTransaction, leash, destBounds) - .round(finishTransaction, leash, isInPip); + .round(finishTransaction, leash, isInPip) + .shadow(finishTransaction, leash, isInPip); } /** Hides and shows the existing PIP during fixed rotation transition of other activities. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 949d6f558c32..2fff0e469f3d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -240,6 +240,18 @@ public abstract class PipTransitionController implements Transitions.TransitionH @NonNull final Transitions.TransitionFinishCallback finishCallback) { } + /** + * Applies the proper surface states (rounded corners/shadows) to pip surfaces in `info`. + * This is intended to be used when PiP is part of another animation but isn't, itself, + * animating (eg. unlocking). + * @return `true` if there was a pip in `info`. + */ + public boolean syncPipSurfaceState(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + return false; + } + /** End the currently-playing PiP animation. */ public void end() { } 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 f8e143575583..9e6bd4760900 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 @@ -946,10 +946,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.getDisplayBounds().right, mPipBoundsState.getDisplayBounds().bottom); mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, rect); + updatePipPositionForKeepClearAreas(); } else { mPipBoundsState.removeNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG); + // postpone moving in response to hide of Launcher in case there's another change + mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback); + mMainExecutor.executeDelayed( + mMovePipInResponseToKeepClearAreasChangeCallback, + PIP_KEEP_CLEAR_AREAS_DELAY); } - updatePipPositionForKeepClearAreas(); } private void setLauncherAppIconSize(int iconSizePx) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index d07641892552..613791ccc062 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -31,12 +31,19 @@ import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; import android.content.Context; +import android.content.res.Resources; +import android.graphics.Outline; +import android.graphics.Path; import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.PathShape; import android.os.Handler; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOutlineProvider; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.ImageView; @@ -91,6 +98,8 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L private final ImageView mArrowLeft; private final TvWindowMenuActionButton mA11yDoneButton; + private final int mArrowElevation; + private @TvPipMenuController.TvPipMenuMode int mCurrentMenuMode = MODE_NO_MENU; private final Rect mCurrentPipBounds = new Rect(); private int mCurrentPipGravity; @@ -131,21 +140,70 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left); mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button); - mResizeAnimationDuration = context.getResources().getInteger( - R.integer.config_pipResizeAnimationDuration); - mPipMenuFadeAnimationDuration = context.getResources() - .getInteger(R.integer.tv_window_menu_fade_animation_duration); + final Resources res = context.getResources(); + mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); + mPipMenuFadeAnimationDuration = + res.getInteger(R.integer.tv_window_menu_fade_animation_duration); + mPipMenuOuterSpace = res.getDimensionPixelSize(R.dimen.pip_menu_outer_space); + mPipMenuBorderWidth = res.getDimensionPixelSize(R.dimen.pip_menu_border_width); + mArrowElevation = res.getDimensionPixelSize(R.dimen.pip_menu_arrow_elevation); - mPipMenuOuterSpace = context.getResources() - .getDimensionPixelSize(R.dimen.pip_menu_outer_space); - mPipMenuBorderWidth = context.getResources() - .getDimensionPixelSize(R.dimen.pip_menu_border_width); + initMoveArrows(); mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, this); mEduTextContainer = (ViewGroup) findViewById(R.id.tv_pip_menu_edu_text_container); mEduTextContainer.addView(mEduTextDrawer); } + private void initMoveArrows() { + final int arrowSize = + mContext.getResources().getDimensionPixelSize(R.dimen.pip_menu_arrow_size); + final Path arrowPath = createArrowPath(arrowSize); + + final ShapeDrawable arrowDrawable = new ShapeDrawable(); + arrowDrawable.setShape(new PathShape(arrowPath, arrowSize, arrowSize)); + arrowDrawable.setTint(mContext.getResources().getColor(R.color.tv_pip_menu_arrow_color)); + + final ViewOutlineProvider arrowOutlineProvider = new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setPath(createArrowPath(view.getMeasuredHeight())); + } + }; + + initArrow(mArrowRight, arrowOutlineProvider, arrowDrawable, 0); + initArrow(mArrowDown, arrowOutlineProvider, arrowDrawable, 90); + initArrow(mArrowLeft, arrowOutlineProvider, arrowDrawable, 180); + initArrow(mArrowUp, arrowOutlineProvider, arrowDrawable, 270); + } + + /** + * Creates a Path for a movement arrow in the MODE_MOVE_MENU. The resulting Path is a simple + * right-pointing triangle with its tip in the center of a size x size square: + * _ _ _ _ _ + * |* | + * |* * | + * |* * | + * |* * | + * |* _ _ _ _| + * + */ + private Path createArrowPath(int size) { + final Path triangle = new Path(); + triangle.lineTo(0, size); + triangle.lineTo(size / 2, size / 2); + triangle.close(); + return triangle; + } + + private void initArrow(View v, ViewOutlineProvider arrowOutlineProvider, Drawable arrowDrawable, + int rotation) { + v.setOutlineProvider(arrowOutlineProvider); + v.setBackground(arrowDrawable); + v.setRotation(rotation); + v.setElevation(mArrowElevation); + } + void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { if (targetBounds == null) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index ef5e501917f8..2a61445b27ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -42,7 +42,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { "ShellBackPreview"), WM_SHELL_RECENT_TASKS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), - WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + // TODO(b/282232877): turn logToLogcat to false. + WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM_SHELL), WM_SHELL_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM_SPLIT_SCREEN), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl index f819bee2d5e0..c414e708b28d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl @@ -73,8 +73,8 @@ interface ISplitScreen { /** * Starts an activity in a stage. */ - oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int position, - in Bundle options, in InstanceId instanceId) = 9; + oneway void startIntent(in PendingIntent intent, int userId, in Intent fillInIntent, + int position, in Bundle options, in InstanceId instanceId) = 9; /** * Starts tasks simultaneously in one transition. @@ -86,8 +86,8 @@ interface ISplitScreen { /** * Starts a pair of intent and task in one transition. */ - oneway void startIntentAndTask(in PendingIntent pendingIntent, in Bundle options1, int taskId, - in Bundle options2, int sidePosition, float splitRatio, + oneway void startIntentAndTask(in PendingIntent pendingIntent, int userId1, in Bundle options1, + int taskId, in Bundle options2, int sidePosition, float splitRatio, in RemoteTransition remoteTransition, in InstanceId instanceId) = 16; /** @@ -107,7 +107,7 @@ interface ISplitScreen { /** * Starts a pair of intent and task using legacy transition system. */ - oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent, + oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent, int userId1, in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12; @@ -121,18 +121,18 @@ interface ISplitScreen { /** * Start a pair of intents using legacy transition system. */ - oneway void startIntentsWithLegacyTransition(in PendingIntent pendingIntent1, + oneway void startIntentsWithLegacyTransition(in PendingIntent pendingIntent1, int userId1, in ShortcutInfo shortcutInfo1, in Bundle options1, in PendingIntent pendingIntent2, - in ShortcutInfo shortcutInfo2, in Bundle options2, int splitPosition, float splitRatio, - in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 18; + int userId2, in ShortcutInfo shortcutInfo2, in Bundle options2, int splitPosition, + float splitRatio, in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 18; /** * Start a pair of intents in one transition. */ - oneway void startIntents(in PendingIntent pendingIntent1, in ShortcutInfo shortcutInfo1, - in Bundle options1, in PendingIntent pendingIntent2, in ShortcutInfo shortcutInfo2, - in Bundle options2, int splitPosition, float splitRatio, - in RemoteTransition remoteTransition, in InstanceId instanceId) = 19; + oneway void startIntents(in PendingIntent pendingIntent1, int userId1, + in ShortcutInfo shortcutInfo1, in Bundle options1, in PendingIntent pendingIntent2, + int userId2, in ShortcutInfo shortcutInfo2, in Bundle options2, int splitPosition, + float splitRatio, in RemoteTransition remoteTransition, in InstanceId instanceId) = 19; /** * Blocking call that notifies and gets additional split-screen targets when entering diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index c654c1b0d431..34701f1db7b7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -502,7 +502,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, if (options == null) options = new Bundle(); final ActivityOptions activityOptions = ActivityOptions.fromBundle(options); - if (samePackage(packageName, getPackageName(reverseSplitPosition(position)))) { + if (samePackage(packageName, getPackageName(reverseSplitPosition(position)), + user.getIdentifier(), getUserId(reverseSplitPosition(position)))) { if (supportMultiInstancesSplit(packageName)) { activityOptions.setApplyMultipleTaskFlagForShortcut(true); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); @@ -531,7 +532,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, final String packageName1 = shortcutInfo.getPackage(); final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); - if (samePackage(packageName1, packageName2)) { + final int userId1 = shortcutInfo.getUserId(); + final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); + if (samePackage(packageName1, packageName2, userId1, userId2)) { if (supportMultiInstancesSplit(shortcutInfo.getPackage())) { activityOptions.setApplyMultipleTaskFlagForShortcut(true); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); @@ -558,7 +561,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, // NOTE: This doesn't correctly pull out packageName2 if taskId is referring to a task in // recents that hasn't launched and is not being organized final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); - if (samePackage(packageName1, packageName2)) { + final int userId1 = shortcutInfo.getUserId(); + final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); + if (samePackage(packageName1, packageName2, userId1, userId2)) { if (supportMultiInstancesSplit(packageName1)) { activityOptions.setApplyMultipleTaskFlagForShortcut(true); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); @@ -578,23 +583,24 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } /** - * See {@link #startIntent(PendingIntent, Intent, int, Bundle)} + * See {@link #startIntent(PendingIntent, int, Intent, int, Bundle)} * @param instanceId to be used by {@link SplitscreenEventLogger} */ - public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, + public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) { mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER); - startIntent(intent, fillInIntent, position, options); + startIntent(intent, userId, fillInIntent, position, options); } - private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, + private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, int userId1, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { Intent fillInIntent = null; final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent); final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); - if (samePackage(packageName1, packageName2)) { + final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); + if (samePackage(packageName1, packageName2, userId1, userId2)) { if (supportMultiInstancesSplit(packageName1)) { fillInIntent = new Intent(); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); @@ -611,15 +617,17 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId); } - private void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1, - int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, - float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + private void startIntentAndTask(PendingIntent pendingIntent, int userId1, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { Intent fillInIntent = null; final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent); // NOTE: This doesn't correctly pull out packageName2 if taskId is referring to a task in // recents that hasn't launched and is not being organized final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); - if (samePackage(packageName1, packageName2)) { + final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); + if (samePackage(packageName1, packageName2, userId1, userId2)) { if (supportMultiInstancesSplit(packageName1)) { fillInIntent = new Intent(); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); @@ -639,16 +647,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, options2, splitPosition, splitRatio, remoteTransition, instanceId); } - private void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, + private void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, int userId1, @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, - PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2, + PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { Intent fillInIntent1 = null; Intent fillInIntent2 = null; final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1); final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2); - if (samePackage(packageName1, packageName2)) { + if (samePackage(packageName1, packageName2, userId1, userId2)) { if (supportMultiInstancesSplit(packageName1)) { fillInIntent1 = new Intent(); fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); @@ -668,16 +676,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, splitPosition, splitRatio, adapter, instanceId); } - private void startIntents(PendingIntent pendingIntent1, + private void startIntents(PendingIntent pendingIntent1, int userId1, @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, - PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2, + PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { Intent fillInIntent1 = null; Intent fillInIntent2 = null; final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1); final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2); - if (samePackage(packageName1, packageName2)) { + if (samePackage(packageName1, packageName2, userId1, userId2)) { if (supportMultiInstancesSplit(packageName1)) { fillInIntent1 = new Intent(); fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); @@ -698,7 +706,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, + public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { // Flag this as a no-user-action launch to prevent sending user leaving event to the current // top activity since it's going to be put into another side of the split. This prevents the @@ -708,7 +716,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, final String packageName1 = SplitScreenUtils.getPackageName(intent); final String packageName2 = getPackageName(reverseSplitPosition(position)); - if (SplitScreenUtils.samePackage(packageName1, packageName2)) { + final int userId2 = getUserId(reverseSplitPosition(position)); + if (samePackage(packageName1, packageName2, userId1, userId2)) { if (supportMultiInstancesSplit(packageName1)) { // To prevent accumulating large number of instances in the background, reuse task // in the background with priority. @@ -761,6 +770,24 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return taskInfo != null ? SplitScreenUtils.getPackageName(taskInfo.baseIntent) : null; } + /** Retrieve user id of a specific split position if split screen is activated, otherwise + * returns the user id of the top running task. */ + private int getUserId(@SplitPosition int position) { + ActivityManager.RunningTaskInfo taskInfo; + if (isSplitScreenVisible()) { + taskInfo = getTaskInfo(position); + } else { + taskInfo = mRecentTasksOptional + .map(recentTasks -> recentTasks.getTopRunningTask()) + .orElse(null); + if (!isValidToSplit(taskInfo)) { + return -1; + } + } + + return taskInfo != null ? taskInfo.userId : -1; + } + @VisibleForTesting boolean supportMultiInstancesSplit(String packageName) { if (packageName != null) { @@ -1069,14 +1096,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, + public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, int userId1, Bundle options1, int taskId, Bundle options2, int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentAndTaskWithLegacyTransition", (controller) -> controller.startIntentAndTaskWithLegacyTransition(pendingIntent, - options1, taskId, options2, splitPosition, splitRatio, adapter, - instanceId)); + userId1, options1, taskId, options2, splitPosition, splitRatio, + adapter, instanceId)); } @Override @@ -1102,13 +1129,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1, - int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, - float splitRatio, @Nullable RemoteTransition remoteTransition, - InstanceId instanceId) { + public void startIntentAndTask(PendingIntent pendingIntent, int userId1, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentAndTask", - (controller) -> controller.startIntentAndTask(pendingIntent, options1, taskId, - options2, splitPosition, splitRatio, remoteTransition, instanceId)); + (controller) -> controller.startIntentAndTask(pendingIntent, userId1, options1, + taskId, options2, splitPosition, splitRatio, remoteTransition, + instanceId)); } @Override @@ -1122,29 +1150,29 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, + public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, int userId1, @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, - PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2, + PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentsWithLegacyTransition", (controller) -> - controller.startIntentsWithLegacyTransition(pendingIntent1, shortcutInfo1, - options1, pendingIntent2, shortcutInfo2, options2, splitPosition, - splitRatio, adapter, instanceId) + controller.startIntentsWithLegacyTransition(pendingIntent1, userId1, + shortcutInfo1, options1, pendingIntent2, userId2, shortcutInfo2, + options2, splitPosition, splitRatio, adapter, instanceId) ); } @Override - public void startIntents(PendingIntent pendingIntent1, @Nullable ShortcutInfo shortcutInfo1, - @Nullable Bundle options1, PendingIntent pendingIntent2, - @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2, - @SplitPosition int splitPosition, float splitRatio, + public void startIntents(PendingIntent pendingIntent1, int userId1, + @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, + PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2, + @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntents", (controller) -> - controller.startIntents(pendingIntent1, shortcutInfo1, - options1, pendingIntent2, shortcutInfo2, options2, + controller.startIntents(pendingIntent1, userId1, shortcutInfo1, + options1, pendingIntent2, userId2, shortcutInfo2, options2, splitPosition, splitRatio, remoteTransition, instanceId) ); } @@ -1158,11 +1186,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startIntent(PendingIntent intent, Intent fillInIntent, int position, + public void startIntent(PendingIntent intent, int userId, Intent fillInIntent, int position, @Nullable Bundle options, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntent", - (controller) -> controller.startIntent(intent, fillInIntent, position, options, - instanceId)); + (controller) -> controller.startIntent(intent, userId, fillInIntent, position, + options, instanceId)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index a2af93fc42c6..14ea86a8c0e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -20,11 +20,11 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static com.android.wm.shell.animation.Interpolators.ALPHA_IN; +import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; +import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION; import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString; @@ -36,7 +36,6 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; -import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; import android.view.WindowManager; @@ -65,7 +64,7 @@ class SplitScreenTransitions { private final Runnable mOnFinish; DismissSession mPendingDismiss = null; - TransitSession mPendingEnter = null; + EnterSession mPendingEnter = null; TransitSession mPendingResize = null; private IBinder mAnimatingTransition = null; @@ -88,6 +87,7 @@ class SplitScreenTransitions { mStageCoordinator = stageCoordinator; } + /** Play animation for enter transition or dismiss transition. */ void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -118,17 +118,19 @@ class SplitScreenTransitions { playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot, topRoot); } + /** Internal funcation of playAnimation. */ private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, @NonNull WindowContainerToken topRoot) { // Play some place-holder fade animations + final boolean isEnter = isPendingEnter(transition); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); final SurfaceControl leash = change.getLeash(); final int mode = info.getChanges().get(i).getMode(); + final int rootIdx = TransitionUtil.rootIndexFor(change, info); if (mode == TRANSIT_CHANGE) { - final int rootIdx = TransitionUtil.rootIndexFor(change, info); if (change.getParent() != null) { // This is probably reparented, so we want the parent to be immediately visible final TransitionInfo.Change parentChange = info.getChange(change.getParent()); @@ -144,17 +146,23 @@ class SplitScreenTransitions { change.getEndRelOffset().x, change.getEndRelOffset().y); } } - boolean isRootOrSplitSideRoot = change.getParent() == null - || topRoot.equals(change.getParent()); - boolean isDivider = change.getFlags() == FLAG_IS_DIVIDER_BAR; - // For enter or exit, we only want to animate side roots and the divider but not the - // top-root. - if (!isRootOrSplitSideRoot || topRoot.equals(change.getContainer()) || isDivider) { - continue; - } - if (isPendingEnter(transition) && (mainRoot.equals(change.getContainer()) - || sideRoot.equals(change.getContainer()))) { + final boolean isTopRoot = topRoot.equals(change.getContainer()); + final boolean isMainRoot = mainRoot.equals(change.getContainer()); + final boolean isSideRoot = sideRoot.equals(change.getContainer()); + final boolean isDivider = change.getFlags() == FLAG_IS_DIVIDER_BAR; + final boolean isMainChild = mainRoot.equals(change.getParent()); + final boolean isSideChild = sideRoot.equals(change.getParent()); + if (isEnter && (isMainChild || isSideChild)) { + // Reset child tasks bounds on finish. + mFinishTransaction.setPosition(leash, + change.getEndRelOffset().x, change.getEndRelOffset().y); + mFinishTransaction.setCrop(leash, null); + } else if (isTopRoot) { + // Ensure top root is visible at start. + t.setAlpha(leash, 1.f); + t.show(leash); + } else if (isEnter && isMainRoot || isSideRoot) { t.setPosition(leash, change.getEndAbsBounds().left, change.getEndAbsBounds().top); t.setWindowCrop(leash, change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); @@ -163,53 +171,56 @@ class SplitScreenTransitions { t.setLayer(leash, Integer.MAX_VALUE); t.show(leash); } + + // We want to use child tasks to animate so ignore split root container and non task + // except divider change. + if (isTopRoot || isMainRoot || isSideRoot + || (change.getTaskInfo() == null && !isDivider)) { + continue; + } + if (isEnter && mPendingEnter.mResizeAnim) { + // We will run animation in next transition so skip anim here + continue; + } else if (isPendingDismiss(transition) + && mPendingDismiss.mReason == EXIT_REASON_DRAG_DIVIDER) { + // TODO(b/280020345): need to refine animation for this but just skip anim now. + continue; + } + + // Because cross fade might be looked more flicker during animation + // (surface become black in middle of animation), we only do fade-out + // and show opening surface directly. boolean isOpening = TransitionUtil.isOpeningType(info.getType()); - if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { - // fade in - startExampleAnimation(leash, true /* show */); - } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { + if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { // fade out - if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { - // Dismissing via snap-to-top/bottom means that the dismissed task is already - // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 - // and don't animate it so it doesn't pop-in when reparented. - t.setAlpha(leash, 0.f); + if (change.getSnapshot() != null) { + // This case is happened if task is going to reparent to TDA, the origin leash + // doesn't rendor so we use snapshot to replace it animating. + t.reparent(change.getSnapshot(), info.getRoot(rootIdx).getLeash()); + // Use origin leash layer. + t.setLayer(change.getSnapshot(), info.getChanges().size() - i); + t.setPosition(change.getSnapshot(), change.getStartAbsBounds().left, + change.getStartAbsBounds().top); + t.show(change.getSnapshot()); + startFadeAnimation(change.getSnapshot(), false /* show */); } else { - startExampleAnimation(leash, false /* show */); + startFadeAnimation(leash, false /* show */); } + } else if (mode == TRANSIT_CHANGE && change.getSnapshot() != null) { + t.reparent(change.getSnapshot(), info.getRoot(rootIdx).getLeash()); + // Ensure snapshot it on the top of all transition surfaces + t.setLayer(change.getSnapshot(), info.getChanges().size() + 1); + t.setPosition(change.getSnapshot(), change.getStartAbsBounds().left, + change.getStartAbsBounds().top); + t.show(change.getSnapshot()); + startFadeAnimation(change.getSnapshot(), false /* show */); } } t.apply(); onFinish(null /* wct */, null /* wctCB */); } - void applyDismissTransition(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback, - @NonNull WindowContainerToken topRoot, - @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, - @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) { - if (mPendingDismiss.mDismissTop != STAGE_TYPE_UNDEFINED) { - mFinishCallback = finishCallback; - mAnimatingTransition = transition; - mFinishTransaction = finishTransaction; - - startTransaction.apply(); - - final SplitDecorManager topDecor = mPendingDismiss.mDismissTop == STAGE_TYPE_MAIN - ? mainDecor : sideDecor; - topDecor.fadeOutDecor(() -> { - mTransitions.getMainExecutor().execute(() -> { - onFinish(null /* wct */, null /* wctCB */); - }); - }); - } else { - playAnimation(transition, info, startTransaction, finishTransaction, - finishCallback, mainRoot, sideRoot, topRoot); - } - } - + /** Play animation for resize transition. */ void playResizeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -237,11 +248,13 @@ class SplitScreenTransitions { mAnimations.add(va); decor.setScreenshotIfNeeded(change.getSnapshot(), startTransaction); - decor.onResized(startTransaction, () -> { - mTransitions.getMainExecutor().execute(() -> { - mAnimations.remove(va); - onFinish(null /* wct */, null /* wctCB */); - }); + decor.onResized(startTransaction, animated -> { + mAnimations.remove(va); + if (animated) { + mTransitions.getMainExecutor().execute(() -> { + onFinish(null /* wct */, null /* wctCB */); + }); + } }); } } @@ -294,7 +307,7 @@ class SplitScreenTransitions { Transitions.TransitionHandler handler, @Nullable TransitionConsumedCallback consumedCallback, @Nullable TransitionFinishedCallback finishedCallback, - int extraTransitType) { + int extraTransitType, boolean resizeAnim) { if (mPendingEnter != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + " skip to start enter split transition since it already exist. "); @@ -302,7 +315,7 @@ class SplitScreenTransitions { } final IBinder transition = mTransitions.startTransition(transitType, wct, handler); setEnterTransition(transition, remoteTransition, consumedCallback, finishedCallback, - extraTransitType); + extraTransitType, resizeAnim); return transition; } @@ -311,9 +324,10 @@ class SplitScreenTransitions { @Nullable RemoteTransition remoteTransition, @Nullable TransitionConsumedCallback consumedCallback, @Nullable TransitionFinishedCallback finishedCallback, - int extraTransitType) { - mPendingEnter = new TransitSession( - transition, consumedCallback, finishedCallback, remoteTransition, extraTransitType); + int extraTransitType, boolean resizeAnim) { + mPendingEnter = new EnterSession( + transition, consumedCallback, finishedCallback, remoteTransition, extraTransitType, + resizeAnim); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + " deduced Enter split screen"); @@ -436,78 +450,28 @@ class SplitScreenTransitions { } } - // TODO(shell-transitions): real animations - private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) { + private void startFadeAnimation(@NonNull SurfaceControl leash, boolean show) { final float end = show ? 1.f : 0.f; final float start = 1.f - end; final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); final ValueAnimator va = ValueAnimator.ofFloat(start, end); - va.setDuration(500); + va.setDuration(FADE_DURATION); + va.setInterpolator(show ? ALPHA_IN : ALPHA_OUT); va.addUpdateListener(animation -> { float fraction = animation.getAnimatedFraction(); transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); transaction.apply(); }); - final Runnable finisher = () -> { - transaction.setAlpha(leash, end); - transaction.apply(); - mTransactionPool.release(transaction); - mTransitions.getMainExecutor().execute(() -> { - mAnimations.remove(va); - onFinish(null /* wct */, null /* wctCB */); - }); - }; va.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationCancel(Animator animation) { - finisher.run(); - } - }); - mAnimations.add(va); - mTransitions.getAnimExecutor().execute(va::start); - } - - // TODO(shell-transitions): real animations - private void startExampleResizeAnimation(@NonNull SurfaceControl leash, - @NonNull Rect startBounds, @NonNull Rect endBounds) { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f); - va.setDuration(500); - va.addUpdateListener(animation -> { - float fraction = animation.getAnimatedFraction(); - transaction.setWindowCrop(leash, - (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction), - (int) (startBounds.height() * (1.f - fraction) - + endBounds.height() * fraction)); - transaction.setPosition(leash, - startBounds.left * (1.f - fraction) + endBounds.left * fraction, - startBounds.top * (1.f - fraction) + endBounds.top * fraction); - transaction.apply(); - }); - final Runnable finisher = () -> { - transaction.setWindowCrop(leash, 0, 0); - transaction.setPosition(leash, endBounds.left, endBounds.top); - transaction.apply(); - mTransactionPool.release(transaction); - mTransitions.getMainExecutor().execute(() -> { - mAnimations.remove(va); - onFinish(null /* wct */, null /* wctCB */); - }); - }; - va.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationCancel(Animator animation) { - finisher.run(); + transaction.setAlpha(leash, end); + transaction.apply(); + mTransactionPool.release(transaction); + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(null /* wct */, null /* wctCB */); + }); } }); mAnimations.add(va); @@ -596,6 +560,21 @@ class SplitScreenTransitions { } } + /** Bundled information of enter transition. */ + class EnterSession extends TransitSession { + final boolean mResizeAnim; + + EnterSession(IBinder transition, + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback, + @Nullable RemoteTransition remoteTransition, + int extraTransitType, boolean resizeAnim) { + super(transition, consumedCallback, finishedCallback, remoteTransition, + extraTransitType); + this.mResizeAnim = resizeAnim; + } + } + /** Bundled information of dismiss transition. */ class DismissSession extends TransitSession { final int mReason; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 749549d1ca55..bf20567834e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -23,6 +23,7 @@ import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVIT import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_OPENING; @@ -311,6 +312,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, transitions.addHandler(this); mSplitUnsupportedToast = Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); + // With shell transition, we should update recents tile each callback so set this to true by + // default. + mShouldUpdateRecents = ENABLE_SHELL_TRANSITIONS; } @VisibleForTesting @@ -391,7 +395,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this, null /* consumedCallback */, null /* finishedCallback */, isSplitScreenVisible() - ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN); + ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN, + !mIsDropEntering); } else { mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { @@ -499,7 +504,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(wct, null /* taskInfo */, position); mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this, - null /* consumedCallback */, null /* finishedCallback */, extraTransitType); + null /* consumedCallback */, null /* finishedCallback */, extraTransitType, + !mIsDropEntering); } /** Launches an activity into split by legacy transition. */ @@ -576,7 +582,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } - prepareEvictChildTasksIfSplitActive(wct); setSideStagePosition(splitPosition, wct); options1 = options1 != null ? options1 : new Bundle(); addActivityOptions(options1, mSideStage); @@ -598,7 +603,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } - prepareEvictChildTasksIfSplitActive(wct); setSideStagePosition(splitPosition, wct); options1 = options1 != null ? options1 : new Bundle(); addActivityOptions(options1, mSideStage); @@ -619,7 +623,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } - prepareEvictChildTasksIfSplitActive(wct); setSideStagePosition(splitPosition, wct); options1 = options1 != null ? options1 : new Bundle(); addActivityOptions(options1, mSideStage); @@ -657,7 +660,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.startEnterTransition( TRANSIT_TO_FRONT, wct, remoteTransition, this, null, null, - TRANSIT_SPLIT_SCREEN_PAIR_OPEN); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); setEnterInstanceId(instanceId); } @@ -685,7 +688,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, false /* reparent */); } - prepareEvictChildTasksIfSplitActive(wct); mSplitLayout.setDivideRatio(splitRatio); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); @@ -709,7 +711,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.startEnterTransition( TRANSIT_TO_FRONT, wct, remoteTransition, this, null, null, - TRANSIT_SPLIT_SCREEN_PAIR_OPEN); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); setEnterInstanceId(instanceId); } @@ -1069,24 +1071,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - prepareEvictNonOpeningChildTasks(SPLIT_POSITION_TOP_OR_LEFT, apps, evictWct); - prepareEvictNonOpeningChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, apps, evictWct); + mMainStage.evictNonOpeningChildren(apps, evictWct); + mSideStage.evictNonOpeningChildren(apps, evictWct); mSyncQueue.queue(evictWct); } - - /** - * Collects all the current child tasks of a specific split and prepares transaction to evict - * them to display. - */ - void prepareEvictChildTasks(@SplitPosition int position, WindowContainerTransaction wct) { - if (position == mSideStagePosition) { - mSideStage.evictAllChildren(wct); - } else { - mMainStage.evictAllChildren(wct); - } - } - void prepareEvictNonOpeningChildTasks(@SplitPosition int position, RemoteAnimationTarget[] apps, WindowContainerTransaction wct) { if (position == mSideStagePosition) { @@ -1101,13 +1090,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.evictInvisibleChildren(wct); } - void prepareEvictChildTasksIfSplitActive(WindowContainerTransaction wct) { - if (mMainStage.isActive()) { - mMainStage.evictAllChildren(wct); - mSideStage.evictAllChildren(wct); - } - } - Bundle resolveStartStage(@StageType int stage, @SplitPosition int position, @Nullable Bundle options, @Nullable WindowContainerTransaction wct) { switch (stage) { @@ -1464,18 +1446,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void prepareExitSplitScreen(@StageType int stageToTop, @NonNull WindowContainerTransaction wct) { if (!mMainStage.isActive()) return; - // Set the dismiss-to-top side to fullscreen for dismiss transition. - // Reparent the non-dismiss-to-top side to properly update its visibility. - if (stageToTop == STAGE_TYPE_MAIN) { - wct.setBounds(mMainStage.mRootTaskInfo.token, null /* bounds */); - mSideStage.removeAllTasks(wct, false /* toTop */); - } else if (stageToTop == STAGE_TYPE_SIDE) { - wct.setBounds(mSideStage.mRootTaskInfo.token, null /* bounds */); - mMainStage.deactivate(wct, false /* toTop */); - } else { - mSideStage.removeAllTasks(wct, false /* toTop */); - mMainStage.deactivate(wct, false /* toTop */); - } + mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); + mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN); wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* reparentLeafTaskIfRelaunch */); } @@ -1500,29 +1472,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void prepareBringSplit(WindowContainerTransaction wct, @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition) { - StageTaskListener targetStage; - if (isSplitScreenVisible()) { - // If the split screen is foreground, retrieves target stage based on position. - targetStage = startPosition == mSideStagePosition ? mSideStage : mMainStage; - } else { - targetStage = mSideStage; - } - if (taskInfo != null) { wct.startTask(taskInfo.taskId, resolveStartStage(STAGE_TYPE_UNDEFINED, startPosition, null, wct)); } - // If running background, we need to reparent current top visible task to another stage - // and evict all tasks current under its. + // If running background, we need to reparent current top visible task to main stage. if (!isSplitScreenVisible()) { - // Recreate so we need to reset position rather than keep position of background split. - mSplitLayout.resetDividerPosition(); - updateWindowBounds(mSplitLayout, wct); - final StageTaskListener anotherStage = targetStage == mMainStage - ? mSideStage : mMainStage; - anotherStage.reparentTopTask(wct); - wct.reorder(mRootTaskInfo.token, true); - setRootForceTranslucent(false, wct); + mMainStage.reparentTopTask(wct); + prepareSplitLayout(wct); } } @@ -1538,8 +1495,22 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.addTask(taskInfo, wct); } mMainStage.activate(wct, true /* includingTopTask */); - mSplitLayout.resetDividerPosition(); + prepareSplitLayout(wct); + } + + private void prepareSplitLayout(WindowContainerTransaction wct) { + if (mIsDropEntering) { + mSplitLayout.resetDividerPosition(); + } else { + mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); + } updateWindowBounds(mSplitLayout, wct); + if (!mIsDropEntering) { + // Reset its smallest width dp to avoid is change layout before it actually resized to + // split bounds. + wct.setSmallestScreenWidthDp(mMainStage.mRootTaskInfo.token, + SMALLEST_SCREEN_WIDTH_DP_UNDEFINED); + } wct.reorder(mRootTaskInfo.token, true); setRootForceTranslucent(false, wct); } @@ -1554,7 +1525,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); t.show(mRootTaskLeash); setSplitsVisible(true); - mShouldUpdateRecents = true; + mIsDropEntering = false; updateRecentTasksSplitPair(); if (!mLogger.hasStartedSession()) { mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), @@ -1784,18 +1755,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Handle entering split screen while there is a split pair running in the background. if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive() && mSplitRequest == null) { - if (mIsDropEntering) { - mSplitLayout.resetDividerPosition(); - } else { - mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); - } final WindowContainerTransaction wct = new WindowContainerTransaction(); - mMainStage.reparentTopTask(wct); + prepareEnterSplitScreen(wct); mMainStage.evictAllChildren(wct); mSideStage.evictOtherChildren(wct, taskId); - updateWindowBounds(mSplitLayout, wct); - wct.reorder(mRootTaskInfo.token, true); - setRootForceTranslucent(false, wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { @@ -1990,20 +1953,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, exitSplitScreen(null /* childrenToTop */, EXIT_REASON_APP_FINISHED); } } else if (isSideStage && hasChildren && !mMainStage.isActive()) { - mSplitLayout.init(); - final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (mIsDropEntering) { - prepareEnterSplitScreen(wct); - } else { - // TODO (b/238697912) : Add the validation to prevent entering non-recovered status - onSplitScreenEnter(); - mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); - mMainStage.activate(wct, true /* includingTopTask */); - updateWindowBounds(mSplitLayout, wct); - wct.reorder(mRootTaskInfo.token, true); - setRootForceTranslucent(false, wct); - } + prepareEnterSplitScreen(wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { @@ -2084,18 +2035,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Reset this flag every time onLayoutSizeChanged. mShowDecorImmediately = false; - if (!ENABLE_SHELL_TRANSITIONS) { - // Only need screenshot for legacy case because shell transition should screenshot - // itself during transition. - final SurfaceControl.Transaction startT = mTransactionPool.acquire(); - mMainStage.screenshotIfNeeded(startT); - mSideStage.screenshotIfNeeded(startT); - mTransactionPool.release(startT); - } - final WindowContainerTransaction wct = new WindowContainerTransaction(); boolean sizeChanged = updateWindowBounds(layout, wct); - if (!sizeChanged) return; + if (!sizeChanged) { + // We still need to resize on decor for ensure all current status clear. + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + mMainStage.onResized(t); + mSideStage.onResized(t); + mTransactionPool.release(t); + return; + } sendOnBoundsChanged(); if (ENABLE_SHELL_TRANSITIONS) { @@ -2103,6 +2052,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.startResizeTransition(wct, this, (finishWct, t) -> mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish")); } else { + // Only need screenshot for legacy case because shell transition should screenshot + // itself during transition. + final SurfaceControl.Transaction startT = mTransactionPool.acquire(); + mMainStage.screenshotIfNeeded(startT); + mSideStage.screenshotIfNeeded(startT); + mTransactionPool.release(startT); + mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { updateSurfaceBounds(layout, t, false /* applyResizingOffset */); @@ -2183,6 +2139,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } mDisplayLayout.set(mDisplayController.getDisplayLayout(displayId)); + + if (mSplitLayout != null && mSplitLayout.isDensityChanged(newConfig.densityDpi) + && mMainStage.isActive() + && mSplitLayout.updateConfiguration(newConfig) + && ENABLE_SHELL_TRANSITIONS) { + mSplitLayout.update(null /* t */); + onLayoutSizeChanged(mSplitLayout); + } } void updateSurfaces(SurfaceControl.Transaction transaction) { @@ -2352,7 +2316,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(out); mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), null /* consumedCallback */, null /* finishedCallback */, - 0 /* extraTransitType */); + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, !mIsDropEntering); } } return out; @@ -2377,6 +2341,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " so make sure split-screen state is cleaned-up. " + "mainStageCount=%d sideStageCount=%d", mMainStage.getChildCount(), mSideStage.getChildCount()); + if (triggerTask != null) { + mRecentTasks.ifPresent( + recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId)); + } prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, outWCT); } } @@ -2565,13 +2533,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } else if (mSplitTransitions.isPendingDismiss(transition)) { shouldAnimate = startPendingDismissAnimation( mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction); - if (shouldAnimate) { - mSplitTransitions.applyDismissTransition(transition, info, - startTransaction, finishTransaction, finishCallback, mRootTaskInfo.token, - mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token, - mMainStage.getSplitDecorManager(), mSideStage.getSplitDecorManager()); - return true; - } } else if (mSplitTransitions.isPendingResize(transition)) { mSplitTransitions.playResizeAnimation(transition, info, startTransaction, finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token, @@ -2598,7 +2559,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } private boolean startPendingEnterAnimation( - @NonNull SplitScreenTransitions.TransitSession enterTransition, + @NonNull SplitScreenTransitions.EnterSession enterTransition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { // First, verify that we actually have opened apps in both splits. @@ -2622,7 +2583,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) { if (mainChild == null && sideChild == null) { Log.w(TAG, "Launched a task in split, but didn't receive any task in transition."); - mSplitTransitions.mPendingEnter.cancel(null /* finishedCb */); + mSplitTransitions.mPendingEnter.cancel((cancelWct, cancelT) + -> prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, cancelWct)); return true; } } else { @@ -2632,10 +2594,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN : (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED); mSplitTransitions.mPendingEnter.cancel( - (cancelWct, cancelT) -> { - mSideStage.removeAllTasks(cancelWct, dismissTop == STAGE_TYPE_SIDE); - mMainStage.deactivate(cancelWct, dismissTop == STAGE_TYPE_MAIN); - }); + (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct)); return true; } } @@ -2644,27 +2603,43 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // transitions locally, but remotes (like Launcher) may get confused if they were // depending on listener callbacks. This can happen because task-organizer callbacks // aren't serialized with transition callbacks. + // This usually occurred on app use trampoline launch new task and finish itself. // TODO(b/184679596): Find a way to either include task-org information in // the transition, or synchronize task-org callbacks. - if (mainChild != null && !mMainStage.containsTask(mainChild.getTaskInfo().taskId)) { + final boolean mainNotContainOpenTask = + mainChild != null && !mMainStage.containsTask(mainChild.getTaskInfo().taskId); + final boolean sideNotContainOpenTask = + sideChild != null && !mSideStage.containsTask(sideChild.getTaskInfo().taskId); + if (mainNotContainOpenTask) { Log.w(TAG, "Expected onTaskAppeared on " + mMainStage + " to have been called with " + mainChild.getTaskInfo().taskId + " before startAnimation()."); } - if (sideChild != null && !mSideStage.containsTask(sideChild.getTaskInfo().taskId)) { + if (sideNotContainOpenTask) { Log.w(TAG, "Expected onTaskAppeared on " + mSideStage + " to have been called with " + sideChild.getTaskInfo().taskId + " before startAnimation()."); } - final TransitionInfo.Change finalMainChild = mainChild; final TransitionInfo.Change finalSideChild = sideChild; enterTransition.setFinishedCallback((callbackWct, callbackT) -> { if (finalMainChild != null) { - mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId); + if (!mainNotContainOpenTask) { + mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId); + } else { + mMainStage.evictInvisibleChildren(callbackWct); + } } if (finalSideChild != null) { - mSideStage.evictOtherChildren(callbackWct, finalSideChild.getTaskInfo().taskId); + if (!sideNotContainOpenTask) { + mSideStage.evictOtherChildren(callbackWct, finalSideChild.getTaskInfo().taskId); + } else { + mSideStage.evictInvisibleChildren(callbackWct); + } + } + if (enterTransition.mResizeAnim) { + mShowDecorImmediately = true; + mSplitLayout.flingDividerToCenter(); } }); @@ -2760,22 +2735,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRecentTasks.ifPresent(recentTasks -> { // Notify recents if we are exiting in a way that breaks the pair, and disable further // updates to splits in the recents until we enter split again - if (shouldBreakPairedTaskInRecents(dismissReason) && mShouldUpdateRecents) { - if (toStage == STAGE_TYPE_UNDEFINED) { + if (shouldBreakPairedTaskInRecents(dismissReason)) { for (TransitionInfo.Change change : info.getChanges()) { final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo != null - && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + && taskInfo.getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW) { recentTasks.removeSplitPair(taskInfo.taskId); } } - } else { - recentTasks.removeSplitPair(mMainStage.getTopVisibleChildTaskId()); - recentTasks.removeSplitPair(mSideStage.getTopVisibleChildTaskId()); - } } }); - mShouldUpdateRecents = false; mSplitRequest = null; // Update local states. @@ -2817,18 +2786,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.release(t); mSplitTransitions.mPendingDismiss = null; return false; - } else { - final @SplitScreen.StageType int dismissTop = dismissTransition.mDismissTop; - // Reparent all tasks after dismiss transition finished. - dismissTransition.setFinishedCallback( - new SplitScreenTransitions.TransitionFinishedCallback() { - @Override - public void onFinished(WindowContainerTransaction wct, - SurfaceControl.Transaction t) { - mSideStage.removeAllTasks(wct, dismissTop == STAGE_TYPE_SIDE); - mMainStage.deactivate(wct, dismissTop == STAGE_TYPE_MAIN); - } - }); } addDividerBarToTransition(info, false /* show */); @@ -2868,6 +2825,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void addDividerBarToTransition(@NonNull TransitionInfo info, boolean show) { final SurfaceControl leash = mSplitLayout.getDividerLeash(); + if (leash == null || !leash.isValid()) { + Slog.w(TAG, "addDividerBarToTransition but leash was released or not be created"); + return; + } + final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash); mSplitLayout.getRefDividerBounds(mTempRect1); barChange.setParent(mRootTaskInfo.token); @@ -2932,9 +2894,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!isSplitScreenVisible()) { mIsDropEntering = true; } - if (!isSplitScreenVisible()) { + if (!isSplitScreenVisible() && !ENABLE_SHELL_TRANSITIONS) { // If split running background, exit split first. - // TODO(b/280392203) : skip doing this on shell transition once this bug is fixed. + // Skip this on shell transition due to we could evict existing tasks on transition + // finished. exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); } mLogger.enterRequestedByDrag(position, dragSessionId); @@ -2944,9 +2907,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, * Sets info to be logged when splitscreen is next entered. */ public void onRequestToSplit(InstanceId sessionId, int enterReason) { - if (!isSplitScreenVisible()) { + if (!isSplitScreenVisible() && !ENABLE_SHELL_TRANSITIONS) { // If split running background, exit split first. - // TODO(b/280392203) : skip doing this on shell transition once this bug is fixed. + // Skip this on shell transition due to we could evict existing tasks on transition + // finished. exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); } mLogger.enterRequested(sessionId, enterReason); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index fe2faaf79a1a..2e7fca3f2b46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -135,6 +135,21 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { } /** + * Looks through the pending transitions for a opening transaction that matches the provided + * `taskView`. + * @param taskView the pending transition should be for this. + */ + private PendingTransition findPendingOpeningTransition(TaskViewTaskController taskView) { + for (int i = mPending.size() - 1; i >= 0; --i) { + if (mPending.get(i).mTaskView != taskView) continue; + if (TransitionUtil.isOpeningType(mPending.get(i).mType)) { + return mPending.get(i); + } + } + return null; + } + + /** * Looks through the pending transitions for one matching `taskView`. * @param taskView the pending transition should be for this. * @param type the type of transition it's looking for @@ -149,6 +164,19 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { return null; } + /** + * Returns all the pending transitions for a given `taskView`. + * @param taskView the pending transition should be for this. + */ + ArrayList<PendingTransition> findAllPending(TaskViewTaskController taskView) { + ArrayList<PendingTransition> list = new ArrayList<>(); + for (int i = mPending.size() - 1; i >= 0; --i) { + if (mPending.get(i).mTaskView != taskView) continue; + list.add(mPending.get(i)); + } + return list; + } + private PendingTransition findPending(IBinder claimed) { for (int i = 0; i < mPending.size(); ++i) { if (mPending.get(i).mClaimed != claimed) continue; @@ -249,9 +277,10 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { // Task view isn't visible, the bounds will next visibility update. return; } - if (hasPending()) { - // There is already a transition in-flight, the window bounds will be set in - // prepareOpenAnimation. + PendingTransition pendingOpen = findPendingOpeningTransition(taskView); + if (pendingOpen != null) { + // There is already an opening transition in-flight, the window bounds will be + // set in prepareOpenAnimation (via the window crop) if needed. return; } WindowContainerTransaction wct = new WindowContainerTransaction(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 6fa1861a23e2..863b5ab73a7d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -40,6 +40,7 @@ import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -63,6 +64,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, private PipTransitionController mPipHandler; private RecentsTransitionHandler mRecentsHandler; private StageCoordinator mSplitHandler; + private final KeyguardTransitionHandler mKeyguardHandler; private static class MixedTransition { static final int TYPE_ENTER_PIP_FROM_SPLIT = 1; @@ -76,6 +78,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, /** Recents transition while split-screen foreground. */ static final int TYPE_RECENTS_DURING_SPLIT = 4; + /** Keyguard exit/occlude/unocclude transition. */ + static final int TYPE_KEYGUARD = 5; + /** The default animation for this mixed transition. */ static final int ANIM_TYPE_DEFAULT = 0; @@ -126,8 +131,10 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player, Optional<SplitScreenController> splitScreenControllerOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, - Optional<RecentsTransitionHandler> recentsHandlerOptional) { + Optional<RecentsTransitionHandler> recentsHandlerOptional, + KeyguardTransitionHandler keyguardHandler) { mPlayer = player; + mKeyguardHandler = keyguardHandler; if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent() && splitScreenControllerOptional.isPresent()) { // Add after dependencies because it is higher priority @@ -263,12 +270,26 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + MixedTransition mixed = null; for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { if (mActiveTransitions.get(i).mTransition != transition) continue; mixed = mActiveTransitions.get(i); break; } + + // Offer Keyguard the opportunity to take over lock transitions - ideally we could know by + // the time of handleRequest, but we need more information than is available at that time. + if (KeyguardTransitionHandler.handles(info)) { + if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Converting mixed transition into a keyguard transition"); + onTransitionConsumed(transition, false, null); + } + mixed = new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition); + mActiveTransitions.add(mixed); + } + if (mixed == null) return false; if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { @@ -282,6 +303,9 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { return animateRecentsDuringSplit(mixed, info, startTransaction, finishTransaction, finishCallback); + } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) { + return animateKeyguard(mixed, info, startTransaction, finishTransaction, + finishCallback); } else { mActiveTransitions.remove(mixed); throw new IllegalStateException("Starting mixed animation without a known mixed type? " @@ -533,6 +557,27 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, return handled; } + private boolean animateKeyguard(@NonNull final MixedTransition mixed, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + boolean consumed = mKeyguardHandler.startAnimation( + mixed.mTransition, info, startTransaction, finishTransaction, finishCallback); + if (!consumed) { + return false; + } + // Sync pip state. + if (mPipHandler != null) { + // We don't know when to apply `startTransaction` so use a separate transaction here. + // This should be fine because these surface properties are independent. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mPipHandler.syncPipSurfaceState(info, t, finishTransaction); + t.apply(); + } + return true; + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @@ -574,6 +619,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, } mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) { + mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); } else { throw new IllegalStateException("Playing a mixed transition with unknown type? " + mixed.mType); @@ -597,6 +644,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) { mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); + } else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) { + mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 21dca95d056a..6a2468a7eaa2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -435,6 +435,23 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { backgroundColorForTransition = uiContext.getColor(R.color.overview_background); } + if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN + && TransitionUtil.isOpeningType(info.getType())) { + // Need to flip the z-order of opening/closing because the WALLPAPER_OPEN + // always animates the closing task over the opening one while + // traditionally, an OPEN transition animates the opening over the closing. + + // See Transitions#setupAnimHierarchy for details about these variables. + final int numChanges = info.getChanges().size(); + final int zSplitLine = numChanges + 1; + if (TransitionUtil.isOpeningType(mode)) { + final int layer = zSplitLine - i; + startTransaction.setLayer(change.getLeash(), layer); + } else if (TransitionUtil.isClosingType(mode)) { + final int layer = zSplitLine + numChanges - i; + startTransaction.setLayer(change.getLeash(), layer); + } + } } final float cornerRadius; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index ef2a511177b2..a242c72db8b3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -94,8 +94,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (!Transitions.SHELL_TRANSITIONS_ROTATION && TransitionUtil.hasDisplayChange(info) - && !TransitionUtil.alwaysReportToKeyguard(info)) { + if (!Transitions.SHELL_TRANSITIONS_ROTATION && TransitionUtil.hasDisplayChange(info)) { // Note that if the remote doesn't have permission ACCESS_SURFACE_FLINGER, some // operations of the start transaction may be ignored. mRequestedRemotes.remove(transition); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index ab27c55f20dc..f33b0778a1b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -71,6 +71,7 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -686,7 +687,11 @@ public class Transitions implements RemoteCallable<Transitions>, active.mToken, info, active.mStartT, active.mFinishT); } - if (info.getRootCount() == 0 && !TransitionUtil.alwaysReportToKeyguard(info)) { + /* + * Some transitions we always need to report to keyguard even if they are empty. + * TODO (b/274954192): Remove this once keyguard dispatching fully moves to Shell. + */ + if (info.getRootCount() == 0 && !KeyguardTransitionHandler.handles(info)) { // No root-leashes implies that the transition is empty/no-op, so just do // housekeeping and return. ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots in %s so" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java index 402b0ce7c87c..143b42a1ec96 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java @@ -24,9 +24,7 @@ import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -85,23 +83,6 @@ public class TransitionUtil { return false; } - /** - * Some transitions we always need to report to keyguard even if they are empty. - * TODO (b/274954192): Remove this once keyguard dispatching moves to Shell. - */ - public static boolean alwaysReportToKeyguard(TransitionInfo info) { - // occlusion status of activities can change while screen is off so there will be no - // visibility change but we still need keyguardservice to be notified. - if (info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE) return true; - - // It's possible for some activities to stop with bad timing (esp. since we can't yet - // queue activity transitions initiated by apps) that results in an empty transition for - // keyguard going-away. In general, we should should always report Keyguard-going-away. - if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) return true; - - return false; - } - /** Returns `true` if `change` is a wallpaper. */ public static boolean isWallpaper(TransitionInfo.Change change) { return (change.getTaskInfo() == null) @@ -195,7 +176,14 @@ public class TransitionUtil { } // Put all the OPEN/SHOW on top - if (TransitionUtil.isOpeningType(mode)) { + if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + // Wallpaper is always at the bottom, opening wallpaper on top of closing one. + if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) { + t.setLayer(leash, -zSplitLine + info.getChanges().size() - layer); + } else { + t.setLayer(leash, -zSplitLine - layer); + } + } else if (TransitionUtil.isOpeningType(mode)) { if (isOpening) { t.setLayer(leash, zSplitLine + info.getChanges().size() - layer); if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) { @@ -252,11 +240,20 @@ public class TransitionUtil { public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, TransitionInfo info, SurfaceControl.Transaction t, @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) { + return newTarget(change, order, false /* forceTranslucent */, info, t, leashMap); + } + + /** + * Creates a new RemoteAnimationTarget from the provided change info + */ + public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, + boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t, + @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) { final SurfaceControl leash = createLeash(info, change, order, t); if (leashMap != null) { leashMap.put(change.getLeash(), leash); } - return newTarget(change, order, leash); + return newTarget(change, order, leash, forceTranslucent); } /** @@ -264,6 +261,14 @@ public class TransitionUtil { */ public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, SurfaceControl leash) { + return newTarget(change, order, leash, false /* forceTranslucent */); + } + + /** + * Creates a new RemoteAnimationTarget from the provided change and leash + */ + public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, + SurfaceControl leash, boolean forceTranslucent) { if (isDividerBar(change)) { return getDividerTarget(change, leash); } @@ -293,7 +298,7 @@ public class TransitionUtil { // TODO: once we can properly sync transactions across process, // then get rid of this leash. leash, - (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0, + forceTranslucent || (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0, null, // TODO(shell-transitions): we need to send content insets? evaluate how its used. new Rect(0, 0, 0, 0), @@ -312,6 +317,7 @@ public class TransitionUtil { target.setWillShowImeOnTarget( (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0); target.setRotationChange(change.getEndRotation() - change.getStartRotation()); + target.backgroundColor = change.getBackgroundColor(); return target; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 54babce13205..9fd57d7e1201 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -104,6 +104,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final InputMonitorFactory mInputMonitorFactory; private TaskOperations mTaskOperations; private final Supplier<SurfaceControl.Transaction> mTransactionFactory; + private final Transitions mTransitions; private Optional<SplitScreenController> mSplitScreenController; @@ -118,6 +119,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { ShellTaskOrganizer taskOrganizer, DisplayController displayController, SyncTransactionQueue syncQueue, + Transitions transitions, Optional<DesktopModeController> desktopModeController, Optional<DesktopTasksController> desktopTasksController, Optional<SplitScreenController> splitScreenController) { @@ -128,6 +130,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { taskOrganizer, displayController, syncQueue, + transitions, desktopModeController, desktopTasksController, splitScreenController, @@ -144,6 +147,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { ShellTaskOrganizer taskOrganizer, DisplayController displayController, SyncTransactionQueue syncQueue, + Transitions transitions, Optional<DesktopModeController> desktopModeController, Optional<DesktopTasksController> desktopTasksController, Optional<SplitScreenController> splitScreenController, @@ -158,6 +162,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDisplayController = displayController; mSplitScreenController = splitScreenController; mSyncQueue = syncQueue; + mTransitions = transitions; mDesktopModeController = desktopModeController; mDesktopTasksController = desktopTasksController; @@ -818,7 +823,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else { windowDecoration.createResizeVeil(); return new VeiledResizeTaskPositioner(mTaskOrganizer, windowDecoration, - mDisplayController, disallowedAreaForEndBounds, mDragStartListener); + mDisplayController, disallowedAreaForEndBounds, mDragStartListener, + mTransitions); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 95ed42a222b8..ce11b2604559 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -296,15 +296,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /** * Fade in the resize veil */ - void showResizeVeil() { - mResizeVeil.showVeil(mTaskSurface); + void showResizeVeil(Rect taskBounds) { + mResizeVeil.showVeil(mTaskSurface, taskBounds); } /** * Set new bounds for the resize veil */ void updateResizeVeil(Rect newBounds) { - mResizeVeil.relayout(newBounds); + mResizeVeil.updateResizeVeil(newBounds); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java index 4899453ac991..82771095cd82 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java @@ -44,7 +44,7 @@ import java.util.function.Supplier; * Creates and updates a veil that covers task contents on resize. */ public class ResizeVeil { - private static final int RESIZE_ALPHA_DURATION = 200; + private static final int RESIZE_ALPHA_DURATION = 100; private final Context mContext; private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; @@ -104,7 +104,7 @@ public class ResizeVeil { /** * Animate veil's alpha to 1, fading it in. */ - public void showVeil(SurfaceControl parentSurface) { + public void showVeil(SurfaceControl parentSurface, Rect taskBounds) { // Parent surface can change, ensure it is up to date. SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); if (!parentSurface.equals(mParentSurface)) { @@ -115,8 +115,6 @@ public class ResizeVeil { int backgroundColorId = getBackgroundColorId(); mViewHost.getView().setBackgroundColor(mContext.getColor(backgroundColorId)); - t.show(mVeilSurface) - .apply(); final ValueAnimator animator = new ValueAnimator(); animator.setFloatValues(0f, 1f); animator.setDuration(RESIZE_ALPHA_DURATION); @@ -124,19 +122,32 @@ public class ResizeVeil { t.setAlpha(mVeilSurface, animator.getAnimatedFraction()); t.apply(); }); - animator.start(); + + relayout(taskBounds, t); + t.show(mVeilSurface) + .addTransactionCommittedListener(mContext.getMainExecutor(), () -> animator.start()) + .setAlpha(mVeilSurface, 0); + mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); } /** * Update veil bounds to match bounds changes. * @param newBounds bounds to update veil to. */ - public void relayout(Rect newBounds) { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + private void relayout(Rect newBounds, SurfaceControl.Transaction t) { mViewHost.relayout(newBounds.width(), newBounds.height()); t.setWindowCrop(mVeilSurface, newBounds.width(), newBounds.height()); t.setPosition(mParentSurface, newBounds.left, newBounds.top); t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height()); + } + + /** + * Calls relayout to update task and veil bounds. + * @param newBounds bounds to update veil to. + */ + public void updateResizeVeil(Rect newBounds) { + SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + relayout(newBounds, t); mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); } @@ -144,7 +155,6 @@ public class ResizeVeil { * Animate veil's alpha to 0, fading it out. */ public void hideVeil() { - final View resizeVeilView = mViewHost.getView(); final ValueAnimator animator = new ValueAnimator(); animator.setFloatValues(1, 0); animator.setDuration(RESIZE_ALPHA_DURATION); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index 3a3ac4ca7d0c..58c78e6a5b9f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -16,13 +16,22 @@ package com.android.wm.shell.windowdecor; +import static android.view.WindowManager.TRANSIT_CHANGE; + import android.graphics.PointF; import android.graphics.Rect; +import android.os.IBinder; import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.transition.Transitions; import java.util.function.Supplier; @@ -32,12 +41,14 @@ import java.util.function.Supplier; * If the drag is resizing the task, we resize the veil instead. * If the drag is repositioning, we update in the typical manner. */ -public class VeiledResizeTaskPositioner implements DragPositioningCallback { +public class VeiledResizeTaskPositioner implements DragPositioningCallback, + Transitions.TransitionHandler { private DesktopModeWindowDecoration mDesktopWindowDecoration; private ShellTaskOrganizer mTaskOrganizer; private DisplayController mDisplayController; private DragPositioningCallbackUtility.DragStartListener mDragStartListener; + private final Transitions mTransitions; private final Rect mStableBounds = new Rect(); private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mRepositionStartPoint = new PointF(); @@ -46,28 +57,29 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback { // finalize the bounds there using WCT#setBounds private final Rect mDisallowedAreaForEndBounds = new Rect(); private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; - private boolean mHasDragResized; private int mCtrlType; public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, DesktopModeWindowDecoration windowDecoration, DisplayController displayController, Rect disallowedAreaForEndBounds, - DragPositioningCallbackUtility.DragStartListener dragStartListener) { + DragPositioningCallbackUtility.DragStartListener dragStartListener, + Transitions transitions) { this(taskOrganizer, windowDecoration, displayController, disallowedAreaForEndBounds, - dragStartListener, SurfaceControl.Transaction::new); + dragStartListener, SurfaceControl.Transaction::new, transitions); } public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, DesktopModeWindowDecoration windowDecoration, DisplayController displayController, Rect disallowedAreaForEndBounds, DragPositioningCallbackUtility.DragStartListener dragStartListener, - Supplier<SurfaceControl.Transaction> supplier) { + Supplier<SurfaceControl.Transaction> supplier, Transitions transitions) { mTaskOrganizer = taskOrganizer; mDesktopWindowDecoration = windowDecoration; mDisplayController = displayController; mDragStartListener = dragStartListener; mDisallowedAreaForEndBounds.set(disallowedAreaForEndBounds); mTransactionSupplier = supplier; + mTransitions = transitions; } @Override @@ -77,14 +89,13 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback { mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); mRepositionStartPoint.set(x, y); if (isResizing()) { - mDesktopWindowDecoration.showResizeVeil(); + mDesktopWindowDecoration.showResizeVeil(mTaskBoundsAtDragStart); if (!mDesktopWindowDecoration.mTaskInfo.isFocused) { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true); mTaskOrganizer.applyTransaction(wct); } } - mHasDragResized = false; mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId); mRepositionTaskBounds.set(mTaskBoundsAtDragStart); } @@ -96,7 +107,6 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback { mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, mDisplayController, mDesktopWindowDecoration)) { mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); - mHasDragResized = true; } else if (mCtrlType == CTRL_TYPE_UNDEFINED) { final SurfaceControl.Transaction t = mTransactionSupplier.get(); DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration, @@ -111,18 +121,23 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback { PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint); if (isResizing()) { - if (mHasDragResized) { + if (!mTaskBoundsAtDragStart.equals(mRepositionTaskBounds)) { DragPositioningCallbackUtility.changeBounds( mCtrlType, mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, mDisplayController, mDesktopWindowDecoration); - DragPositioningCallbackUtility.applyTaskBoundsChange( - new WindowContainerTransaction(), mDesktopWindowDecoration, - mRepositionTaskBounds, mTaskOrganizer); + mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitions.startTransition(TRANSIT_CHANGE, wct, this); + } else { + mTaskOrganizer.applyTransaction(wct); + } + } else { + // If bounds haven't changed, perform necessary veil reset here as startAnimation + // won't be called. + mDesktopWindowDecoration.hideResizeVeil(); } - // TODO: (b/279062291) Synchronize the start of hide to the end of the draw triggered - // above. - mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); - mDesktopWindowDecoration.hideResizeVeil(); } else if (!mDisallowedAreaForEndBounds.contains((int) x, (int) y)) { DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, x, y); @@ -133,7 +148,6 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback { mCtrlType = CTRL_TYPE_UNDEFINED; mTaskBoundsAtDragStart.setEmpty(); mRepositionStartPoint.set(0, 0); - mHasDragResized = false; } private boolean isResizing() { @@ -141,4 +155,26 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback { || (mCtrlType & CTRL_TYPE_LEFT) != 0 || (mCtrlType & CTRL_TYPE_RIGHT) != 0; } + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + startTransaction.apply(); + mDesktopWindowDecoration.hideResizeVeil(); + mCtrlType = CTRL_TYPE_UNDEFINED; + finishCallback.onTransitionFinished(null, null); + return true; + } + + /** + * We should never reach this as this handler's transitions are only started from shell + * explicitly. + */ + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + return null; + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt index 3fd6d17f27cb..4505b9978b76 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt @@ -81,13 +81,9 @@ class DismissSplitScreenByDivider(override val flicker: FlickerTest) : @Presubmit @Test fun secondaryAppBoundsIsFullscreenAtEnd() { - flicker.assertLayers { - this.isVisible(secondaryApp).then().isInvisible(secondaryApp).then().invoke( - "secondaryAppBoundsIsFullscreenAtEnd" - ) { - val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.endRotation) - it.visibleRegion(secondaryApp).coversExactly(displayBounds) - } + flicker.assertLayersEnd { + val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.endRotation) + visibleRegion(secondaryApp).coversExactly(displayBounds) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java index a625346e69c0..4fca8b46a069 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -65,12 +65,14 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) .build(); - doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any()); + doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), + any()); mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction); final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class); - verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction), + verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), + eq(mFinishTransaction), finishCallback.capture(), any()); verify(mStartTransaction).apply(); verify(mAnimator).start(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java index 4f4f356ef2e6..ab1ccd4599a2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java @@ -47,6 +47,7 @@ abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase { @Mock ShellInit mShellInit; + @Mock Transitions mTransitions; @Mock diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java index b8f615a1855f..ba34f1f74cd3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java @@ -29,9 +29,13 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import android.animation.Animator; +import android.animation.ValueAnimator; import android.graphics.Rect; +import android.view.SurfaceControl; import android.window.TransitionInfo; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -58,7 +62,8 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation @Before public void setup() { super.setUp(); - doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any()); + doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), + any()); } @Test @@ -182,6 +187,44 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation verifyNoMoreInteractions(mFinishTransaction); } + @UiThreadTest + @Test + public void testMergeAnimation() { + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(createEmbeddedChange( + EMBEDDED_LEFT_BOUNDS, EMBEDDED_LEFT_BOUNDS, TASK_BOUNDS)) + .build(); + + final ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mController.onAnimationFinished(mTransition); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }); + doReturn(animator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any()); + mController.startAnimation(mTransition, info, mStartTransaction, + mFinishTransaction, mFinishCallback); + verify(mFinishCallback, never()).onTransitionFinished(any(), any()); + mController.mergeAnimation(mTransition, info, new SurfaceControl.Transaction(), + mTransition, + (wct, cb) -> { + }); + verify(mFinishCallback).onTransitionFinished(any(), any()); + } + @Test public void testOnAnimationFinished() { // Should not call finish when there is no transition. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java deleted file mode 100644 index d62e6601723a..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.back; - -import static org.junit.Assert.assertEquals; - -import android.window.BackEvent; -import android.window.BackMotionEvent; - -import org.junit.Before; -import org.junit.Test; - -public class TouchTrackerTest { - private static final float FAKE_THRESHOLD = 400; - private static final float INITIAL_X_LEFT_EDGE = 5; - private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE; - private TouchTracker mTouchTracker; - - @Before - public void setUp() throws Exception { - mTouchTracker = new TouchTracker(); - mTouchTracker.setProgressThreshold(FAKE_THRESHOLD); - } - - @Test - public void generatesProgress_onStart() { - mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); - BackMotionEvent event = mTouchTracker.createStartEvent(null); - assertEquals(event.getProgress(), 0f, 0f); - } - - @Test - public void generatesProgress_leftEdge() { - mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); - float touchX = 10; - float velocityX = 0; - float velocityY = 0; - - // Pre-commit - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); - - // Post-commit - touchX += 100; - mTouchTracker.setTriggerBack(true); - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); - - // Cancel - touchX -= 10; - mTouchTracker.setTriggerBack(false); - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), 0, 0f); - - // Cancel more - touchX -= 10; - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), 0, 0f); - - // Restart - touchX += 10; - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), 0, 0f); - - // Restarted, but pre-commit - float restartX = touchX; - touchX += 10; - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f); - - // Restarted, post-commit - touchX += 10; - mTouchTracker.setTriggerBack(true); - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); - } - - @Test - public void generatesProgress_rightEdge() { - mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT); - float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge - float velocityX = 0f; - float velocityY = 0f; - - // Pre-commit - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); - - // Post-commit - touchX -= 100; - mTouchTracker.setTriggerBack(true); - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); - - // Cancel - touchX += 10; - mTouchTracker.setTriggerBack(false); - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), 0, 0f); - - // Cancel more - touchX += 10; - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), 0, 0f); - - // Restart - touchX -= 10; - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), 0, 0f); - - // Restarted, but pre-commit - float restartX = touchX; - touchX -= 10; - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f); - - // Restarted, post-commit - touchX -= 10; - mTouchTracker.setTriggerBack(true); - mTouchTracker.update(touchX, 0, velocityX, velocityY); - assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); - } - - private float getProgress() { - return mTouchTracker.createProgressEvent().getProgress(); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt new file mode 100644 index 000000000000..9088e8997e79 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.back + +import android.util.MathUtils +import android.window.BackEvent +import org.junit.Assert.assertEquals +import org.junit.Test + +class TouchTrackerTest { + private fun linearTouchTracker(): TouchTracker = TouchTracker().apply { + setProgressThresholds(MAX_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR) + } + + private fun nonLinearTouchTracker(): TouchTracker = TouchTracker().apply { + setProgressThresholds(LINEAR_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR) + } + + private fun TouchTracker.assertProgress(expected: Float) { + val actualProgress = createProgressEvent().progress + assertEquals(expected, actualProgress, /* delta = */ 0f) + } + + @Test + fun generatesProgress_onStart() { + val linearTracker = linearTouchTracker() + linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT) + val event = linearTracker.createStartEvent(null) + assertEquals(0f, event.progress, 0f) + } + + @Test + fun generatesProgress_leftEdge() { + val linearTracker = linearTouchTracker() + linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT) + var touchX = 10f + val velocityX = 0f + val velocityY = 0f + + // Pre-commit + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE) + + // Post-commit + touchX += 100f + linearTracker.setTriggerBack(true) + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE) + + // Cancel + touchX -= 10f + linearTracker.setTriggerBack(false) + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress(0f) + + // Cancel more + touchX -= 10f + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress(0f) + + // Restart + touchX += 10f + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress(0f) + + // Restarted, but pre-commit + val restartX = touchX + touchX += 10f + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((touchX - restartX) / MAX_DISTANCE) + + // Restarted, post-commit + touchX += 10f + linearTracker.setTriggerBack(true) + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE) + } + + @Test + fun generatesProgress_rightEdge() { + val linearTracker = linearTouchTracker() + linearTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0f, BackEvent.EDGE_RIGHT) + var touchX = INITIAL_X_RIGHT_EDGE - 10 // Fake right edge + val velocityX = 0f + val velocityY = 0f + val target = MAX_DISTANCE + + // Pre-commit + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target) + + // Post-commit + touchX -= 100f + linearTracker.setTriggerBack(true) + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target) + + // Cancel + touchX += 10f + linearTracker.setTriggerBack(false) + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress(0f) + + // Cancel more + touchX += 10f + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress(0f) + + // Restart + touchX -= 10f + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress(0f) + + // Restarted, but pre-commit + val restartX = touchX + touchX -= 10f + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((restartX - touchX) / target) + + // Restarted, post-commit + touchX -= 10f + linearTracker.setTriggerBack(true) + linearTracker.update(touchX, 0f, velocityX, velocityY) + linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target) + } + + @Test + fun generatesNonLinearProgress_leftEdge() { + val nonLinearTracker = nonLinearTouchTracker() + nonLinearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT) + var touchX = 10f + val velocityX = 0f + val velocityY = 0f + val linearTarget = LINEAR_DISTANCE + (MAX_DISTANCE - LINEAR_DISTANCE) * NON_LINEAR_FACTOR + + // Pre-commit: linear progress + nonLinearTracker.update(touchX, 0f, velocityX, velocityY) + nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget) + + // Post-commit: still linear progress + touchX += 100f + nonLinearTracker.setTriggerBack(true) + nonLinearTracker.update(touchX, 0f, velocityX, velocityY) + nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget) + + // still linear progress + touchX = INITIAL_X_LEFT_EDGE + LINEAR_DISTANCE + nonLinearTracker.update(touchX, 0f, velocityX, velocityY) + nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget) + + // non linear progress + touchX += 10 + nonLinearTracker.update(touchX, 0f, velocityX, velocityY) + val nonLinearTouch = (touchX - INITIAL_X_LEFT_EDGE) - LINEAR_DISTANCE + val nonLinearProgress = nonLinearTouch / NON_LINEAR_DISTANCE + val nonLinearTarget = MathUtils.lerp(linearTarget, MAX_DISTANCE, nonLinearProgress) + nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / nonLinearTarget) + } + + companion object { + private const val MAX_DISTANCE = 500f + private const val LINEAR_DISTANCE = 400f + private const val NON_LINEAR_DISTANCE = MAX_DISTANCE - LINEAR_DISTANCE + private const val NON_LINEAR_FACTOR = 0.2f + private const val INITIAL_X_LEFT_EDGE = 5f + private const val INITIAL_X_RIGHT_EDGE = MAX_DISTANCE - INITIAL_X_LEFT_EDGE + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java index 9e988e8e8726..7c1da35888b8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -211,7 +211,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), + verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), eq(SPLIT_POSITION_UNDEFINED), any()); } @@ -223,12 +223,12 @@ public class DragAndDropPolicyTest extends ShellTestCase { mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), + verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any()); reset(mSplitScreenStarter); mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), + verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); } @@ -240,12 +240,12 @@ public class DragAndDropPolicyTest extends ShellTestCase { mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), + verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any()); reset(mSplitScreenStarter); mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), + verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index d0e26019f9bf..c37a497c5d40 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -181,7 +181,8 @@ public class SplitScreenControllerTests extends ShellTestCase { PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); - mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, + SPLIT_POSITION_TOP_OR_LEFT, null); verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); @@ -200,7 +201,8 @@ public class SplitScreenControllerTests extends ShellTestCase { createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(); - mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, + SPLIT_POSITION_TOP_OR_LEFT, null); verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); @@ -223,7 +225,8 @@ public class SplitScreenControllerTests extends ShellTestCase { ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo(); doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any()); - mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, + SPLIT_POSITION_TOP_OR_LEFT, null); verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); @@ -246,7 +249,8 @@ public class SplitScreenControllerTests extends ShellTestCase { doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks) .findTaskInBackground(any()); - mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, + SPLIT_POSITION_TOP_OR_LEFT, null); verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); @@ -265,7 +269,8 @@ public class SplitScreenControllerTests extends ShellTestCase { doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo( SPLIT_POSITION_BOTTOM_OR_RIGHT); - mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, + SPLIT_POSITION_TOP_OR_LEFT, null); verify(mStageCoordinator).switchSplitPosition(anyString()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index b76d2dcc6e1e..80384531e9ae 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -182,7 +182,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder transition = mSplitScreenTransitions.startEnterTransition( TRANSIT_OPEN, new WindowContainerTransaction(), new RemoteTransition(testRemote, "Test"), mStageCoordinator, null, null, - TRANSIT_SPLIT_SCREEN_PAIR_OPEN); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -376,8 +376,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder transition = mock(IBinder.class); WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request); - // Don't reparent tasks until the animation is complete. - assertFalse(containsSplitExit(result)); + assertTrue(containsSplitExit(result)); // make sure we haven't made any local changes yet (need to wait until transition is ready) assertTrue(mStageCoordinator.isSplitScreenVisible()); @@ -409,7 +408,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder enterTransit = mSplitScreenTransitions.startEnterTransition( TRANSIT_OPEN, new WindowContainerTransaction(), new RemoteTransition(new TestRemoteTransition(), "Test"), - mStageCoordinator, null, null, TRANSIT_SPLIT_SCREEN_PAIR_OPEN); + mStageCoordinator, null, null, TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); mStageCoordinator.startAnimation(enterTransit, enterInfo, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index 44a0ede82e5c..6621ab8ce0d8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -158,7 +158,6 @@ public class StageCoordinatorTests extends ShellTestCase { verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT)); verify(mMainStage).reparentTopTask(eq(wct)); - verify(mSplitLayout).resetDividerPosition(); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java index 9d566860c1cd..71ad0d79eaca 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java @@ -45,6 +45,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -58,6 +60,12 @@ public class TaskViewTransitionsTest extends ShellTestCase { ActivityManager.RunningTaskInfo mTaskInfo; @Mock WindowContainerToken mToken; + @Mock + TaskViewTaskController mTaskViewTaskController2; + @Mock + ActivityManager.RunningTaskInfo mTaskInfo2; + @Mock + WindowContainerToken mToken2; TaskViewTransitions mTaskViewTransitions; @@ -73,10 +81,16 @@ public class TaskViewTransitionsTest extends ShellTestCase { mTaskInfo.token = mToken; mTaskInfo.taskId = 314; mTaskInfo.taskDescription = mock(ActivityManager.TaskDescription.class); + when(mTaskViewTaskController.getTaskInfo()).thenReturn(mTaskInfo); + + mTaskInfo2 = new ActivityManager.RunningTaskInfo(); + mTaskInfo2.token = mToken2; + mTaskInfo2.taskId = 315; + mTaskInfo2.taskDescription = mock(ActivityManager.TaskDescription.class); + when(mTaskViewTaskController2.getTaskInfo()).thenReturn(mTaskInfo2); mTaskViewTransitions = spy(new TaskViewTransitions(mTransitions)); mTaskViewTransitions.addTaskView(mTaskViewTaskController); - when(mTaskViewTaskController.getTaskInfo()).thenReturn(mTaskInfo); } @Test @@ -119,7 +133,7 @@ public class TaskViewTransitionsTest extends ShellTestCase { } @Test - public void testSetTaskBounds_taskVisibleWithPending_noTransaction() { + public void testSetTaskBounds_taskVisibleWithPendingOpen_noTransaction() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true); @@ -135,6 +149,43 @@ public class TaskViewTransitionsTest extends ShellTestCase { } @Test + public void testSetTaskBounds_taskVisibleWithPendingChange_transition() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true); + + // Consume the pending transition from visibility change + TaskViewTransitions.PendingTransition pending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(pending).isNotNull(); + mTaskViewTransitions.startAnimation(pending.mClaimed, + mock(TransitionInfo.class), + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mock(Transitions.TransitionFinishCallback.class)); + // Verify it was consumed + TaskViewTransitions.PendingTransition checkPending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(checkPending).isNull(); + + // Test that set bounds creates a new transition + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, + new Rect(0, 0, 100, 100)); + assertThat(mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE)) + .isNotNull(); + + // Test that set bounds again (with different bounds) creates another transition + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, + new Rect(0, 0, 300, 200)); + List<TaskViewTransitions.PendingTransition> pendingList = + mTaskViewTransitions.findAllPending(mTaskViewTaskController) + .stream() + .filter(pendingTransition -> pendingTransition.mType == TRANSIT_CHANGE) + .toList(); + assertThat(pendingList.size()).isEqualTo(2); + } + + @Test public void testSetTaskBounds_sameBounds_noTransaction() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); @@ -161,6 +212,16 @@ public class TaskViewTransitionsTest extends ShellTestCase { mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE); assertThat(pendingBounds).isNotNull(); + // Test that setting same bounds with in-flight transition doesn't cause another one + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, + new Rect(0, 0, 100, 100)); + List<TaskViewTransitions.PendingTransition> pendingList = + mTaskViewTransitions.findAllPending(mTaskViewTaskController) + .stream() + .filter(pendingTransition -> pendingTransition.mType == TRANSIT_CHANGE) + .toList(); + assertThat(pendingList.size()).isEqualTo(1); + // Consume the pending bounds transaction mTaskViewTransitions.startAnimation(pendingBounds.mClaimed, mock(TransitionInfo.class), @@ -180,6 +241,42 @@ public class TaskViewTransitionsTest extends ShellTestCase { assertThat(pendingBounds2).isNull(); } + + @Test + public void testSetTaskBounds_taskVisibleWithDifferentTaskViewPendingChange_transition() { + assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); + + mTaskViewTransitions.addTaskView(mTaskViewTaskController2); + + mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController, true); + + // Consume the pending transition from visibility change + TaskViewTransitions.PendingTransition pending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(pending).isNotNull(); + mTaskViewTransitions.startAnimation(pending.mClaimed, + mock(TransitionInfo.class), + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mock(Transitions.TransitionFinishCallback.class)); + // Verify it was consumed + TaskViewTransitions.PendingTransition checkPending = + mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT); + assertThat(checkPending).isNull(); + + // Set the second taskview as visible & check that it has a pending transition + mTaskViewTransitions.setTaskViewVisible(mTaskViewTaskController2, true); + TaskViewTransitions.PendingTransition pending2 = + mTaskViewTransitions.findPending(mTaskViewTaskController2, TRANSIT_TO_FRONT); + assertThat(pending2).isNotNull(); + + // Test that set bounds on the first taskview will create a new transition + mTaskViewTransitions.setTaskBounds(mTaskViewTaskController, + new Rect(0, 0, 100, 100)); + assertThat(mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_CHANGE)) + .isNotNull(); + } + @Test public void testSetTaskVisibility_taskRemoved_noNPE() { mTaskViewTransitions.removeTaskView(mTaskViewTaskController); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java index 4c27706a39a3..41bab95b7dd4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java @@ -54,6 +54,7 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.desktopmode.DesktopModeController; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.Transitions; import org.junit.Before; import org.junit.Test; @@ -87,6 +88,7 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase { @Mock private DesktopTasksController mDesktopTasksController; @Mock private InputMonitor mInputMonitor; @Mock private InputManager mInputManager; + @Mock private Transitions mTransitions; @Mock private DesktopModeWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory; @Mock private Supplier<SurfaceControl.Transaction> mTransactionFactory; @Mock private SurfaceControl.Transaction mTransaction; @@ -106,6 +108,7 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase { mTaskOrganizer, mDisplayController, mSyncQueue, + mTransitions, Optional.of(mDesktopModeController), Optional.of(mDesktopTasksController), Optional.of(mSplitScreenController), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 445a73a2ad38..4147dd8e6152 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -22,12 +22,14 @@ import android.os.IBinder import android.testing.AndroidTestingRunner import android.view.Display import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_CHANGE import android.window.WindowContainerToken import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED @@ -78,6 +80,8 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { private lateinit var mockTransactionFactory: Supplier<SurfaceControl.Transaction> @Mock private lateinit var mockTransaction: SurfaceControl.Transaction + @Mock + private lateinit var mockTransitions: Transitions private lateinit var taskPositioner: VeiledResizeTaskPositioner @@ -92,7 +96,8 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { mockDisplayController, DISALLOWED_AREA_FOR_END_BOUNDS, mockDragStartListener, - mockTransactionFactory + mockTransactionFactory, + mockTransitions ) whenever(taskToken.asBinder()).thenReturn(taskBinder) @@ -123,12 +128,18 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil() + verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningEnd( STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) + verify(mockTransitions, never()).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && + change.configuration.windowConfiguration.bounds == STARTING_BOUNDS}}, + eq(taskPositioner)) verify(mockDesktopWindowDecoration).hideResizeVeil() } @@ -180,7 +191,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil() + verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningMove( STARTING_BOUNDS.right.toFloat() + 10, @@ -206,15 +217,12 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { rectAfterEnd.right += 10 rectAfterEnd.top += 10 verify(mockDesktopWindowDecoration, times(2)).updateResizeVeil(any()) - verify(mockDesktopWindowDecoration).hideResizeVeil() - - verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> return@argThat wct.changes.any { (token, change) -> token == taskBinder && (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds == rectAfterEnd - } - }) + change.configuration.windowConfiguration.bounds == rectAfterEnd}}, + eq(taskPositioner)) } @Test @@ -224,7 +232,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil() + verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningMove( STARTING_BOUNDS.left.toFloat(), @@ -235,7 +243,13 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.left.toFloat() + 10, STARTING_BOUNDS.top.toFloat() + 10 ) - verify(mockDesktopWindowDecoration).hideResizeVeil() + + verify(mockTransitions, never()).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && + change.configuration.windowConfiguration.bounds == STARTING_BOUNDS}}, + eq(taskPositioner)) verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp index 9a06be006dca..701a87f0cce4 100644 --- a/libs/hwui/hwui/ImageDecoder.cpp +++ b/libs/hwui/hwui/ImageDecoder.cpp @@ -51,6 +51,9 @@ using namespace android; sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const { const skcms_ICCProfile* encodedProfile = mCodec->getICCProfile(); if (encodedProfile) { + if (encodedProfile->has_CICP) { + return mCodec->computeOutputColorSpace(kN32_SkColorType); + } // If the profile maps directly to an SkColorSpace, that SkColorSpace // will be returned. Otherwise, nullptr will be returned. In either // case, using this SkColorSpace results in doing no color correction. diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp index 38d17de166e9..8abcd9a59122 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -194,7 +194,11 @@ static bool decodeGainmap(std::unique_ptr<SkStream> gainmapStream, const SkGainm ALOGE("Can not create a codec for Gainmap."); return false; } - SkColorType decodeColorType = codec->computeOutputColorType(kN32_SkColorType); + SkColorType decodeColorType = kN32_SkColorType; + if (codec->getInfo().colorType() == kGray_8_SkColorType) { + decodeColorType = kGray_8_SkColorType; + } + decodeColorType = codec->computeOutputColorType(decodeColorType); sk_sp<SkColorSpace> decodeColorSpace = codec->computeOutputColorSpace(decodeColorType, nullptr); SkISize size = codec->getSampledDimensions(sampleSize); @@ -217,7 +221,11 @@ static bool decodeGainmap(std::unique_ptr<SkStream> gainmapStream, const SkGainm const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType, alphaType, decodeColorSpace); - const SkImageInfo& bitmapInfo = decodeInfo; + SkImageInfo bitmapInfo = decodeInfo; + if (decodeColorType == kGray_8_SkColorType) { + // We treat gray8 as alpha8 in Bitmap's API surface + bitmapInfo = bitmapInfo.makeColorType(kAlpha_8_SkColorType); + } SkBitmap decodeBitmap; sk_sp<Bitmap> nativeBitmap = nullptr; diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index c58ba6868eb5..8d5967bbd461 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -27,6 +27,7 @@ #include "include/gpu/GpuTypes.h" // from Skia #include "utils/GLUtils.h" #include <effects/GainmapRenderer.h> +#include "renderthread/CanvasContext.h" namespace android { namespace uirenderer { @@ -131,6 +132,8 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { mat4.getColMajor(&info.transform[0]); info.color_space_ptr = canvas->imageInfo().colorSpace(); info.currentHdrSdrRatio = getTargetHdrSdrRatio(info.color_space_ptr); + info.fboColorType = canvas->imageInfo().colorType(); + info.shouldDither = renderthread::CanvasContext::shouldDither(); // ensure that the framebuffer that the webview will render into is bound before we clear // the stencil and/or draw the functor. diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp index e299d12b1d67..b62711f50c94 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp @@ -15,22 +15,25 @@ */ #include "VkFunctorDrawable.h" -#include <private/hwui/DrawVkInfo.h> #include <GrBackendDrawableInfo.h> #include <SkAndroidFrameworkUtils.h> #include <SkImage.h> #include <SkM44.h> #include <gui/TraceUtils.h> +#include <private/hwui/DrawVkInfo.h> #include <utils/Color.h> #include <utils/Trace.h> #include <vk/GrVkTypes.h> + #include <thread> + +#include "effects/GainmapRenderer.h" +#include "renderthread/CanvasContext.h" #include "renderthread/RenderThread.h" #include "renderthread/VulkanManager.h" #include "thread/ThreadBase.h" #include "utils/TimeUtils.h" -#include "effects/GainmapRenderer.h" namespace android { namespace uirenderer { @@ -75,6 +78,7 @@ void VkFunctorDrawHandler::draw(const GrBackendDrawableInfo& info) { .clip_bottom = mClip.fBottom, .is_layer = !vulkan_info.fFromSwapchainOrAndroidWindow, .currentHdrSdrRatio = getTargetHdrSdrRatio(mImageInfo.colorSpace()), + .shouldDither = renderthread::CanvasContext::shouldDither(), }; mat4.getColMajor(¶ms.transform[0]); params.secondary_command_buffer = vulkan_info.fSecondaryCommandBuffer; diff --git a/libs/hwui/private/hwui/DrawGlInfo.h b/libs/hwui/private/hwui/DrawGlInfo.h index 7888c8719e88..eb1f9304a5c8 100644 --- a/libs/hwui/private/hwui/DrawGlInfo.h +++ b/libs/hwui/private/hwui/DrawGlInfo.h @@ -18,6 +18,7 @@ #define ANDROID_HWUI_DRAW_GL_INFO_H #include <SkColorSpace.h> +#include <SkColorType.h> namespace android { namespace uirenderer { @@ -91,6 +92,12 @@ struct DrawGlInfo { // be baked into the color_space_ptr, so this is just to indicate the amount of extended // range is available if desired float currentHdrSdrRatio; + + // Whether or not dithering is globally enabled + bool shouldDither; + + // The color type of the destination framebuffer + SkColorType fboColorType; }; // struct DrawGlInfo } // namespace uirenderer diff --git a/libs/hwui/private/hwui/DrawVkInfo.h b/libs/hwui/private/hwui/DrawVkInfo.h index 8f7063d72314..122080658927 100644 --- a/libs/hwui/private/hwui/DrawVkInfo.h +++ b/libs/hwui/private/hwui/DrawVkInfo.h @@ -76,6 +76,9 @@ struct VkFunctorDrawParams { // be baked into the color_space_ptr, so this is just to indicate the amount of extended // range is available if desired float currentHdrSdrRatio; + + // Whether or not dithering is globally enabled + bool shouldDither; }; } // namespace uirenderer diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 2bd400dba346..16b35ffcabac 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -1078,6 +1078,12 @@ void CanvasContext::startHintSession() { mHintSessionWrapper.init(); } +bool CanvasContext::shouldDither() { + CanvasContext* self = getActiveContext(); + if (!self) return false; + return self->mColorMode != ColorMode::Default; +} + } /* namespace renderthread */ } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index 08e24245d16c..5219b5757008 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -234,6 +234,8 @@ public: void startHintSession(); + static bool shouldDither(); + private: CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory, std::unique_ptr<IRenderPipeline> renderPipeline, diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp index 597cbf732d82..814ac4d90028 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.cpp +++ b/libs/hwui/renderthread/HintSessionWrapper.cpp @@ -156,6 +156,7 @@ void HintSessionWrapper::updateTargetWorkDuration(long targetWorkDurationNanos) void HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) { if (!init()) return; + mResetsSinceLastReport = 0; if (actualDurationNanos > kSanityCheckLowerBound && actualDurationNanos < kSanityCheckUpperBound) { gAPH_reportActualWorkDurationFn(mHintSession, actualDurationNanos); @@ -163,9 +164,12 @@ void HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) { } void HintSessionWrapper::sendLoadResetHint() { + static constexpr int kMaxResetsSinceLastReport = 2; if (!init()) return; nsecs_t now = systemTime(); - if (now - mLastFrameNotification > kResetHintTimeout) { + if (now - mLastFrameNotification > kResetHintTimeout && + mResetsSinceLastReport <= kMaxResetsSinceLastReport) { + ++mResetsSinceLastReport; gAPH_sendHintFn(mHintSession, static_cast<int>(SessionHint::CPU_LOAD_RESET)); } mLastFrameNotification = now; diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h index b7a433fb4eae..24b8150dd489 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.h +++ b/libs/hwui/renderthread/HintSessionWrapper.h @@ -42,6 +42,7 @@ private: APerformanceHintSession* mHintSession = nullptr; std::future<APerformanceHintSession*> mHintSessionFuture; + int mResetsSinceLastReport = 0; nsecs_t mLastFrameNotification = 0; nsecs_t mLastTargetWorkDuration = 0; diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index 7e238e44043b..0e9c162e4929 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -57,6 +57,30 @@ public final class MediaRoute2Info implements Parcelable { } }; + /** + * The {@link #getOriginalId() original id} of the route that represents the built-in media + * route. + * + * <p>A route with this id will only be visible to apps with permission to do system routing, + * which means having {@link android.Manifest.permission#BLUETOOTH_CONNECT} and {@link + * android.Manifest.permission#BLUETOOTH_SCAN}, or {@link + * android.Manifest.permission#MODIFY_AUDIO_ROUTING}. + * + * @hide + */ + public static final String ROUTE_ID_DEVICE = "DEVICE_ROUTE"; + + /** + * The {@link #getOriginalId() original id} of the route that represents the default system + * media route. + * + * <p>A route with this id will be visible to apps with no permission over system routing. See + * {@link #ROUTE_ID_DEVICE} for details. + * + * @hide + */ + public static final String ROUTE_ID_DEFAULT = "DEFAULT_ROUTE"; + /** @hide */ @IntDef({CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING, CONNECTION_STATE_CONNECTED}) diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp index 6f67d68ed915..1b04f18e38c7 100644 --- a/media/jni/android_media_tv_Tuner.cpp +++ b/media/jni/android_media_tv_Tuner.cpp @@ -372,13 +372,12 @@ static JAudioPresentationInfo::fields_t gAudioPresentationFields; void LnbClientCallbackImpl::onEvent(const LnbEventType lnbEventType) { ALOGV("LnbClientCallbackImpl::onEvent, type=%d", lnbEventType); JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobject lnb(env->NewLocalRef(mLnbObj)); - if (!env->IsSameObject(lnb, nullptr)) { + ScopedLocalRef lnb(env, env->NewLocalRef(mLnbObj)); + if (!env->IsSameObject(lnb.get(), nullptr)) { env->CallVoidMethod( - lnb, + lnb.get(), gFields.onLnbEventID, (jint)lnbEventType); - env->DeleteLocalRef(lnb); } else { ALOGE("LnbClientCallbackImpl::onEvent:" "Lnb object has been freed. Ignoring callback."); @@ -388,17 +387,15 @@ void LnbClientCallbackImpl::onEvent(const LnbEventType lnbEventType) { void LnbClientCallbackImpl::onDiseqcMessage(const vector<uint8_t> &diseqcMessage) { ALOGV("LnbClientCallbackImpl::onDiseqcMessage"); JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobject lnb(env->NewLocalRef(mLnbObj)); - if (!env->IsSameObject(lnb, nullptr)) { - jbyteArray array = env->NewByteArray(diseqcMessage.size()); - env->SetByteArrayRegion(array, 0, diseqcMessage.size(), + ScopedLocalRef lnb(env, env->NewLocalRef(mLnbObj)); + if (!env->IsSameObject(lnb.get(), nullptr)) { + ScopedLocalRef array(env, env->NewByteArray(diseqcMessage.size())); + env->SetByteArrayRegion(array.get(), 0, diseqcMessage.size(), reinterpret_cast<const jbyte *>(&diseqcMessage[0])); env->CallVoidMethod( - lnb, + lnb.get(), gFields.onLnbDiseqcMessageID, - array); - env->DeleteLocalRef(lnb); - env->DeleteLocalRef(array); + array.get()); } else { ALOGE("LnbClientCallbackImpl::onDiseqcMessage:" "Lnb object has been freed. Ignoring callback."); @@ -422,10 +419,9 @@ LnbClientCallbackImpl::~LnbClientCallbackImpl() { void DvrClientCallbackImpl::onRecordStatus(RecordStatus status) { ALOGV("DvrClientCallbackImpl::onRecordStatus"); JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobject dvr(env->NewLocalRef(mDvrObj)); - if (!env->IsSameObject(dvr, nullptr)) { - env->CallVoidMethod(dvr, gFields.onDvrRecordStatusID, (jint)status); - env->DeleteLocalRef(dvr); + ScopedLocalRef dvr(env, env->NewLocalRef(mDvrObj)); + if (!env->IsSameObject(dvr.get(), nullptr)) { + env->CallVoidMethod(dvr.get(), gFields.onDvrRecordStatusID, (jint)status); } else { ALOGE("DvrClientCallbackImpl::onRecordStatus:" "Dvr object has been freed. Ignoring callback."); @@ -435,10 +431,9 @@ void DvrClientCallbackImpl::onRecordStatus(RecordStatus status) { void DvrClientCallbackImpl::onPlaybackStatus(PlaybackStatus status) { ALOGV("DvrClientCallbackImpl::onPlaybackStatus"); JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobject dvr(env->NewLocalRef(mDvrObj)); - if (!env->IsSameObject(dvr, nullptr)) { - env->CallVoidMethod(dvr, gFields.onDvrPlaybackStatusID, (jint)status); - env->DeleteLocalRef(dvr); + ScopedLocalRef dvr(env, env->NewLocalRef(mDvrObj)); + if (!env->IsSameObject(dvr.get(), nullptr)) { + env->CallVoidMethod(dvr.get(), gFields.onDvrPlaybackStatusID, (jint)status); } else { ALOGE("DvrClientCallbackImpl::onPlaybackStatus:" "Dvr object has been freed. Ignoring callback."); @@ -614,7 +609,7 @@ int64_t MediaEvent::getAudioHandle() { } /////////////// FilterClientCallbackImpl /////////////////////// -void FilterClientCallbackImpl::getSectionEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getSectionEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); @@ -624,20 +619,20 @@ void FilterClientCallbackImpl::getSectionEvent(jobjectArray &arr, const int size jint sectionNum = sectionEvent.sectionNum; jlong dataLength = sectionEvent.dataLength; - jobject obj = env->NewObject(mSectionEventClass, mSectionEventInitID, tableId, version, - sectionNum, dataLength); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mSectionEventClass, mSectionEventInitID, tableId, + version, sectionNum, dataLength)); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getMediaEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getMediaEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); const DemuxFilterMediaEvent &mediaEvent = event.get<DemuxFilterEvent::Tag::media>(); - jobject audioDescriptor = nullptr; + ScopedLocalRef<jobject> audioDescriptor(env); gAudioPresentationFields.init(env); - jobject presentationsJObj = JAudioPresentationInfo::asJobject(env, gAudioPresentationFields); + ScopedLocalRef presentationsJObj(env, JAudioPresentationInfo::asJobject( + env, gAudioPresentationFields)); switch (mediaEvent.extraMetaData.getTag()) { case DemuxFilterMediaEventExtraMetaData::Tag::audio: { @@ -650,9 +645,9 @@ void FilterClientCallbackImpl::getMediaEvent(jobjectArray &arr, const int size, jbyte adGainFront = ad.adGainFront; jbyte adGainSurround = ad.adGainSurround; - audioDescriptor = env->NewObject(mAudioDescriptorClass, mAudioDescriptorInitID, adFade, - adPan, versionTextTag, adGainCenter, adGainFront, - adGainSurround); + audioDescriptor.reset(env->NewObject(mAudioDescriptorClass, mAudioDescriptorInitID, + adFade, adPan, versionTextTag, adGainCenter, + adGainFront, adGainSurround)); break; } case DemuxFilterMediaEventExtraMetaData::Tag::audioPresentations: { @@ -660,7 +655,7 @@ void FilterClientCallbackImpl::getMediaEvent(jobjectArray &arr, const int size, env, gAudioPresentationFields, mediaEvent.extraMetaData .get<DemuxFilterMediaEventExtraMetaData::Tag::audioPresentations>(), - presentationsJObj); + presentationsJObj.get()); break; } default: { @@ -693,31 +688,27 @@ void FilterClientCallbackImpl::getMediaEvent(jobjectArray &arr, const int size, sc = mediaEvent.scIndexMask.get<DemuxFilterScIndexMask::Tag::scVvc>(); } - jobject obj = env->NewObject(mMediaEventClass, mMediaEventInitID, streamId, isPtsPresent, pts, - isDtsPresent, dts, dataLength, offset, nullptr, isSecureMemory, - avDataId, mpuSequenceNumber, isPesPrivateData, sc, - audioDescriptor, presentationsJObj); + ScopedLocalRef obj(env, env->NewObject(mMediaEventClass, mMediaEventInitID, streamId, + isPtsPresent, pts, isDtsPresent, dts, dataLength, + offset, nullptr, isSecureMemory, avDataId, + mpuSequenceNumber, isPesPrivateData, sc, + audioDescriptor.get(), presentationsJObj.get())); uint64_t avSharedMemSize = mFilterClient->getAvSharedHandleInfo().size; if (mediaEvent.avMemory.fds.size() > 0 || mediaEvent.avDataId != 0 || (dataLength > 0 && (dataLength + offset) < avSharedMemSize)) { sp<MediaEvent> mediaEventSp = new MediaEvent(mFilterClient, dupFromAidl(mediaEvent.avMemory), - mediaEvent.avDataId, dataLength + offset, obj); + mediaEvent.avDataId, dataLength + offset, obj.get()); mediaEventSp->mAvHandleRefCnt++; - env->SetLongField(obj, mMediaEventFieldContextID, (jlong)mediaEventSp.get()); - mediaEventSp->incStrong(obj); + env->SetLongField(obj.get(), mMediaEventFieldContextID, (jlong)mediaEventSp.get()); + mediaEventSp->incStrong(obj.get()); } - env->SetObjectArrayElement(arr, size, obj); - if(audioDescriptor != nullptr) { - env->DeleteLocalRef(audioDescriptor); - } - env->DeleteLocalRef(obj); - env->DeleteLocalRef(presentationsJObj); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getPesEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getPesEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); @@ -726,13 +717,12 @@ void FilterClientCallbackImpl::getPesEvent(jobjectArray &arr, const int size, jint dataLength = pesEvent.dataLength; jint mpuSequenceNumber = pesEvent.mpuSequenceNumber; - jobject obj = env->NewObject(mPesEventClass, mPesEventInitID, streamId, dataLength, - mpuSequenceNumber); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mPesEventClass, mPesEventInitID, streamId, dataLength, + mpuSequenceNumber)); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getTsRecordEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getTsRecordEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); @@ -764,13 +754,12 @@ void FilterClientCallbackImpl::getTsRecordEvent(jobjectArray &arr, const int siz jlong pts = tsRecordEvent.pts; jint firstMbInSlice = tsRecordEvent.firstMbInSlice; - jobject obj = env->NewObject(mTsRecordEventClass, mTsRecordEventInitID, jpid, ts, sc, - byteNumber, pts, firstMbInSlice); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mTsRecordEventClass, mTsRecordEventInitID, jpid, ts, sc, + byteNumber, pts, firstMbInSlice)); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getMmtpRecordEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getMmtpRecordEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); @@ -783,13 +772,13 @@ void FilterClientCallbackImpl::getMmtpRecordEvent(jobjectArray &arr, const int s jint firstMbInSlice = mmtpRecordEvent.firstMbInSlice; jlong tsIndexMask = mmtpRecordEvent.tsIndexMask; - jobject obj = env->NewObject(mMmtpRecordEventClass, mMmtpRecordEventInitID, scHevcIndexMask, - byteNumber, mpuSequenceNumber, pts, firstMbInSlice, tsIndexMask); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mMmtpRecordEventClass, mMmtpRecordEventInitID, + scHevcIndexMask, byteNumber, mpuSequenceNumber, pts, + firstMbInSlice, tsIndexMask)); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getDownloadEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getDownloadEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); @@ -801,25 +790,25 @@ void FilterClientCallbackImpl::getDownloadEvent(jobjectArray &arr, const int siz jint lastItemFragmentIndex = downloadEvent.lastItemFragmentIndex; jint dataLength = downloadEvent.dataLength; - jobject obj = env->NewObject(mDownloadEventClass, mDownloadEventInitID, itemId, downloadId, - mpuSequenceNumber, itemFragmentIndex, lastItemFragmentIndex, - dataLength); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mDownloadEventClass, mDownloadEventInitID, itemId, + downloadId, mpuSequenceNumber, itemFragmentIndex, + lastItemFragmentIndex, dataLength)); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getIpPayloadEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getIpPayloadEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); - const DemuxFilterIpPayloadEvent &ipPayloadEvent = event.get<DemuxFilterEvent::Tag::ipPayload>(); + const DemuxFilterIpPayloadEvent &ipPayloadEvent = + event.get<DemuxFilterEvent::Tag::ipPayload>(); jint dataLength = ipPayloadEvent.dataLength; - jobject obj = env->NewObject(mIpPayloadEventClass, mIpPayloadEventInitID, dataLength); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mIpPayloadEventClass, mIpPayloadEventInitID, + dataLength)); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getTemiEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getTemiEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); @@ -828,110 +817,108 @@ void FilterClientCallbackImpl::getTemiEvent(jobjectArray &arr, const int size, jbyte descrTag = temiEvent.descrTag; std::vector<uint8_t> descrData = temiEvent.descrData; - jbyteArray array = env->NewByteArray(descrData.size()); - env->SetByteArrayRegion(array, 0, descrData.size(), reinterpret_cast<jbyte *>(&descrData[0])); + ScopedLocalRef array(env, env->NewByteArray(descrData.size())); + env->SetByteArrayRegion(array.get(), 0, descrData.size(), + reinterpret_cast<jbyte *>(&descrData[0])); - jobject obj = env->NewObject(mTemiEventClass, mTemiEventInitID, pts, descrTag, array); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(array); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mTemiEventClass, mTemiEventInitID, pts, descrTag, + array.get())); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getScramblingStatusEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getScramblingStatusEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); const DemuxFilterMonitorEvent &scramblingStatus = event.get<DemuxFilterEvent::Tag::monitorEvent>() .get<DemuxFilterMonitorEvent::Tag::scramblingStatus>(); - jobject obj = env->NewObject(mScramblingStatusEventClass, mScramblingStatusEventInitID, - scramblingStatus); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mScramblingStatusEventClass, + mScramblingStatusEventInitID, + scramblingStatus)); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getIpCidChangeEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getIpCidChangeEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); const DemuxFilterMonitorEvent &cid = event.get<DemuxFilterEvent::Tag::monitorEvent>() .get<DemuxFilterMonitorEvent::Tag::cid>(); - jobject obj = env->NewObject(mIpCidChangeEventClass, mIpCidChangeEventInitID, cid); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mIpCidChangeEventClass, mIpCidChangeEventInitID, cid)); + env->SetObjectArrayElement(arr, size, obj.get()); } -void FilterClientCallbackImpl::getRestartEvent(jobjectArray &arr, const int size, +void FilterClientCallbackImpl::getRestartEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent &event) { JNIEnv *env = AndroidRuntime::getJNIEnv(); const int32_t &startId = event.get<DemuxFilterEvent::Tag::startId>(); - jobject obj = env->NewObject(mRestartEventClass, mRestartEventInitID, startId); - env->SetObjectArrayElement(arr, size, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(mRestartEventClass, mRestartEventInitID, startId)); + env->SetObjectArrayElement(arr, size, obj.get()); } void FilterClientCallbackImpl::onFilterEvent(const vector<DemuxFilterEvent> &events) { ALOGV("FilterClientCallbackImpl::onFilterEvent"); JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobjectArray array; + ScopedLocalRef<jobjectArray> array(env); if (!events.empty()) { - array = env->NewObjectArray(events.size(), mEventClass, nullptr); + array.reset(env->NewObjectArray(events.size(), mEventClass, nullptr)); } for (int i = 0, arraySize = 0; i < events.size(); i++) { const DemuxFilterEvent &event = events[i]; switch (event.getTag()) { case DemuxFilterEvent::Tag::media: { - getMediaEvent(array, arraySize, event); + getMediaEvent(array.get(), arraySize, event); arraySize++; break; } case DemuxFilterEvent::Tag::section: { - getSectionEvent(array, arraySize, event); + getSectionEvent(array.get(), arraySize, event); arraySize++; break; } case DemuxFilterEvent::Tag::pes: { - getPesEvent(array, arraySize, event); + getPesEvent(array.get(), arraySize, event); arraySize++; break; } case DemuxFilterEvent::Tag::tsRecord: { - getTsRecordEvent(array, arraySize, event); + getTsRecordEvent(array.get(), arraySize, event); arraySize++; break; } case DemuxFilterEvent::Tag::mmtpRecord: { - getMmtpRecordEvent(array, arraySize, event); + getMmtpRecordEvent(array.get(), arraySize, event); arraySize++; break; } case DemuxFilterEvent::Tag::download: { - getDownloadEvent(array, arraySize, event); + getDownloadEvent(array.get(), arraySize, event); arraySize++; break; } case DemuxFilterEvent::Tag::ipPayload: { - getIpPayloadEvent(array, arraySize, event); + getIpPayloadEvent(array.get(), arraySize, event); arraySize++; break; } case DemuxFilterEvent::Tag::temi: { - getTemiEvent(array, arraySize, event); + getTemiEvent(array.get(), arraySize, event); arraySize++; break; } case DemuxFilterEvent::Tag::monitorEvent: { switch (event.get<DemuxFilterEvent::Tag::monitorEvent>().getTag()) { case DemuxFilterMonitorEvent::Tag::scramblingStatus: { - getScramblingStatusEvent(array, arraySize, event); + getScramblingStatusEvent(array.get(), arraySize, event); arraySize++; break; } case DemuxFilterMonitorEvent::Tag::cid: { - getIpCidChangeEvent(array, arraySize, event); + getIpCidChangeEvent(array.get(), arraySize, event); arraySize++; break; } @@ -943,7 +930,7 @@ void FilterClientCallbackImpl::onFilterEvent(const vector<DemuxFilterEvent> &eve break; } case DemuxFilterEvent::Tag::startId: { - getRestartEvent(array, arraySize, event); + getRestartEvent(array.get(), arraySize, event); arraySize++; break; } @@ -953,32 +940,29 @@ void FilterClientCallbackImpl::onFilterEvent(const vector<DemuxFilterEvent> &eve } } } - jobject filter(env->NewLocalRef(mFilterObj)); - if (!env->IsSameObject(filter, nullptr)) { + ScopedLocalRef filter(env, env->NewLocalRef(mFilterObj)); + if (!env->IsSameObject(filter.get(), nullptr)) { jmethodID methodID = gFields.onFilterEventID; if (mSharedFilter) { methodID = gFields.onSharedFilterEventID; } - env->CallVoidMethod(filter, methodID, array); - env->DeleteLocalRef(filter); + env->CallVoidMethod(filter.get(), methodID, array.get()); } else { ALOGE("FilterClientCallbackImpl::onFilterEvent:" "Filter object has been freed. Ignoring callback."); } - env->DeleteLocalRef(array); } void FilterClientCallbackImpl::onFilterStatus(const DemuxFilterStatus status) { ALOGV("FilterClientCallbackImpl::onFilterStatus"); JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobject filter(env->NewLocalRef(mFilterObj)); - if (!env->IsSameObject(filter, nullptr)) { + ScopedLocalRef filter(env, env->NewLocalRef(mFilterObj)); + if (!env->IsSameObject(filter.get(), nullptr)) { jmethodID methodID = gFields.onFilterStatusID; if (mSharedFilter) { methodID = gFields.onSharedFilterStatusID; } - env->CallVoidMethod(filter, methodID, (jint)static_cast<uint8_t>(status)); - env->DeleteLocalRef(filter); + env->CallVoidMethod(filter.get(), methodID, (jint)static_cast<uint8_t>(status)); } else { ALOGE("FilterClientCallbackImpl::onFilterStatus:" "Filter object has been freed. Ignoring callback."); @@ -1115,13 +1099,12 @@ void FrontendClientCallbackImpl::onEvent(FrontendEventType frontendEventType) { std::scoped_lock<std::mutex> lock(mMutex); for (const auto& mapEntry : mListenersMap) { ALOGV("JTuner:%p, jweak:%p", mapEntry.first, mapEntry.second); - jobject frontend(env->NewLocalRef(mapEntry.second)); - if (!env->IsSameObject(frontend, nullptr)) { + ScopedLocalRef frontend(env, env->NewLocalRef(mapEntry.second)); + if (!env->IsSameObject(frontend.get(), nullptr)) { env->CallVoidMethod( - frontend, + frontend.get(), gFields.onFrontendEventID, (jint)frontendEventType); - env->DeleteLocalRef(frontend); } else { ALOGW("FrontendClientCallbackImpl::onEvent:" "Frontend object has been freed. Ignoring callback."); @@ -1133,20 +1116,18 @@ void FrontendClientCallbackImpl::onScanMessage( FrontendScanMessageType type, const FrontendScanMessage& message) { ALOGV("FrontendClientCallbackImpl::onScanMessage, type=%d", type); JNIEnv *env = AndroidRuntime::getJNIEnv(); - jclass clazz = env->FindClass("android/media/tv/tuner/Tuner"); + ScopedLocalRef clazz(env, env->FindClass("android/media/tv/tuner/Tuner")); std::scoped_lock<std::mutex> lock(mMutex); for (const auto& mapEntry : mListenersMap) { - jobject frontend(env->NewLocalRef(mapEntry.second)); - if (env->IsSameObject(frontend, nullptr)) { + ScopedLocalRef frontend(env, env->NewLocalRef(mapEntry.second)); + if (env->IsSameObject(frontend.get(), nullptr)) { ALOGE("FrontendClientCallbackImpl::onScanMessage:" "Tuner object has been freed. Ignoring callback."); continue; } - executeOnScanMessage(env, clazz, frontend, type, message); - env->DeleteLocalRef(frontend); + executeOnScanMessage(env, clazz.get(), frontend.get(), type, message); } - env->DeleteLocalRef(clazz); } void FrontendClientCallbackImpl::executeOnScanMessage( @@ -1183,20 +1164,19 @@ void FrontendClientCallbackImpl::executeOnScanMessage( } case FrontendScanMessageType::FREQUENCY: { std::vector<int64_t> v = message.get<FrontendScanMessage::Tag::frequencies>(); - jlongArray freqs = env->NewLongArray(v.size()); - env->SetLongArrayRegion(freqs, 0, v.size(), reinterpret_cast<jlong *>(&v[0])); + ScopedLocalRef freqs(env, env->NewLongArray(v.size())); + env->SetLongArrayRegion(freqs.get(), 0, v.size(), reinterpret_cast<jlong *>(&v[0])); env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onFrequenciesReport", "([J)V"), - freqs); - env->DeleteLocalRef(freqs); + freqs.get()); break; } case FrontendScanMessageType::SYMBOL_RATE: { std::vector<int32_t> v = message.get<FrontendScanMessage::Tag::symbolRates>(); - jintArray symbolRates = env->NewIntArray(v.size()); - env->SetIntArrayRegion(symbolRates, 0, v.size(), reinterpret_cast<jint *>(&v[0])); + ScopedLocalRef symbolRates(env, env->NewIntArray(v.size())); + env->SetIntArrayRegion(symbolRates.get(), 0, v.size(), + reinterpret_cast<jint *>(&v[0])); env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onSymbolRates", "([I)V"), - symbolRates); - env->DeleteLocalRef(symbolRates); + symbolRates.get()); break; } case FrontendScanMessageType::HIERARCHY: { @@ -1211,27 +1191,29 @@ void FrontendClientCallbackImpl::executeOnScanMessage( } case FrontendScanMessageType::PLP_IDS: { std::vector<int32_t> jintV = message.get<FrontendScanMessage::Tag::plpIds>(); - jintArray plpIds = env->NewIntArray(jintV.size()); - env->SetIntArrayRegion(plpIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0])); - env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onPlpIds", "([I)V"), plpIds); - env->DeleteLocalRef(plpIds); + ScopedLocalRef plpIds(env, env->NewIntArray(jintV.size())); + env->SetIntArrayRegion(plpIds.get(), 0, jintV.size(), + reinterpret_cast<jint *>(&jintV[0])); + env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onPlpIds", "([I)V"), + plpIds.get()); break; } case FrontendScanMessageType::GROUP_IDS: { std::vector<int32_t> jintV = message.get<FrontendScanMessage::groupIds>(); - jintArray groupIds = env->NewIntArray(jintV.size()); - env->SetIntArrayRegion(groupIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0])); - env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onGroupIds", "([I)V"), groupIds); - env->DeleteLocalRef(groupIds); + ScopedLocalRef groupIds(env, env->NewIntArray(jintV.size())); + env->SetIntArrayRegion(groupIds.get(), 0, jintV.size(), + reinterpret_cast<jint *>(&jintV[0])); + env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onGroupIds", "([I)V"), + groupIds.get()); break; } case FrontendScanMessageType::INPUT_STREAM_IDS: { std::vector<int32_t> jintV = message.get<FrontendScanMessage::inputStreamIds>(); - jintArray streamIds = env->NewIntArray(jintV.size()); - env->SetIntArrayRegion(streamIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0])); + ScopedLocalRef streamIds(env, env->NewIntArray(jintV.size())); + env->SetIntArrayRegion(streamIds.get(), 0, jintV.size(), + reinterpret_cast<jint *>(&jintV[0])); env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onInputStreamIds", "([I)V"), - streamIds); - env->DeleteLocalRef(streamIds); + streamIds.get()); break; } case FrontendScanMessageType::STANDARD: { @@ -1254,26 +1236,25 @@ void FrontendClientCallbackImpl::executeOnScanMessage( break; } case FrontendScanMessageType::ATSC3_PLP_INFO: { - jclass plpClazz = env->FindClass("android/media/tv/tuner/frontend/Atsc3PlpInfo"); - jmethodID init = env->GetMethodID(plpClazz, "<init>", "(IZ)V"); + ScopedLocalRef plpClazz(env, + env->FindClass("android/media/tv/tuner/frontend/Atsc3PlpInfo")); + jmethodID init = env->GetMethodID(plpClazz.get(), "<init>", "(IZ)V"); std::vector<FrontendScanAtsc3PlpInfo> plpInfos = message.get<FrontendScanMessage::atsc3PlpInfos>(); - jobjectArray array = env->NewObjectArray(plpInfos.size(), plpClazz, nullptr); + ScopedLocalRef array(env, + env->NewObjectArray(plpInfos.size(), plpClazz.get(), nullptr)); for (int i = 0; i < plpInfos.size(); i++) { const FrontendScanAtsc3PlpInfo &info = plpInfos[i]; jint plpId = info.plpId; jboolean lls = info.bLlsFlag; - jobject obj = env->NewObject(plpClazz, init, plpId, lls); - env->SetObjectArrayElement(array, i, obj); - env->DeleteLocalRef(obj); + ScopedLocalRef obj(env, env->NewObject(plpClazz.get(), init, plpId, lls)); + env->SetObjectArrayElement(array.get(), i, obj.get()); } env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onAtsc3PlpInfos", "([Landroid/media/tv/tuner/frontend/" "Atsc3PlpInfo;)V"), - array); - env->DeleteLocalRef(array); - env->DeleteLocalRef(plpClazz); + array.get()); break; } case FrontendScanMessageType::MODULATION: { @@ -1341,11 +1322,12 @@ void FrontendClientCallbackImpl::executeOnScanMessage( } case FrontendScanMessageType::DVBT_CELL_IDS: { std::vector<int32_t> jintV = message.get<FrontendScanMessage::dvbtCellIds>(); - jintArray cellIds = env->NewIntArray(jintV.size()); - env->SetIntArrayRegion(cellIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0])); - env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onDvbtCellIdsReported", "([I)V"), - cellIds); - env->DeleteLocalRef(cellIds); + ScopedLocalRef cellIds(env, env->NewIntArray(jintV.size())); + env->SetIntArrayRegion(cellIds.get(), 0, jintV.size(), + reinterpret_cast<jint *>(&jintV[0])); + env->CallVoidMethod(frontend, + env->GetMethodID(clazz, "onDvbtCellIdsReported", "([I)V"), + cellIds.get()); break; } default: @@ -1434,7 +1416,8 @@ jobject JTuner::getFrontendIds() { JNIEnv *env = AndroidRuntime::getJNIEnv(); jclass arrayListClazz = env->FindClass("java/util/ArrayList"); jmethodID arrayListAdd = env->GetMethodID(arrayListClazz, "add", "(Ljava/lang/Object;)Z"); - jobject obj = env->NewObject(arrayListClazz, env->GetMethodID(arrayListClazz, "<init>", "()V")); + jobject obj = env->NewObject(arrayListClazz, + env->GetMethodID(arrayListClazz, "<init>", "()V")); jclass integerClazz = env->FindClass("java/lang/Integer"); jmethodID intInit = env->GetMethodID(integerClazz, "<init>", "(I)V"); @@ -1672,7 +1655,7 @@ jobject JTuner::getFrontendInfo(int id) { jclass clazz = env->FindClass("android/media/tv/tuner/frontend/FrontendInfo"); jmethodID infoInit = env->GetMethodID(clazz, "<init>", - "(IIJJIIJI[ILandroid/media/tv/tuner/frontend/FrontendCapabilities;)V"); + "(IIJJIIJI[ILandroid/media/tv/tuner/frontend/FrontendCapabilities;)V"); jint type = (jint)feInfo->type; jlong minFrequency = feInfo->minFrequency; @@ -1812,9 +1795,8 @@ jobjectArray JTuner::getFrontendStatusReadiness(jintArray types) { jmethodID init = env->GetMethodID(clazz, "<init>", "(II)V"); jobjectArray valObj = env->NewObjectArray(size, clazz, nullptr); for (int i = 0; i < size; i++) { - jobject readinessObj = env->NewObject(clazz, init, intTypes[i], readiness[i]); - env->SetObjectArrayElement(valObj, i, readinessObj); - env->DeleteLocalRef(readinessObj); + ScopedLocalRef readinessObj(env, env->NewObject(clazz, init, intTypes[i], readiness[i])); + env->SetObjectArrayElement(valObj, i, readinessObj.get()); } return valObj; } @@ -2260,79 +2242,72 @@ jobject JTuner::getFrontendStatus(jintArray types) { switch (s.getTag()) { case FrontendStatus::Tag::isDemodLocked: { jfieldID field = env->GetFieldID(clazz, "mIsDemodLocked", "Ljava/lang/Boolean;"); - jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, - s.get<FrontendStatus::Tag::isDemodLocked>()); - env->SetObjectField(statusObj, field, newBooleanObj); - env->DeleteLocalRef(newBooleanObj); + ScopedLocalRef newBooleanObj(env, + env->NewObject(booleanClazz, initBoolean, + s.get<FrontendStatus::Tag::isDemodLocked>())); + env->SetObjectField(statusObj, field, newBooleanObj.get()); break; } case FrontendStatus::Tag::snr: { jfieldID field = env->GetFieldID(clazz, "mSnr", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::snr>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::snr>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::ber: { jfieldID field = env->GetFieldID(clazz, "mBer", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::ber>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::ber>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::per: { jfieldID field = env->GetFieldID(clazz, "mPer", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::per>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::per>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::preBer: { jfieldID field = env->GetFieldID(clazz, "mPerBer", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::preBer>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::preBer>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::signalQuality: { jfieldID field = env->GetFieldID(clazz, "mSignalQuality", "Ljava/lang/Integer;"); - jobject newIntegerObj = env->NewObject(intClazz, initInt, - s.get<FrontendStatus::Tag::signalQuality>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, + s.get<FrontendStatus::Tag::signalQuality>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::signalStrength: { jfieldID field = env->GetFieldID(clazz, "mSignalStrength", "Ljava/lang/Integer;"); - jobject newIntegerObj = + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, - s.get<FrontendStatus::Tag::signalStrength>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + s.get<FrontendStatus::Tag::signalStrength>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::symbolRate: { jfieldID field = env->GetFieldID(clazz, "mSymbolRate", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::symbolRate>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, + s.get<FrontendStatus::Tag::symbolRate>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::innerFec: { jfieldID field = env->GetFieldID(clazz, "mInnerFec", "Ljava/lang/Long;"); - jclass longClazz = env->FindClass("java/lang/Long"); - jmethodID initLong = env->GetMethodID(longClazz, "<init>", "(J)V"); - jobject newLongObj = - env->NewObject(longClazz, initLong, - static_cast<long>(s.get<FrontendStatus::Tag::innerFec>())); - env->SetObjectField(statusObj, field, newLongObj); - env->DeleteLocalRef(newLongObj); - env->DeleteLocalRef(longClazz); + ScopedLocalRef longClazz(env, env->FindClass("java/lang/Long")); + jmethodID initLong = env->GetMethodID(longClazz.get(), "<init>", "(J)V"); + ScopedLocalRef newLongObj(env, + env->NewObject(longClazz.get(), initLong, + static_cast<long>(s.get<FrontendStatus::Tag::innerFec>()))); + env->SetObjectField(statusObj, field, newLongObj.get()); break; } case FrontendStatus::Tag::modulationStatus: { @@ -2373,139 +2348,128 @@ jobject JTuner::getFrontendStatus(jintArray types) { } } if (valid) { - jobject newIntegerObj = env->NewObject(intClazz, initInt, intModulation); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, intModulation)); + env->SetObjectField(statusObj, field, newIntegerObj.get()); } break; } case FrontendStatus::Tag::inversion: { jfieldID field = env->GetFieldID(clazz, "mInversion", "Ljava/lang/Integer;"); - jobject newIntegerObj = + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, - static_cast<jint>(s.get<FrontendStatus::Tag::inversion>())); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + static_cast<jint>(s.get<FrontendStatus::Tag::inversion>()))); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::lnbVoltage: { jfieldID field = env->GetFieldID(clazz, "mLnbVoltage", "Ljava/lang/Integer;"); - jobject newIntegerObj = + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, - static_cast<jint>(s.get<FrontendStatus::Tag::lnbVoltage>())); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + static_cast<jint>(s.get<FrontendStatus::Tag::lnbVoltage>()))); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::plpId: { jfieldID field = env->GetFieldID(clazz, "mPlpId", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::plpId>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::plpId>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::isEWBS: { jfieldID field = env->GetFieldID(clazz, "mIsEwbs", "Ljava/lang/Boolean;"); - jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, - s.get<FrontendStatus::Tag::isEWBS>()); - env->SetObjectField(statusObj, field, newBooleanObj); - env->DeleteLocalRef(newBooleanObj); + ScopedLocalRef newBooleanObj(env, env->NewObject(booleanClazz, initBoolean, + s.get<FrontendStatus::Tag::isEWBS>())); + env->SetObjectField(statusObj, field, newBooleanObj.get()); break; } case FrontendStatus::Tag::agc: { jfieldID field = env->GetFieldID(clazz, "mAgc", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::agc>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::agc>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::isLnaOn: { jfieldID field = env->GetFieldID(clazz, "mIsLnaOn", "Ljava/lang/Boolean;"); - jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, - s.get<FrontendStatus::Tag::isLnaOn>()); - env->SetObjectField(statusObj, field, newBooleanObj); - env->DeleteLocalRef(newBooleanObj); + ScopedLocalRef newBooleanObj(env, env->NewObject(booleanClazz, initBoolean, + s.get<FrontendStatus::Tag::isLnaOn>())); + env->SetObjectField(statusObj, field, newBooleanObj.get()); break; } case FrontendStatus::Tag::isLayerError: { jfieldID field = env->GetFieldID(clazz, "mIsLayerErrors", "[Z"); vector<bool> layerErr = s.get<FrontendStatus::Tag::isLayerError>(); - jbooleanArray valObj = env->NewBooleanArray(layerErr.size()); + ScopedLocalRef valObj(env, env->NewBooleanArray(layerErr.size())); for (size_t i = 0; i < layerErr.size(); i++) { jboolean x = layerErr[i]; - env->SetBooleanArrayRegion(valObj, i, 1, &x); + env->SetBooleanArrayRegion(valObj.get(), i, 1, &x); } - env->SetObjectField(statusObj, field, valObj); - env->DeleteLocalRef(valObj); + env->SetObjectField(statusObj, field, valObj.get()); break; } case FrontendStatus::Tag::mer: { jfieldID field = env->GetFieldID(clazz, "mMer", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::mer>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::mer>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::freqOffset: { jfieldID field = env->GetFieldID(clazz, "mFreqOffset", "Ljava/lang/Long;"); - jobject newLongObj = env->NewObject(longClazz, initLong, - s.get<FrontendStatus::Tag::freqOffset>()); - env->SetObjectField(statusObj, field, newLongObj); - env->DeleteLocalRef(newLongObj); + ScopedLocalRef newLongObj(env, env->NewObject(longClazz, initLong, + s.get<FrontendStatus::Tag::freqOffset>())); + env->SetObjectField(statusObj, field, newLongObj.get()); break; } case FrontendStatus::Tag::hierarchy: { jfieldID field = env->GetFieldID(clazz, "mHierarchy", "Ljava/lang/Integer;"); - jobject newIntegerObj = + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, - static_cast<jint>(s.get<FrontendStatus::Tag::hierarchy>())); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + static_cast<jint>(s.get<FrontendStatus::Tag::hierarchy>()))); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::isRfLocked: { jfieldID field = env->GetFieldID(clazz, "mIsRfLocked", "Ljava/lang/Boolean;"); - jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, - s.get<FrontendStatus::Tag::isRfLocked>()); - env->SetObjectField(statusObj, field, newBooleanObj); - env->DeleteLocalRef(newBooleanObj); + ScopedLocalRef newBooleanObj(env, env->NewObject(booleanClazz, initBoolean, + s.get<FrontendStatus::Tag::isRfLocked>())); + env->SetObjectField(statusObj, field, newBooleanObj.get()); break; } case FrontendStatus::Tag::plpInfo: { jfieldID field = env->GetFieldID(clazz, "mPlpInfo", "[Landroid/media/tv/tuner/frontend/FrontendStatus$Atsc3PlpTuningInfo;"); - jclass plpClazz = env->FindClass( - "android/media/tv/tuner/frontend/FrontendStatus$Atsc3PlpTuningInfo"); - jmethodID initPlp = env->GetMethodID(plpClazz, "<init>", "(IZI)V"); - - vector<FrontendStatusAtsc3PlpInfo> plpInfos = s.get<FrontendStatus::Tag::plpInfo>(); - jobjectArray valObj = env->NewObjectArray(plpInfos.size(), plpClazz, nullptr); + ScopedLocalRef plpClazz(env, env->FindClass( + "android/media/tv/tuner/frontend/FrontendStatus$Atsc3PlpTuningInfo")); + jmethodID initPlp = env->GetMethodID(plpClazz.get(), "<init>", "(IZI)V"); + + vector<FrontendStatusAtsc3PlpInfo> plpInfos = + s.get<FrontendStatus::Tag::plpInfo>(); + ScopedLocalRef valObj(env, env->NewObjectArray(plpInfos.size(), plpClazz.get(), + nullptr)); for (int i = 0; i < plpInfos.size(); i++) { const FrontendStatusAtsc3PlpInfo &info = plpInfos[i]; jint plpId = info.plpId; jboolean isLocked = info.isLocked; jint uec = info.uec; - jobject plpObj = env->NewObject(plpClazz, initPlp, plpId, isLocked, uec); - env->SetObjectArrayElement(valObj, i, plpObj); - env->DeleteLocalRef(plpObj); + ScopedLocalRef plpObj(env, env->NewObject(plpClazz.get(), initPlp, plpId, + isLocked, uec)); + env->SetObjectArrayElement(valObj.get(), i, plpObj.get()); } - env->SetObjectField(statusObj, field, valObj); - env->DeleteLocalRef(valObj); - env->DeleteLocalRef(plpClazz); + env->SetObjectField(statusObj, field, valObj.get()); break; } case FrontendStatus::Tag::modulations: { jfieldID field = env->GetFieldID(clazz, "mModulationsExt", "[I"); std::vector<FrontendModulation> v = s.get<FrontendStatus::Tag::modulations>(); - jintArray valObj = env->NewIntArray(v.size()); + ScopedLocalRef valObj(env, env->NewIntArray(v.size())); bool valid = false; jint m[1]; for (int i = 0; i < v.size(); i++) { @@ -2514,63 +2478,63 @@ jobject JTuner::getFrontendStatus(jintArray types) { case FrontendModulation::Tag::dvbc: { m[0] = static_cast<jint>( modulation.get<FrontendModulation::Tag::dvbc>()); - env->SetIntArrayRegion(valObj, i, 1, m); + env->SetIntArrayRegion(valObj.get(), i, 1, m); valid = true; break; } case FrontendModulation::Tag::dvbs: { m[0] = static_cast<jint>( modulation.get<FrontendModulation::Tag::dvbs>()); - env->SetIntArrayRegion(valObj, i, 1, m); + env->SetIntArrayRegion(valObj.get(), i, 1, m); valid = true; break; } case FrontendModulation::Tag::dvbt: { m[0] = static_cast<jint>( modulation.get<FrontendModulation::Tag::dvbt>()); - env->SetIntArrayRegion(valObj, i, 1, m); + env->SetIntArrayRegion(valObj.get(), i, 1, m); valid = true; break; } case FrontendModulation::Tag::isdbs: { m[0] = static_cast<jint>( modulation.get<FrontendModulation::Tag::isdbs>()); - env->SetIntArrayRegion(valObj, i, 1, m); + env->SetIntArrayRegion(valObj.get(), i, 1, m); valid = true; break; } case FrontendModulation::Tag::isdbs3: { m[0] = static_cast<jint>( modulation.get<FrontendModulation::Tag::isdbs3>()); - env->SetIntArrayRegion(valObj, i, 1, m); + env->SetIntArrayRegion(valObj.get(), i, 1, m); valid = true; break; } case FrontendModulation::Tag::isdbt: { m[0] = static_cast<jint>( modulation.get<FrontendModulation::Tag::isdbt>()); - env->SetIntArrayRegion(valObj, i, 1, m); + env->SetIntArrayRegion(valObj.get(), i, 1, m); valid = true; break; } case FrontendModulation::Tag::atsc: { m[0] = static_cast<jint>( modulation.get<FrontendModulation::Tag::atsc>()); - env->SetIntArrayRegion(valObj, i, 1, m); + env->SetIntArrayRegion(valObj.get(), i, 1, m); valid = true; break; } case FrontendModulation::Tag::atsc3: { m[0] = static_cast<jint>( modulation.get<FrontendModulation::Tag::atsc3>()); - env->SetIntArrayRegion(valObj, i, 1, m); + env->SetIntArrayRegion(valObj.get(), i, 1, m); valid = true; break; } case FrontendModulation::Tag::dtmb: { m[0] = static_cast<jint>( modulation.get<FrontendModulation::Tag::dtmb>()); - env->SetIntArrayRegion(valObj, i, 1, m); + env->SetIntArrayRegion(valObj.get(), i, 1, m); valid = true; break; } @@ -2579,31 +2543,28 @@ jobject JTuner::getFrontendStatus(jintArray types) { } } if (valid) { - env->SetObjectField(statusObj, field, valObj); + env->SetObjectField(statusObj, field, valObj.get()); } - env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::bers: { jfieldID field = env->GetFieldID(clazz, "mBers", "[I"); std::vector<int32_t> v = s.get<FrontendStatus::Tag::bers>(); - jintArray valObj = env->NewIntArray(v.size()); - env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&v[0])); + ScopedLocalRef valObj(env, env->NewIntArray(v.size())); + env->SetIntArrayRegion(valObj.get(), 0, v.size(), reinterpret_cast<jint *>(&v[0])); - env->SetObjectField(statusObj, field, valObj); - env->DeleteLocalRef(valObj); + env->SetObjectField(statusObj, field, valObj.get()); break; } case FrontendStatus::Tag::codeRates: { jfieldID field = env->GetFieldID(clazz, "mCodeRates", "[I"); std::vector<FrontendInnerFec> v = s.get<FrontendStatus::Tag::codeRates>(); - jintArray valObj = env->NewIntArray(v.size()); - env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&v[0])); + ScopedLocalRef valObj(env, env->NewIntArray(v.size())); + env->SetIntArrayRegion(valObj.get(), 0, v.size(), reinterpret_cast<jint *>(&v[0])); - env->SetObjectField(statusObj, field, valObj); - env->DeleteLocalRef(valObj); + env->SetObjectField(statusObj, field, valObj.get()); break; } case FrontendStatus::Tag::bandwidth: { @@ -2642,9 +2603,9 @@ jobject JTuner::getFrontendStatus(jintArray types) { break; } if (valid) { - jobject newIntegerObj = env->NewObject(intClazz, initInt, intBandwidth); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, + intBandwidth)); + env->SetObjectField(statusObj, field, newIntegerObj.get()); } break; } @@ -2655,8 +2616,8 @@ jobject JTuner::getFrontendStatus(jintArray types) { bool valid = true; switch (interval.getTag()) { case FrontendGuardInterval::Tag::dvbt: { - intInterval = - static_cast<jint>(interval.get<FrontendGuardInterval::Tag::dvbt>()); + intInterval = static_cast<jint>( + interval.get<FrontendGuardInterval::Tag::dvbt>()); break; } case FrontendGuardInterval::Tag::isdbt: { @@ -2665,8 +2626,8 @@ jobject JTuner::getFrontendStatus(jintArray types) { break; } case FrontendGuardInterval::Tag::dtmb: { - intInterval = - static_cast<jint>(interval.get<FrontendGuardInterval::Tag::dtmb>()); + intInterval = static_cast<jint>( + interval.get<FrontendGuardInterval::Tag::dtmb>()); break; } default: @@ -2674,14 +2635,15 @@ jobject JTuner::getFrontendStatus(jintArray types) { break; } if (valid) { - jobject newIntegerObj = env->NewObject(intClazz, initInt, intInterval); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, + intInterval)); + env->SetObjectField(statusObj, field, newIntegerObj.get()); } break; } case FrontendStatus::Tag::transmissionMode: { - jfieldID field = env->GetFieldID(clazz, "mTransmissionMode", "Ljava/lang/Integer;"); + jfieldID field = env->GetFieldID(clazz, "mTransmissionMode", + "Ljava/lang/Integer;"); const FrontendTransmissionMode &transmissionMode = s.get<FrontendStatus::Tag::transmissionMode>(); jint intTransmissionMode; @@ -2707,32 +2669,30 @@ jobject JTuner::getFrontendStatus(jintArray types) { break; } if (valid) { - jobject newIntegerObj = env->NewObject(intClazz, initInt, intTransmissionMode); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, + intTransmissionMode)); + env->SetObjectField(statusObj, field, newIntegerObj.get()); } break; } case FrontendStatus::Tag::uec: { jfieldID field = env->GetFieldID(clazz, "mUec", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::uec>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::uec>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::systemId: { jfieldID field = env->GetFieldID(clazz, "mSystemId", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::systemId>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::systemId>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::interleaving: { jfieldID field = env->GetFieldID(clazz, "mInterleaving", "[I"); std::vector<FrontendInterleaveMode> v = s.get<FrontendStatus::Tag::interleaving>(); - jintArray valObj = env->NewIntArray(v.size()); + ScopedLocalRef valObj(env, env->NewIntArray(v.size())); bool valid = false; jint in[1]; for (int i = 0; i < v.size(); i++) { @@ -2741,28 +2701,28 @@ jobject JTuner::getFrontendStatus(jintArray types) { case FrontendInterleaveMode::Tag::atsc3: { in[0] = static_cast<jint>( interleaving.get<FrontendInterleaveMode::Tag::atsc3>()); - env->SetIntArrayRegion(valObj, i, 1, in); + env->SetIntArrayRegion(valObj.get(), i, 1, in); valid = true; break; } case FrontendInterleaveMode::Tag::dvbc: { in[0] = static_cast<jint>( interleaving.get<FrontendInterleaveMode::Tag::dvbc>()); - env->SetIntArrayRegion(valObj, i, 1, in); + env->SetIntArrayRegion(valObj.get(), i, 1, in); valid = true; break; } case FrontendInterleaveMode::Tag::dtmb: { in[0] = static_cast<jint>( interleaving.get<FrontendInterleaveMode::Tag::dtmb>()); - env->SetIntArrayRegion(valObj, i, 1, in); + env->SetIntArrayRegion(valObj.get(), i, 1, in); valid = true; break; } case FrontendInterleaveMode::Tag::isdbt: { in[0] = static_cast<jint>( interleaving.get<FrontendInterleaveMode::Tag::isdbt>()); - env->SetIntArrayRegion(valObj, i, 1, in); + env->SetIntArrayRegion(valObj.get(), i, 1, in); valid = true; break; } @@ -2771,31 +2731,28 @@ jobject JTuner::getFrontendStatus(jintArray types) { } } if (valid) { - env->SetObjectField(statusObj, field, valObj); + env->SetObjectField(statusObj, field, valObj.get()); } - env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::isdbtSegment: { jfieldID field = env->GetFieldID(clazz, "mIsdbtSegment", "[I"); std::vector<int32_t> v = s.get<FrontendStatus::Tag::isdbtSegment>(); - jintArray valObj = env->NewIntArray(v.size()); - env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint*>(&v[0])); + ScopedLocalRef valObj(env, env->NewIntArray(v.size())); + env->SetIntArrayRegion(valObj.get(), 0, v.size(), reinterpret_cast<jint*>(&v[0])); - env->SetObjectField(statusObj, field, valObj); - env->DeleteLocalRef(valObj); + env->SetObjectField(statusObj, field, valObj.get()); break; } case FrontendStatus::Tag::tsDataRate: { jfieldID field = env->GetFieldID(clazz, "mTsDataRate", "[I"); std::vector<int32_t> v = s.get<FrontendStatus::Tag::tsDataRate>(); - jintArray valObj = env->NewIntArray(v.size()); - env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&v[0])); + ScopedLocalRef valObj(env, env->NewIntArray(v.size())); + env->SetIntArrayRegion(valObj.get(), 0, v.size(), reinterpret_cast<jint *>(&v[0])); - env->SetObjectField(statusObj, field, valObj); - env->DeleteLocalRef(valObj); + env->SetObjectField(statusObj, field, valObj.get()); break; } case FrontendStatus::Tag::rollOff: { @@ -2813,7 +2770,8 @@ jobject JTuner::getFrontendStatus(jintArray types) { break; } case FrontendRollOff::Tag::isdbs3: { - intRollOff = static_cast<jint>(rollOff.get<FrontendRollOff::Tag::isdbs3>()); + intRollOff = static_cast<jint>( + rollOff.get<FrontendRollOff::Tag::isdbs3>()); break; } default: @@ -2821,141 +2779,135 @@ jobject JTuner::getFrontendStatus(jintArray types) { break; } if (valid) { - jobject newIntegerObj = env->NewObject(intClazz, initInt, intRollOff); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, + intRollOff)); + env->SetObjectField(statusObj, field, newIntegerObj.get()); } break; } case FrontendStatus::Tag::isMiso: { jfieldID field = env->GetFieldID(clazz, "mIsMisoEnabled", "Ljava/lang/Boolean;"); - jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, - s.get<FrontendStatus::Tag::isMiso>()); - env->SetObjectField(statusObj, field, newBooleanObj); - env->DeleteLocalRef(newBooleanObj); + ScopedLocalRef newBooleanObj(env, env->NewObject(booleanClazz, initBoolean, + s.get<FrontendStatus::Tag::isMiso>())); + env->SetObjectField(statusObj, field, newBooleanObj.get()); break; } case FrontendStatus::Tag::isLinear: { jfieldID field = env->GetFieldID(clazz, "mIsLinear", "Ljava/lang/Boolean;"); - jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, - s.get<FrontendStatus::Tag::isLinear>()); - env->SetObjectField(statusObj, field, newBooleanObj); - env->DeleteLocalRef(newBooleanObj); + ScopedLocalRef newBooleanObj(env, env->NewObject(booleanClazz, initBoolean, + s.get<FrontendStatus::Tag::isLinear>())); + env->SetObjectField(statusObj, field, newBooleanObj.get()); break; } case FrontendStatus::Tag::isShortFrames: { jfieldID field = env->GetFieldID(clazz, "mIsShortFrames", "Ljava/lang/Boolean;"); - jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, - s.get<FrontendStatus::Tag::isShortFrames>()); - env->SetObjectField(statusObj, field, newBooleanObj); - env->DeleteLocalRef(newBooleanObj); + ScopedLocalRef newBooleanObj(env, + env->NewObject(booleanClazz, initBoolean, + s.get<FrontendStatus::Tag::isShortFrames>())); + env->SetObjectField(statusObj, field, newBooleanObj.get()); break; } case FrontendStatus::Tag::isdbtMode: { jfieldID field = env->GetFieldID(clazz, "mIsdbtMode", "Ljava/lang/Integer;"); - jobject newIntegerObj = - env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::isdbtMode>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + ScopedLocalRef newIntegerObj(env, + env->NewObject(intClazz, initInt, + s.get<FrontendStatus::Tag::isdbtMode>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::partialReceptionFlag: { jfieldID field = - env->GetFieldID(clazz, "mIsdbtPartialReceptionFlag", "Ljava/lang/Integer;"); - jobject newIntegerObj = + env->GetFieldID(clazz, "mIsdbtPartialReceptionFlag", + "Ljava/lang/Integer;"); + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, - s.get<FrontendStatus::Tag::partialReceptionFlag>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + s.get<FrontendStatus::Tag::partialReceptionFlag>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::streamIdList: { jfieldID field = env->GetFieldID(clazz, "mStreamIds", "[I"); std::vector<int32_t> ids = s.get<FrontendStatus::Tag::streamIdList>(); - jintArray valObj = env->NewIntArray(v.size()); - env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&ids[0])); + ScopedLocalRef valObj(env, env->NewIntArray(v.size())); + env->SetIntArrayRegion(valObj.get(), 0, v.size(), + reinterpret_cast<jint *>(&ids[0])); - env->SetObjectField(statusObj, field, valObj); - env->DeleteLocalRef(valObj); + env->SetObjectField(statusObj, field, valObj.get()); break; } case FrontendStatus::Tag::dvbtCellIds: { jfieldID field = env->GetFieldID(clazz, "mDvbtCellIds", "[I"); std::vector<int32_t> ids = s.get<FrontendStatus::Tag::dvbtCellIds>(); - jintArray valObj = env->NewIntArray(v.size()); - env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&ids[0])); + ScopedLocalRef valObj(env, env->NewIntArray(v.size())); + env->SetIntArrayRegion(valObj.get(), 0, v.size(), + reinterpret_cast<jint *>(&ids[0])); - env->SetObjectField(statusObj, field, valObj); - env->DeleteLocalRef(valObj); + env->SetObjectField(statusObj, field, valObj.get()); break; } case FrontendStatus::Tag::allPlpInfo: { jfieldID field = env->GetFieldID(clazz, "mAllPlpInfo", - "[Landroid/media/tv/tuner/frontend/Atsc3PlpInfo;"); - jclass plpClazz = env->FindClass("android/media/tv/tuner/frontend/Atsc3PlpInfo"); - jmethodID initPlp = env->GetMethodID(plpClazz, "<init>", "(IZ)V"); + "[Landroid/media/tv/tuner/frontend/Atsc3PlpInfo;"); + ScopedLocalRef plpClazz(env, + env->FindClass("android/media/tv/tuner/frontend/Atsc3PlpInfo")); + jmethodID initPlp = env->GetMethodID(plpClazz.get(), "<init>", "(IZ)V"); vector<FrontendScanAtsc3PlpInfo> plpInfos = s.get<FrontendStatus::Tag::allPlpInfo>(); - jobjectArray valObj = env->NewObjectArray(plpInfos.size(), plpClazz, nullptr); + ScopedLocalRef valObj(env, env->NewObjectArray(plpInfos.size(), plpClazz.get(), + nullptr)); for (int i = 0; i < plpInfos.size(); i++) { - jobject plpObj = env->NewObject(plpClazz, initPlp, plpInfos[i].plpId, - plpInfos[i].bLlsFlag); - env->SetObjectArrayElement(valObj, i, plpObj); - env->DeleteLocalRef(plpObj); + ScopedLocalRef plpObj(env, env->NewObject(plpClazz.get(), initPlp, + plpInfos[i].plpId, + plpInfos[i].bLlsFlag)); + env->SetObjectArrayElement(valObj.get(), i, plpObj.get()); } - env->SetObjectField(statusObj, field, valObj); - env->DeleteLocalRef(valObj); - env->DeleteLocalRef(plpClazz); + env->SetObjectField(statusObj, field, valObj.get()); break; } case FrontendStatus::Tag::iptvContentUrl: { jfieldID field = env->GetFieldID(clazz, "mIptvContentUrl", "Ljava/lang/String;"); std::string iptvContentUrl = s.get<FrontendStatus::Tag::iptvContentUrl>(); - jstring iptvContentUrlUtf8 = env->NewStringUTF(iptvContentUrl.c_str()); - env->SetObjectField(statusObj, field, iptvContentUrlUtf8); - env->DeleteLocalRef(iptvContentUrlUtf8); + ScopedLocalRef iptvContentUrlUtf8(env, env->NewStringUTF(iptvContentUrl.c_str())); + env->SetObjectField(statusObj, field, iptvContentUrlUtf8.get()); break; } case FrontendStatus::Tag::iptvPacketsLost: { jfieldID field = env->GetFieldID(clazz, "mIptvPacketsLost", "Ljava/lang/Long;"); - jobject newLongObj = + ScopedLocalRef newLongObj(env, env->NewObject(longClazz, initLong, - s.get<FrontendStatus::Tag::iptvPacketsLost>()); - env->SetObjectField(statusObj, field, newLongObj); - env->DeleteLocalRef(newLongObj); + s.get<FrontendStatus::Tag::iptvPacketsLost>())); + env->SetObjectField(statusObj, field, newLongObj.get()); break; } case FrontendStatus::Tag::iptvPacketsReceived: { - jfieldID field = env->GetFieldID(clazz, "mIptvPacketsReceived", "Ljava/lang/Long;"); - jobject newLongObj = + jfieldID field = env->GetFieldID(clazz, "mIptvPacketsReceived", + "Ljava/lang/Long;"); + ScopedLocalRef newLongObj(env, env->NewObject(longClazz, initLong, - s.get<FrontendStatus::Tag::iptvPacketsReceived>()); - env->SetObjectField(statusObj, field, newLongObj); - env->DeleteLocalRef(newLongObj); + s.get<FrontendStatus::Tag::iptvPacketsReceived>())); + env->SetObjectField(statusObj, field, newLongObj.get()); break; } case FrontendStatus::Tag::iptvWorstJitterMs: { jfieldID field = env->GetFieldID(clazz, "mIptvWorstJitterMs", "Ljava/lang/Integer;"); - jobject newIntegerObj = + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, - s.get<FrontendStatus::Tag::iptvWorstJitterMs>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + s.get<FrontendStatus::Tag::iptvWorstJitterMs>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } case FrontendStatus::Tag::iptvAverageJitterMs: { jfieldID field = env->GetFieldID(clazz, "mIptvAverageJitterMs", "Ljava/lang/Integer;"); - jobject newIntegerObj = + ScopedLocalRef newIntegerObj(env, env->NewObject(intClazz, initInt, - s.get<FrontendStatus::Tag::iptvAverageJitterMs>()); - env->SetObjectField(statusObj, field, newIntegerObj); - env->DeleteLocalRef(newIntegerObj); + s.get<FrontendStatus::Tag::iptvAverageJitterMs>())); + env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } } @@ -3089,21 +3041,22 @@ static vector<FrontendAtsc3PlpSettings> getAtsc3PlpSettings(JNIEnv *env, const j vector<FrontendAtsc3PlpSettings> plps = vector<FrontendAtsc3PlpSettings>(len); // parse PLP settings for (int i = 0; i < len; i++) { - jobject plp = env->GetObjectArrayElement(plpSettings, i); - int32_t plpId = env->GetIntField(plp, env->GetFieldID(plpClazz, "mPlpId", "I")); + ScopedLocalRef plp(env, env->GetObjectArrayElement(plpSettings, i)); + int32_t plpId = env->GetIntField(plp.get(), env->GetFieldID(plpClazz, "mPlpId", "I")); FrontendAtsc3Modulation modulation = static_cast<FrontendAtsc3Modulation>( - env->GetIntField(plp, env->GetFieldID(plpClazz, "mModulation", "I"))); + env->GetIntField(plp.get(), env->GetFieldID(plpClazz, "mModulation", + "I"))); FrontendAtsc3TimeInterleaveMode interleaveMode = static_cast<FrontendAtsc3TimeInterleaveMode>( env->GetIntField( - plp, env->GetFieldID(plpClazz, "mInterleaveMode", "I"))); + plp.get(), env->GetFieldID(plpClazz, "mInterleaveMode", "I"))); FrontendAtsc3CodeRate codeRate = static_cast<FrontendAtsc3CodeRate>( - env->GetIntField(plp, env->GetFieldID(plpClazz, "mCodeRate", "I"))); + env->GetIntField(plp.get(), env->GetFieldID(plpClazz, "mCodeRate", "I"))); FrontendAtsc3Fec fec = static_cast<FrontendAtsc3Fec>( - env->GetIntField(plp, env->GetFieldID(plpClazz, "mFec", "I"))); + env->GetIntField(plp.get(), env->GetFieldID(plpClazz, "mFec", "I"))); FrontendAtsc3PlpSettings frontendAtsc3PlpSettings { .plpId = plpId, .modulation = modulation, @@ -3112,7 +3065,6 @@ static vector<FrontendAtsc3PlpSettings> getAtsc3PlpSettings(JNIEnv *env, const j .fec = fec, }; plps[i] = frontendAtsc3PlpSettings; - env->DeleteLocalRef(plp); } return plps; } @@ -3457,18 +3409,17 @@ static FrontendSettings getIsdbtFrontendSettings(JNIEnv *env, const jobject& set "android/media/tv/tuner/frontend/IsdbtFrontendSettings$IsdbtLayerSettings"); frontendIsdbtSettings.layerSettings.resize(len); for (int i = 0; i < len; i++) { - jobject layer = env->GetObjectArrayElement(layerSettings, i); + ScopedLocalRef layer(env, env->GetObjectArrayElement(layerSettings, i)); frontendIsdbtSettings.layerSettings[i].modulation = static_cast<FrontendIsdbtModulation>( - env->GetIntField(layer, env->GetFieldID(layerClazz, "mModulation", "I"))); + env->GetIntField(layer.get(), env->GetFieldID(layerClazz, "mModulation", "I"))); frontendIsdbtSettings.layerSettings[i].timeInterleave = static_cast<FrontendIsdbtTimeInterleaveMode>( - env->GetIntField(layer, + env->GetIntField(layer.get(), env->GetFieldID(layerClazz, "mTimeInterleaveMode", "I"))); frontendIsdbtSettings.layerSettings[i].coderate = static_cast<FrontendIsdbtCoderate>( - env->GetIntField(layer, env->GetFieldID(layerClazz, "mCodeRate", "I"))); + env->GetIntField(layer.get(), env->GetFieldID(layerClazz, "mCodeRate", "I"))); frontendIsdbtSettings.layerSettings[i].numOfSegment = - env->GetIntField(layer, env->GetFieldID(layerClazz, "mNumOfSegments", "I")); - env->DeleteLocalRef(layer); + env->GetIntField(layer.get(), env->GetFieldID(layerClazz, "mNumOfSegments", "I")); } frontendSettings.set<FrontendSettings::Tag::isdbt>(frontendIsdbtSettings); @@ -3498,7 +3449,8 @@ static FrontendSettings getDtmbFrontendSettings(JNIEnv *env, const jobject &sett env->GetIntField(settings, env->GetFieldID(clazz, "mGuardInterval", "I"))); FrontendDtmbTimeInterleaveMode interleaveMode = static_cast<FrontendDtmbTimeInterleaveMode>( - env->GetIntField(settings, env->GetFieldID(clazz, "mTimeInterleaveMode", "I"))); + env->GetIntField(settings, env->GetFieldID(clazz, "mTimeInterleaveMode", + "I"))); FrontendDtmbSettings frontendDtmbSettings{ .frequency = freq, @@ -3515,7 +3467,8 @@ static FrontendSettings getDtmbFrontendSettings(JNIEnv *env, const jobject &sett return frontendSettings; } -static DemuxIpAddress getDemuxIpAddress(JNIEnv *env, const jobject& config, const char* className) { +static DemuxIpAddress getDemuxIpAddress(JNIEnv *env, const jobject& config, + const char* className) { jclass clazz = env->FindClass(className); jbyteArray jsrcIpAddress = static_cast<jbyteArray>( diff --git a/media/jni/android_media_tv_Tuner.h b/media/jni/android_media_tv_Tuner.h index 6b1b6b1a9e71..01c998dab8d6 100644 --- a/media/jni/android_media_tv_Tuner.h +++ b/media/jni/android_media_tv_Tuner.h @@ -163,17 +163,19 @@ private: jmethodID mRestartEventInitID; jfieldID mMediaEventFieldContextID; bool mSharedFilter; - void getSectionEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getMediaEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getPesEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getTsRecordEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getMmtpRecordEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getDownloadEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getIpPayloadEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getTemiEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getScramblingStatusEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getIpCidChangeEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); - void getRestartEvent(jobjectArray& arr, const int size, const DemuxFilterEvent& event); + void getSectionEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent& event); + void getMediaEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent& event); + void getPesEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent& event); + void getTsRecordEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent& event); + void getMmtpRecordEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent& event); + void getDownloadEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent& event); + void getIpPayloadEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent& event); + void getTemiEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent& event); + void getScramblingStatusEvent(const jobjectArray& arr, const int size, + const DemuxFilterEvent& event); + void getIpCidChangeEvent(const jobjectArray& arr, const int size, + const DemuxFilterEvent& event); + void getRestartEvent(const jobjectArray& arr, const int size, const DemuxFilterEvent& event); }; struct JTuner; diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index 27666caafac4..b3628fa3e5ce 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -69,7 +69,7 @@ public: int updateTargetWorkDuration(int64_t targetDurationNanos); int reportActualWorkDuration(int64_t actualDurationNanos); - int sendHint(int32_t hint); + int sendHint(SessionHint hint); int setThreads(const int32_t* threadIds, size_t size); int getThreadIds(int32_t* const threadIds, size_t* size); @@ -243,7 +243,7 @@ int APerformanceHintSession::reportActualWorkDuration(int64_t actualDurationNano return 0; } -int APerformanceHintSession::sendHint(int32_t hint) { +int APerformanceHintSession::sendHint(SessionHint hint) { if (hint < 0 || hint >= static_cast<int32_t>(mLastHintSentTimestamp.size())) { ALOGE("%s: invalid session hint %d", __FUNCTION__, hint); return EINVAL; @@ -335,7 +335,7 @@ void APerformanceHint_closeSession(APerformanceHintSession* session) { delete session; } -int APerformanceHint_sendHint(void* session, int32_t hint) { +int APerformanceHint_sendHint(void* session, SessionHint hint) { return reinterpret_cast<APerformanceHintSession*>(session)->sendHint(hint); } diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp index 321a7dddb144..791adfd33fcd 100644 --- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp +++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp @@ -127,7 +127,7 @@ TEST_F(PerformanceHintTest, TestSession) { result = APerformanceHint_reportActualWorkDuration(session, -1L); EXPECT_EQ(EINVAL, result); - int hintId = 2; + SessionHint hintId = SessionHint::CPU_LOAD_RESET; EXPECT_CALL(*iSession, sendHint(Eq(hintId))).Times(Exactly(1)); result = APerformanceHint_sendHint(session, hintId); EXPECT_EQ(0, result); @@ -140,7 +140,7 @@ TEST_F(PerformanceHintTest, TestSession) { result = APerformanceHint_sendHint(session, hintId); EXPECT_EQ(0, result); - result = APerformanceHint_sendHint(session, -1); + result = APerformanceHint_sendHint(session, static_cast<SessionHint>(-1)); EXPECT_EQ(EINVAL, result); EXPECT_CALL(*iSession, close()).Times(Exactly(1)); diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java index b888739016c7..946185a3c420 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.os.Bundle; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.webkit.WebView; @@ -183,7 +184,14 @@ public class SlicePurchaseActivity extends Activity { setContentView(mWebView); // Load the URL - mWebView.loadUrl(mUrl.toString()); + String userData = mIntent.getStringExtra(SlicePurchaseController.EXTRA_USER_DATA); + if (TextUtils.isEmpty(userData)) { + logd("Starting WebView with url: " + mUrl.toString()); + mWebView.loadUrl(mUrl.toString()); + } else { + logd("Starting WebView with url: " + mUrl.toString() + ", userData=" + userData); + mWebView.postUrl(mUrl.toString(), userData.getBytes()); + } } private static void logd(@NonNull String s) { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java index 097e47f58db1..483fb8c451ae 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java @@ -30,6 +30,7 @@ import android.content.Intent; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.res.AssetFileDescriptor; +import android.Manifest; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -178,6 +179,9 @@ public class InstallStaging extends AlertActivity { params.setInstallerPackageName(intent.getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME)); params.setInstallReason(PackageManager.INSTALL_REASON_USER); + // Disable full screen intent usage by for sideloads. + params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT, + PackageInstaller.SessionParams.PERMISSION_STATE_DENIED); if (pfd != null) { try { diff --git a/packages/SettingsLib/ActivityEmbedding/OWNERS b/packages/SettingsLib/ActivityEmbedding/OWNERS index 702240273e43..6f1ab9bc7d2d 100644 --- a/packages/SettingsLib/ActivityEmbedding/OWNERS +++ b/packages/SettingsLib/ActivityEmbedding/OWNERS @@ -1,5 +1,4 @@ # Default reviewers for this and subdirectories. -arcwang@google.com -chiujason@google.com +sunnyshao@google.com # Emergency approvers in case the above are not available diff --git a/packages/SettingsLib/MainSwitchPreference/res/color-night-v34/settingslib_main_switch_text_color.xml b/packages/SettingsLib/MainSwitchPreference/res/color-night-v34/settingslib_main_switch_text_color.xml new file mode 100644 index 000000000000..a7ccfc2e88dd --- /dev/null +++ b/packages/SettingsLib/MainSwitchPreference/res/color-night-v34/settingslib_main_switch_text_color.xml @@ -0,0 +1,23 @@ +<?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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:color="@android:color/system_accent1_100" + android:alpha="?android:attr/disabledAlpha" /> + <item android:color="@android:color/system_accent1_100"/> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/MainSwitchPreference/res/color-v31/settingslib_main_switch_text_color.xml b/packages/SettingsLib/MainSwitchPreference/res/color-v31/settingslib_main_switch_text_color.xml new file mode 100644 index 000000000000..de367244dbde --- /dev/null +++ b/packages/SettingsLib/MainSwitchPreference/res/color-v31/settingslib_main_switch_text_color.xml @@ -0,0 +1,23 @@ +<?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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:color="@android:color/black" + android:alpha="?android:attr/disabledAlpha" /> + <item android:color="@android:color/black"/> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/MainSwitchPreference/res/color-v34/settingslib_main_switch_text_color.xml b/packages/SettingsLib/MainSwitchPreference/res/color-v34/settingslib_main_switch_text_color.xml new file mode 100644 index 000000000000..e33905bf1cbc --- /dev/null +++ b/packages/SettingsLib/MainSwitchPreference/res/color-v34/settingslib_main_switch_text_color.xml @@ -0,0 +1,23 @@ +<?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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" + android:color="@android:color/system_accent1_900" + android:alpha="?android:attr/disabledAlpha" /> + <item android:color="@android:color/system_accent1_900"/> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml b/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml index ad888e538ffe..b8e43fc2e344 100644 --- a/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml +++ b/packages/SettingsLib/MainSwitchPreference/res/values-v31/styles.xml @@ -20,5 +20,6 @@ <style name="MainSwitchText.Settingslib" parent="@android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title.Inverse"> <item name="android:textSize">20sp</item> <item name="android:fontFamily">@string/settingslib_config_headlineFontFamily</item> + <item name="android:textColor">@color/settingslib_main_switch_text_color</item> </style> </resources> diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS index 81340f561bc1..b2b7b61f002c 100644 --- a/packages/SettingsLib/OWNERS +++ b/packages/SettingsLib/OWNERS @@ -1,4 +1,7 @@ # People who can approve changes for submission +cantol@google.com +chiujason@google.com +cipson@google.com dsandler@android.com edgarwang@google.com evanlaird@google.com diff --git a/packages/SettingsLib/SettingsTheme/res/color-night-v34/settingslib_switch_track_outline_color.xml b/packages/SettingsLib/SettingsTheme/res/color-night-v34/settingslib_switch_track_outline_color.xml new file mode 100644 index 000000000000..6fab8313587c --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/color-night-v34/settingslib_switch_track_outline_color.xml @@ -0,0 +1,28 @@ +<?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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Disabled status of thumb --> + <item android:state_enabled="false" + android:color="@android:color/system_neutral2_400" + android:alpha="?android:attr/disabledAlpha" /> + <!-- Toggle off status of thumb --> + <item android:state_checked="false" + android:color="@android:color/system_neutral2_400" /> + <!-- Enabled or toggle on status of thumb --> + <item android:color="@color/settingslib_track_on_color" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_switch_thumb_color.xml b/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_switch_thumb_color.xml index 8ccbb06f3edf..a9c5c03a1387 100644 --- a/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_switch_thumb_color.xml +++ b/packages/SettingsLib/SettingsTheme/res/color-v31/settingslib_switch_thumb_color.xml @@ -17,7 +17,8 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Disabled status of thumb --> <item android:state_enabled="false" - android:color="@color/settingslib_thumb_disabled_color" /> + android:color="@color/settingslib_thumb_disabled_color" + android:alpha="?android:attr/disabledAlpha" /> <!-- Toggle off status of thumb --> <item android:state_checked="false" android:color="@color/settingslib_thumb_off_color" /> diff --git a/packages/SettingsLib/SettingsTheme/res/color-v34/settingslib_switch_thumb_color.xml b/packages/SettingsLib/SettingsTheme/res/color-v34/settingslib_switch_thumb_color.xml new file mode 100644 index 000000000000..2bc4ddf0e1b7 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/color-v34/settingslib_switch_thumb_color.xml @@ -0,0 +1,28 @@ +<?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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Disabled status of thumb --> + <item android:state_enabled="false" + android:color="@color/settingslib_thumb_disabled_color" + android:alpha="?android:attr/disabledAlpha" /> + <!-- Toggle off status of thumb --> + <item android:state_checked="false" + android:color="@color/settingslib_thumb_off_color" /> + <!-- Enabled or toggle on status of thumb --> + <item android:color="@color/settingslib_thumb_on_color" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/color-v34/settingslib_switch_track_outline_color.xml b/packages/SettingsLib/SettingsTheme/res/color-v34/settingslib_switch_track_outline_color.xml new file mode 100644 index 000000000000..8abc1fdda733 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/color-v34/settingslib_switch_track_outline_color.xml @@ -0,0 +1,28 @@ +<?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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Disabled status of thumb --> + <item android:state_enabled="false" + android:color="@android:color/system_neutral2_500" + android:alpha="?android:attr/disabledAlpha" /> + <!-- Toggle off status of thumb --> + <item android:state_checked="false" + android:color="@android:color/system_neutral2_500" /> + <!-- Enabled or toggle on status of thumb --> + <item android:color="@color/settingslib_track_on_color" /> +</selector>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v34/settingslib_switch_track.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v34/settingslib_switch_track.xml new file mode 100644 index 000000000000..851e413e41c0 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v34/settingslib_switch_track.xml @@ -0,0 +1,27 @@ +<?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. +--> + +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle" + android:width="52dp" + android:height="28dp"> + + <solid android:color="@color/settingslib_switch_track_color" /> + <corners android:radius="35dp" /> + <stroke android:width="2dp" android:color="@color/settingslib_track_online_color"/> +</shape>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml new file mode 100644 index 000000000000..e3645b55981b --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml @@ -0,0 +1,40 @@ +<?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. +--> + +<resources> + <!-- Material next state on color--> + <color name="settingslib_state_on_color">@android:color/system_accent1_700</color> + + <!-- Material next state off color--> + <color name="settingslib_state_off_color">@android:color/system_accent1_700</color> + + <!-- Material next thumb disable color--> + <color name="settingslib_thumb_disabled_color">@color/settingslib_thumb_off_color</color> + + <!-- Material next thumb off color--> + <color name="settingslib_thumb_on_color">@android:color/system_accent1_800</color> + + <!-- Material next thumb off color--> + <color name="settingslib_thumb_off_color">@android:color/system_neutral2_400</color> + + <!-- Material next track on color--> + <color name="settingslib_track_on_color">@android:color/system_accent1_200</color> + + <!-- Material next track off color--> + <color name="settingslib_track_off_color">@android:color/system_surface_container_highest_dark + </color> +</resources> diff --git a/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml new file mode 100644 index 000000000000..fdd96ec78efc --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml @@ -0,0 +1,42 @@ +<?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. +--> + +<resources> + <!-- Material next state on color--> + <color name="settingslib_state_on_color">@android:color/system_accent1_100</color> + + <!-- Material next state off color--> + <color name="settingslib_state_off_color">@android:color/system_accent1_100</color> + + <!-- Material next thumb disable color--> + <color name="settingslib_thumb_disabled_color">@color/settingslib_thumb_off_color</color> + + <!-- Material next thumb off color--> + <color name="settingslib_thumb_on_color">@android:color/system_accent1_0</color> + + <!-- Material next thumb off color--> + <color name="settingslib_thumb_off_color">@android:color/system_neutral2_500</color> + + <!-- Material next track on color--> + <color name="settingslib_track_on_color">@android:color/system_accent1_600</color> + + <!-- Material next track off color--> + <color name="settingslib_track_off_color">@android:color/system_surface_container_highest_light</color> + + <!-- Material next track outline color--> + <color name="settingslib_track_online_color">@color/settingslib_switch_track_outline_color</color> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt index f6ca0c4fd5c4..67c4cdcc1290 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt @@ -73,6 +73,7 @@ fun SearchScaffold( actions: @Composable RowScope.() -> Unit = {}, content: @Composable (bottomPadding: Dp, searchQuery: State<String>) -> Unit, ) { + ActivityTitle(title) var isSearchMode by rememberSaveable { mutableStateOf(false) } val viewModel: SearchScaffoldViewModel = viewModel() diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt index c6e13a14063f..79635a0c8280 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt @@ -20,12 +20,20 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.TabRow import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import com.android.settingslib.spa.framework.theme.SettingsDimension import kotlin.math.absoluteValue import kotlinx.coroutines.launch @@ -41,7 +49,7 @@ fun SettingsPager(titles: List<String>, content: @Composable (page: Int) -> Unit Column { val coroutineScope = rememberCoroutineScope() - val pagerState = rememberPagerState() + val pagerState = rememberPageStateFixed() TabRow( selectedTabIndex = pagerState.currentPage, @@ -69,3 +77,41 @@ fun SettingsPager(titles: List<String>, content: @Composable (page: Int) -> Unit } } } + +/** + * Gets the state of [PagerState]. + * + * This is a work around. + * + * TODO: Remove this and replace with rememberPageState() after the Compose Foundation 1.5.0-alpha04 + * updated in the platform. + */ +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun rememberPageStateFixed(): PagerState { + var currentPage by rememberSaveable { mutableStateOf(0) } + var targetPage by rememberSaveable { mutableStateOf(-1) } + val pagerState = rememberPagerState() + val configuration = LocalConfiguration.current + var lastScreenWidthDp by rememberSaveable { mutableStateOf(-1) } + val screenWidthDp = configuration.screenWidthDp + LaunchedEffect(screenWidthDp) { + // Reset pager state to fix an issue after configuration change. + // When we declare android:configChanges in the manifest, the pager state is in a weird + // state after configuration change. + targetPage = currentPage + lastScreenWidthDp = screenWidthDp + } + LaunchedEffect(targetPage) { + if (targetPage != -1) { + pagerState.scrollToPage(targetPage) + targetPage = -1 + } + } + SideEffect { + if (lastScreenWidthDp == screenWidthDp) { + currentPage = pagerState.currentPage + } + } + return pagerState +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt index f4e504a954a2..89194021ecd5 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt @@ -16,6 +16,9 @@ package com.android.settingslib.spa.widget.scaffold +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -25,8 +28,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.compose.horizontalValues import com.android.settingslib.spa.framework.compose.verticalValues @@ -44,6 +49,7 @@ fun SettingsScaffold( actions: @Composable RowScope.() -> Unit = {}, content: @Composable (PaddingValues) -> Unit, ) { + ActivityTitle(title) val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -55,6 +61,25 @@ fun SettingsScaffold( } } +/** + * Sets a title for the activity. + * + * So the TalkBack can read out the correct page title. + */ +@Composable +internal fun ActivityTitle(title: String) { + val context = LocalContext.current + LaunchedEffect(true) { + context.getActivity()?.title = title + } +} + +private fun Context.getActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext.getActivity() + else -> null +} + @Preview @Composable private fun SettingsScaffoldPreview() { diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt index f04240485386..872d957a6a24 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt @@ -16,9 +16,12 @@ package com.android.settingslib.spa.widget.scaffold +import android.app.Activity import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.Text +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText @@ -29,12 +32,22 @@ import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule @RunWith(AndroidJUnit4::class) class SettingsScaffoldTest { @get:Rule val composeTestRule = createComposeRule() + @get:Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + @Mock + private lateinit var activity: Activity + @Test fun settingsScaffold_titleIsDisplayed() { composeTestRule.setContent { @@ -78,7 +91,18 @@ class SettingsScaffoldTest { assertThat(actualPaddingValues.calculateRightPadding(LayoutDirection.Rtl)).isEqualTo(0.dp) } + @Test + fun activityTitle() { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides activity) { + ActivityTitle(title = TITLE) + } + } + + verify(activity).title = TITLE + } + private companion object { - const val TITLE = "title" + const val TITLE = "Title" } } diff --git a/packages/SettingsLib/res/drawable/ic_media_car.xml b/packages/SettingsLib/res/drawable/ic_media_car.xml new file mode 100644 index 000000000000..452e7bb5f9c0 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_media_car.xml @@ -0,0 +1,25 @@ +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M240,760L240,800Q240,817 228.5,828.5Q217,840 200,840L160,840Q143,840 131.5,828.5Q120,817 120,800L120,480L204,240Q210,222 225.5,211Q241,200 260,200L700,200Q719,200 734.5,211Q750,222 756,240L840,480L840,800Q840,817 828.5,828.5Q817,840 800,840L760,840Q743,840 731.5,828.5Q720,817 720,800L720,760L240,760ZM232,400L728,400L686,280L274,280L232,400ZM200,480L200,480L200,680L200,680L200,480ZM300,640Q325,640 342.5,622.5Q360,605 360,580Q360,555 342.5,537.5Q325,520 300,520Q275,520 257.5,537.5Q240,555 240,580Q240,605 257.5,622.5Q275,640 300,640ZM660,640Q685,640 702.5,622.5Q720,605 720,580Q720,555 702.5,537.5Q685,520 660,520Q635,520 617.5,537.5Q600,555 600,580Q600,605 617.5,622.5Q635,640 660,640ZM200,680L760,680L760,480L200,480L200,680Z"/> +</vector> diff --git a/packages/SettingsLib/res/drawable/ic_media_computer.xml b/packages/SettingsLib/res/drawable/ic_media_computer.xml new file mode 100644 index 000000000000..2aa6f8e31915 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_media_computer.xml @@ -0,0 +1,25 @@ +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M80,840Q63,840 51.5,828.5Q40,817 40,800Q40,783 51.5,771.5Q63,760 80,760L880,760Q897,760 908.5,771.5Q920,783 920,800Q920,817 908.5,828.5Q897,840 880,840L80,840ZM160,720Q127,720 103.5,696.5Q80,673 80,640L80,200Q80,167 103.5,143.5Q127,120 160,120L800,120Q833,120 856.5,143.5Q880,167 880,200L880,640Q880,673 856.5,696.5Q833,720 800,720L160,720ZM160,640L800,640Q800,640 800,640Q800,640 800,640L800,200Q800,200 800,200Q800,200 800,200L160,200Q160,200 160,200Q160,200 160,200L160,640Q160,640 160,640Q160,640 160,640ZM160,640Q160,640 160,640Q160,640 160,640L160,200Q160,200 160,200Q160,200 160,200L160,200Q160,200 160,200Q160,200 160,200L160,640Q160,640 160,640Q160,640 160,640L160,640Z"/> +</vector> diff --git a/packages/SettingsLib/res/drawable/ic_media_game_console.xml b/packages/SettingsLib/res/drawable/ic_media_game_console.xml new file mode 100644 index 000000000000..8e422ac30c03 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_media_game_console.xml @@ -0,0 +1,25 @@ +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M189,800Q129,800 86.5,757Q44,714 42,653Q42,644 43,635Q44,626 46,617L130,281Q144,227 187,193.5Q230,160 285,160L675,160Q730,160 773,193.5Q816,227 830,281L914,617Q916,626 917.5,635.5Q919,645 919,654Q919,715 875.5,757.5Q832,800 771,800Q729,800 693,778Q657,756 639,718L611,660Q606,650 596,645Q586,640 575,640L385,640Q374,640 364,645Q354,650 349,660L321,718Q303,756 267,778Q231,800 189,800ZM192,720Q211,720 226.5,710Q242,700 250,683L278,626Q293,595 322,577.5Q351,560 385,560L575,560Q609,560 638,578Q667,596 683,626L711,683Q719,700 734.5,710Q750,720 769,720Q797,720 817,701.5Q837,683 838,655Q838,656 836,636L752,301Q745,274 724,257Q703,240 675,240L285,240Q257,240 235.5,257Q214,274 208,301L124,636Q122,642 122,654Q122,682 142.5,701Q163,720 192,720ZM540,440Q557,440 568.5,428.5Q580,417 580,400Q580,383 568.5,371.5Q557,360 540,360Q523,360 511.5,371.5Q500,383 500,400Q500,417 511.5,428.5Q523,440 540,440ZM620,360Q637,360 648.5,348.5Q660,337 660,320Q660,303 648.5,291.5Q637,280 620,280Q603,280 591.5,291.5Q580,303 580,320Q580,337 591.5,348.5Q603,360 620,360ZM620,520Q637,520 648.5,508.5Q660,497 660,480Q660,463 648.5,451.5Q637,440 620,440Q603,440 591.5,451.5Q580,463 580,480Q580,497 591.5,508.5Q603,520 620,520ZM700,440Q717,440 728.5,428.5Q740,417 740,400Q740,383 728.5,371.5Q717,360 700,360Q683,360 671.5,371.5Q660,383 660,400Q660,417 671.5,428.5Q683,440 700,440ZM340,500Q353,500 361.5,491.5Q370,483 370,470L370,430L410,430Q423,430 431.5,421.5Q440,413 440,400Q440,387 431.5,378.5Q423,370 410,370L370,370L370,330Q370,317 361.5,308.5Q353,300 340,300Q327,300 318.5,308.5Q310,317 310,330L310,370L270,370Q257,370 248.5,378.5Q240,387 240,400Q240,413 248.5,421.5Q257,430 270,430L310,430L310,470Q310,483 318.5,491.5Q327,500 340,500ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Z"/> +</vector> diff --git a/packages/SettingsLib/res/drawable/ic_media_smartwatch.xml b/packages/SettingsLib/res/drawable/ic_media_smartwatch.xml new file mode 100644 index 000000000000..9c734857c2cd --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_media_smartwatch.xml @@ -0,0 +1,25 @@ +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M16,0L8,0l-0.95,5.73C5.19,7.19 4,9.45 4,12s1.19,4.81 3.05,6.27L8,24h8l0.96,-5.73C18.81,16.81 20,14.54 20,12s-1.19,-4.81 -3.04,-6.27L16,0zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z"/> +</vector> diff --git a/packages/SettingsLib/res/drawable/ic_media_tablet.xml b/packages/SettingsLib/res/drawable/ic_media_tablet.xml new file mode 100644 index 000000000000..c773b96dade6 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_media_tablet.xml @@ -0,0 +1,25 @@ +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M200,920Q167,920 143.5,896.5Q120,873 120,840L120,120Q120,87 143.5,63.5Q167,40 200,40L760,40Q793,40 816.5,63.5Q840,87 840,120L840,840Q840,873 816.5,896.5Q793,920 760,920L200,920ZM200,720L200,840Q200,840 200,840Q200,840 200,840L760,840Q760,840 760,840Q760,840 760,840L760,720L200,720ZM400,800L560,800L560,760L400,760L400,800ZM200,640L760,640L760,240L200,240L200,640ZM200,160L760,160L760,120Q760,120 760,120Q760,120 760,120L200,120Q200,120 200,120Q200,120 200,120L200,160ZM200,160L200,120Q200,120 200,120Q200,120 200,120L200,120Q200,120 200,120Q200,120 200,120L200,160L200,160ZM200,720L200,720L200,840Q200,840 200,840Q200,840 200,840L200,840Q200,840 200,840Q200,840 200,840L200,720Z"/> +</vector> diff --git a/packages/SettingsLib/res/drawable/ic_person_add.xml b/packages/SettingsLib/res/drawable/ic_person_add.xml new file mode 100644 index 000000000000..d138c69acb6a --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_person_add.xml @@ -0,0 +1,25 @@ +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?android:attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M18,14V11H15V9H18V6H20V9H23V11H20V14ZM9,12Q7.35,12 6.175,10.825Q5,9.65 5,8Q5,6.35 6.175,5.175Q7.35,4 9,4Q10.65,4 11.825,5.175Q13,6.35 13,8Q13,9.65 11.825,10.825Q10.65,12 9,12ZM1,20V17.2Q1,16.35 1.438,15.637Q1.875,14.925 2.6,14.55Q4.15,13.775 5.75,13.387Q7.35,13 9,13Q10.65,13 12.25,13.387Q13.85,13.775 15.4,14.55Q16.125,14.925 16.562,15.637Q17,16.35 17,17.2V20ZM3,18H15V17.2Q15,16.925 14.863,16.7Q14.725,16.475 14.5,16.35Q13.15,15.675 11.775,15.337Q10.4,15 9,15Q7.6,15 6.225,15.337Q4.85,15.675 3.5,16.35Q3.275,16.475 3.138,16.7Q3,16.925 3,17.2ZM9,10Q9.825,10 10.413,9.412Q11,8.825 11,8Q11,7.175 10.413,6.588Q9.825,6 9,6Q8.175,6 7.588,6.588Q7,7.175 7,8Q7,8.825 7.588,9.412Q8.175,10 9,10ZM9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8Q9,8 9,8ZM9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Q9,18 9,18Z"/> +</vector> diff --git a/packages/SettingsLib/res/layout/dialog_with_icon.xml b/packages/SettingsLib/res/layout/dialog_with_icon.xml index 9081ca5cc1bb..54f8096b87bf 100644 --- a/packages/SettingsLib/res/layout/dialog_with_icon.xml +++ b/packages/SettingsLib/res/layout/dialog_with_icon.xml @@ -13,87 +13,88 @@ ~ 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" - android:gravity="center" - android:padding="@dimen/grant_admin_dialog_padding" - android:paddingBottom="0dp"> - <ImageView - android:id="@+id/dialog_with_icon_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:contentDescription=""/> - <TextView - android:id="@+id/dialog_with_icon_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - style="@style/DialogWithIconTitle" - android:text="@string/user_grant_admin_title" - android:textDirection="locale"/> - <TextView - android:id="@+id/dialog_with_icon_message" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="10dp" - android:gravity="center" - style="@style/TextAppearanceSmall" - android:text="" - android:textDirection="locale"/> + +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/new_user_dialog_id"> + <LinearLayout - android:id="@+id/custom_layout" android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="center" - android:paddingBottom="0dp"> - </LinearLayout> - <LinearLayout - android:id="@+id/button_panel" - android:orientation="horizontal" - android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" - android:paddingBottom="0dp"> - <Button - android:id="@+id/button_cancel" - style="@style/DialogButtonNegative" + android:padding="@dimen/dialog_content_padding"> + <ImageView + android:id="@+id/dialog_with_icon_icon" android:layout_width="wrap_content" - android:buttonCornerRadius="28dp" android:layout_height="wrap_content" - android:visibility="gone"/> - <Space - android:layout_width="0dp" - android:layout_height="1dp" - android:layout_weight="1" > - </Space> - <Button - android:id="@+id/button_back" + android:importantForAccessibility="no"/> + <TextView + android:id="@+id/dialog_with_icon_title" android:layout_width="wrap_content" android:layout_height="wrap_content" - style="@style/DialogButtonNegative" - android:buttonCornerRadius="40dp" - android:clickable="true" - android:focusable="true" - android:text="Back" - android:visibility="gone" - /> - <Space - android:layout_width="0dp" - android:layout_height="1dp" - android:layout_weight="0.05" - > - </Space> - <Button - android:id="@+id/button_ok" + android:gravity="center" + style="@style/DialogWithIconTitle"/> + <TextView + android:id="@+id/dialog_with_icon_message" android:layout_width="wrap_content" android:layout_height="wrap_content" - style="@style/DialogButtonPositive" - android:clickable="true" - android:focusable="true" - android:visibility="gone" - /> + android:padding="10dp" + android:gravity="center" + style="@style/TextAppearanceSmall"/> + + <LinearLayout + android:id="@+id/custom_layout" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"> + </LinearLayout> + + <LinearLayout + android:id="@+id/button_panel" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"> + + <Button + android:id="@+id/button_cancel" + style="@style/DialogButtonNegative" + android:layout_width="wrap_content" + android:buttonCornerRadius="28dp" + android:layout_height="wrap_content" + android:visibility="gone"/> + + <Space + android:layout_width="0dp" + android:layout_height="1dp" + android:layout_weight="1"> + </Space> + + <Button + android:id="@+id/button_back" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/DialogButtonNegative" + android:buttonCornerRadius="40dp" + android:visibility="gone"/> + + <Space + android:layout_width="0dp" + android:layout_height="1dp" + android:layout_weight="0.05"> + </Space> + + <Button + android:id="@+id/button_ok" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/DialogButtonPositive" + android:visibility="gone" + /> + </LinearLayout> </LinearLayout> -</LinearLayout> +</ScrollView> diff --git a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml index 33c2e4855b64..4ffaf1b0c3e4 100644 --- a/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml +++ b/packages/SettingsLib/res/layout/edit_user_info_dialog_content.xml @@ -28,7 +28,8 @@ android:orientation="vertical"> <TextView android:id="@+id/user_info_title" - android:layout_width="wrap_content" + android:gravity="center" + android:layout_width="match_parent" android:layout_height="wrap_content" style="@style/EditUserDialogTitle" android:text="@string/user_info_settings_title" diff --git a/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml b/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml index d6acac2cf495..26d82456973c 100644 --- a/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml +++ b/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml @@ -14,10 +14,14 @@ ~ limitations under the License. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/grant_admin_view" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" - android:padding="@dimen/grant_admin_dialog_padding"> + android:paddingLeft="@dimen/dialog_content_padding" + android:paddingBottom="@dimen/dialog_content_padding" + android:paddingRight="@dimen/dialog_content_padding"> + <RadioGroup android:id="@+id/choose_admin" android:layout_width="match_parent" @@ -25,13 +29,19 @@ android:orientation="vertical"> <RadioButton android:id="@+id/grant_admin_yes" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingTop="@dimen/radio_button_padding" + android:paddingBottom="@dimen/radio_button_padding" + android:paddingStart="@dimen/radio_button_padding_start" android:text="@string/grant_admin"/> <RadioButton android:id="@+id/grant_admin_no" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingTop="@dimen/radio_button_padding" + android:paddingBottom="@dimen/radio_button_padding" + android:paddingStart="@dimen/radio_button_padding_start" android:text="@string/not_grant_admin"/> </RadioGroup> </LinearLayout> diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml index 2372c802168c..91549d73dfdb 100644 --- a/packages/SettingsLib/res/values/dimens.xml +++ b/packages/SettingsLib/res/values/dimens.xml @@ -112,8 +112,9 @@ <dimen name="broadcast_dialog_btn_minHeight">44dp</dimen> <dimen name="broadcast_dialog_margin">16dp</dimen> - <!-- Size of grant admin privileges dialog padding --> - <dimen name="grant_admin_dialog_padding">16dp</dimen> + <dimen name="dialog_content_padding">16dp</dimen> + <dimen name="radio_button_padding">12dp</dimen> + <dimen name="radio_button_padding_start">8dp</dimen> <dimen name="dialog_button_horizontal_padding">16dp</dimen> <dimen name="dialog_button_vertical_padding">8dp</dimen> diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java index 964e4b24d130..00ccea1f93f1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java @@ -36,6 +36,7 @@ import android.os.Build; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.os.UserManager.EnforcingUser; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ForegroundColorSpan; @@ -118,12 +119,13 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { return enforcedAdmin; } - final int restrictionSource = enforcingUsers.get(0).getUserRestrictionSource(); + final EnforcingUser enforcingUser = enforcingUsers.get(0); + final int restrictionSource = enforcingUser.getUserRestrictionSource(); if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) { return null; } - final EnforcedAdmin admin = getProfileOrDeviceOwner(context, userHandle); + final EnforcedAdmin admin = getProfileOrDeviceOwner(context, enforcingUser.getUserHandle()); if (admin != null) { return admin; } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index 4d6dd4b538cc..f5bacb62b6b2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java @@ -250,6 +250,7 @@ public class BluetoothEventManager { } } cachedDevice.onActiveDeviceChanged(isActive, bluetoothProfile); + mDeviceManager.onActiveDeviceChanged(cachedDevice); } for (BluetoothCallback callback : mCallbacks) { callback.onActiveDeviceChanged(activeDevice, bluetoothProfile); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 7b4c86207a2a..d55144eefea9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -318,13 +318,23 @@ public class CachedBluetoothDeviceManager { return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, state); } - if (profileId == BluetoothProfile.CSIP_SET_COORDINATOR) { + if (profileId == BluetoothProfile.HEADSET + || profileId == BluetoothProfile.A2DP + || profileId == BluetoothProfile.LE_AUDIO + || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) { return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, state); } return false; } + /** Handles when the device been set as active/inactive. */ + public synchronized void onActiveDeviceChanged(CachedBluetoothDevice cachedBluetoothDevice) { + if (cachedBluetoothDevice.isHearingAidDevice()) { + mHearingAidDeviceManager.onActiveDeviceChanged(cachedBluetoothDevice); + } + } + public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) { device.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID); CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(device); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index 356bb82a92e0..8269b56c425b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -28,6 +28,7 @@ import androidx.annotation.ChecksSdkIntAtLeast; import com.android.internal.annotations.VisibleForTesting; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -83,14 +84,14 @@ public class CsipDeviceManager { boolean setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice) { final int groupId = newDevice.getGroupId(); if (isValidGroupId(groupId)) { - final CachedBluetoothDevice CsipDevice = getCachedDevice(groupId); - log("setMemberDeviceIfNeeded, main: " + CsipDevice + ", member: " + newDevice); + final CachedBluetoothDevice mainDevice = getCachedDevice(groupId); + log("setMemberDeviceIfNeeded, main: " + mainDevice + ", member: " + newDevice); // Just add one of the coordinated set from a pair in the list that is shown in the UI. // Once there is other devices with the same groupId, to add new device as member // devices. - if (CsipDevice != null) { - CsipDevice.addMemberDevice(newDevice); - newDevice.setName(CsipDevice.getName()); + if (mainDevice != null) { + mainDevice.addMemberDevice(newDevice); + newDevice.setName(mainDevice.getName()); return true; } } @@ -152,14 +153,7 @@ public class CsipDeviceManager { log("onGroupIdChanged: groupId is invalid"); return; } - log("onGroupIdChanged: mCachedDevices list =" + mCachedDevices.toString()); - List<CachedBluetoothDevice> memberDevicesList = getMemberDevicesList(groupId); - CachedBluetoothDevice newMainDevice = - getPreferredMainDeviceWithoutConectionState(groupId, memberDevicesList); - - log("onGroupIdChanged: The mainDevice= " + newMainDevice - + " and the memberDevicesList of groupId= " + groupId + " =" + memberDevicesList); - addMemberDevicesIntoMainDevice(memberDevicesList, newMainDevice); + updateRelationshipOfGroupDevices(groupId); } // @return {@code true}, the event is processed inside the method. It is for updating @@ -168,61 +162,30 @@ public class CsipDeviceManager { boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state) { log("onProfileConnectionStateChangedIfProcessed: " + cachedDevice + ", state: " + state); - switch (state) { - case BluetoothProfile.STATE_CONNECTED: - onGroupIdChanged(cachedDevice.getGroupId()); - CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice); - if (mainDevice != null) { - if (mainDevice.isConnected()) { - // When main device exists and in connected state, receiving member device - // connection. To refresh main device UI - mainDevice.refresh(); - return true; - } else { - // When both LE Audio devices are disconnected, receiving member device - // connection. To switch content and dispatch to notify UI change - mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice); - mainDevice.switchMemberDeviceContent(cachedDevice); - mainDevice.refresh(); - // It is necessary to do remove and add for updating the mapping on - // preference and device - mBtManager.getEventManager().dispatchDeviceAdded(mainDevice); - return true; - } - } - break; - case BluetoothProfile.STATE_DISCONNECTED: - mainDevice = findMainDevice(cachedDevice); - if (mainDevice != null) { - // When main device exists, receiving sub device disconnection - // To update main device UI - mainDevice.refresh(); - return true; - } - final Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice(); - if (memberSet.isEmpty()) { - break; - } - for (CachedBluetoothDevice device : memberSet) { - if (device.isConnected()) { - log("set device: " + device + " as the main device"); - // Main device is disconnected and sub device is connected - // To copy data from sub device to main device - mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); - cachedDevice.switchMemberDeviceContent(device); - cachedDevice.refresh(); - // It is necessary to do remove and add for updating the mapping on - // preference and device - mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice); - return true; - } - } - break; - default: - // Do not handle this state. + if (state != BluetoothProfile.STATE_CONNECTED + && state != BluetoothProfile.STATE_DISCONNECTED) { + return false; } - return false; + return updateRelationshipOfGroupDevices(cachedDevice.getGroupId()); + } + + @VisibleForTesting + boolean updateRelationshipOfGroupDevices(int groupId) { + if (!isValidGroupId(groupId)) { + log("The device is not group."); + return false; + } + log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString()); + + // Get the preferred main device by getPreferredMainDeviceWithoutConectionState + List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId); + CachedBluetoothDevice preferredMainDevice = + getPreferredMainDevice(groupId, groupDevicesList); + log("The preferredMainDevice= " + preferredMainDevice + + " and the groupDevicesList of groupId= " + groupId + + " =" + groupDevicesList); + return addMemberDevicesIntoMainDevice(groupId, preferredMainDevice); } CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { @@ -262,114 +225,153 @@ public class CsipDeviceManager { return false; } - private List<CachedBluetoothDevice> getMemberDevicesList(int groupId) { - return mCachedDevices.stream() - .filter(cacheDevice -> cacheDevice.getGroupId() == groupId) - .collect(Collectors.toList()); + @VisibleForTesting + List<CachedBluetoothDevice> getGroupDevicesFromAllOfDevicesList(int groupId) { + List<CachedBluetoothDevice> groupDevicesList = new ArrayList<>(); + if (!isValidGroupId(groupId)) { + return groupDevicesList; + } + for (CachedBluetoothDevice item : mCachedDevices) { + if (groupId != item.getGroupId()) { + continue; + } + groupDevicesList.add(item); + groupDevicesList.addAll(item.getMemberDevice()); + } + return groupDevicesList; } - private CachedBluetoothDevice getPreferredMainDeviceWithoutConectionState(int groupId, - List<CachedBluetoothDevice> memberDevicesList) { - // First, priority connected lead device from LE profile - // Second, the DUAL mode device which has A2DP/HFP and LE audio - // Last, any one of LE device in the list. - if (memberDevicesList == null || memberDevicesList.isEmpty()) { + @VisibleForTesting + CachedBluetoothDevice getPreferredMainDevice(int groupId, + List<CachedBluetoothDevice> groupDevicesList) { + // How to select the preferred main device? + // 1. The DUAL mode connected device which has A2DP/HFP and LE audio. + // 2. One of connected LE device in the list. Default is the lead device from LE profile. + // 3. If there is no connected device, then reset the relationship. Set the DUAL mode + // deviced as the main device. Otherwise, set any one of the device. + if (groupDevicesList == null || groupDevicesList.isEmpty()) { return null; } + CachedBluetoothDevice dualModeDevice = groupDevicesList.stream() + .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() + .anyMatch(profile -> profile instanceof LeAudioProfile)) + .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() + .anyMatch(profile -> profile instanceof A2dpProfile + || profile instanceof HeadsetProfile)) + .findFirst().orElse(null); + if (dualModeDevice != null && dualModeDevice.isConnected()) { + log("getPreferredMainDevice: The connected DUAL mode device"); + return dualModeDevice; + } + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager(); final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); - final BluetoothDevice mainBluetoothDevice = (leAudioProfile != null && isAtLeastT()) + final BluetoothDevice leAudioLeadDevice = (leAudioProfile != null && isAtLeastT()) ? leAudioProfile.getConnectedGroupLeadDevice(groupId) : null; - if (mainBluetoothDevice != null) { + if (leAudioLeadDevice != null) { log("getPreferredMainDevice: The LeadDevice from LE profile is " - + mainBluetoothDevice.getAnonymizedAddress()); + + leAudioLeadDevice.getAnonymizedAddress()); } - - // 1st - CachedBluetoothDevice newMainDevice = - mainBluetoothDevice != null ? deviceManager.findDevice(mainBluetoothDevice) : null; - if (newMainDevice != null) { - if (newMainDevice.isConnected()) { - log("getPreferredMainDevice: The connected LeadDevice from LE profile"); - return newMainDevice; - } else { - log("getPreferredMainDevice: The LeadDevice is not connect."); - } - } else { + CachedBluetoothDevice leAudioLeadCachedDevice = + leAudioLeadDevice != null ? deviceManager.findDevice(leAudioLeadDevice) : null; + if (leAudioLeadCachedDevice == null) { log("getPreferredMainDevice: The LeadDevice is not in the all of devices list"); + } else if (leAudioLeadCachedDevice.isConnected()) { + log("getPreferredMainDevice: The connected LeadDevice from LE profile"); + return leAudioLeadCachedDevice; } - - // 2nd - newMainDevice = memberDevicesList.stream() - .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() - .anyMatch(profile -> profile instanceof A2dpProfile - || profile instanceof HeadsetProfile)) + CachedBluetoothDevice oneOfConnectedDevices = groupDevicesList.stream() + .filter(cachedDevice -> cachedDevice.isConnected()) .findFirst().orElse(null); - if (newMainDevice != null) { - log("getPreferredMainDevice: The DUAL mode device"); - return newMainDevice; + if (oneOfConnectedDevices != null) { + log("getPreferredMainDevice: One of the connected devices."); + return oneOfConnectedDevices; } + if (dualModeDevice != null) { + log("getPreferredMainDevice: The DUAL mode device."); + return dualModeDevice; + } // last - if (!memberDevicesList.isEmpty()) { - newMainDevice = memberDevicesList.get(0); + if (!groupDevicesList.isEmpty()) { + log("getPreferredMainDevice: One of the group devices."); + return groupDevicesList.get(0); } - return newMainDevice; + return null; } - private void addMemberDevicesIntoMainDevice(List<CachedBluetoothDevice> memberDevicesList, - CachedBluetoothDevice newMainDevice) { - if (newMainDevice == null) { + @VisibleForTesting + boolean addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice) { + boolean hasChanged = false; + if (preferredMainDevice == null) { log("addMemberDevicesIntoMainDevice: No main device. Do nothing."); - return; + return hasChanged; } - if (memberDevicesList.isEmpty()) { - log("addMemberDevicesIntoMainDevice: No member device in list. Do nothing."); - return; - } - CachedBluetoothDevice mainDeviceOfNewMainDevice = findMainDevice(newMainDevice); - boolean isMemberInOtherMainDevice = mainDeviceOfNewMainDevice != null; - if (!memberDevicesList.contains(newMainDevice) && isMemberInOtherMainDevice) { - log("addMemberDevicesIntoMainDevice: The 'new main device' is not in list, and it is " - + "the member at other device. Do switch main and member."); + + // If the current main device is not preferred main device, then set it as new main device. + // Otherwise, do nothing. + BluetoothDevice bluetoothDeviceOfPreferredMainDevice = preferredMainDevice.getDevice(); + CachedBluetoothDevice mainDeviceOfPreferredMainDevice = findMainDevice(preferredMainDevice); + boolean hasPreferredMainDeviceAlreadyBeenMainDevice = + mainDeviceOfPreferredMainDevice == null; + + if (!hasPreferredMainDeviceAlreadyBeenMainDevice) { + // preferredMainDevice has not been the main device. + // switch relationship between the mainDeviceOfPreferredMainDevice and + // PreferredMainDevice + + log("addMemberDevicesIntoMainDevice: The PreferredMainDevice have the mainDevice. " + + "Do switch relationship between the mainDeviceOfPreferredMainDevice and " + + "PreferredMainDevice"); // To switch content and dispatch to notify UI change - mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfNewMainDevice); - mainDeviceOfNewMainDevice.switchMemberDeviceContent(newMainDevice); - mainDeviceOfNewMainDevice.refresh(); + mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfPreferredMainDevice); + mainDeviceOfPreferredMainDevice.switchMemberDeviceContent(preferredMainDevice); + mainDeviceOfPreferredMainDevice.refresh(); // It is necessary to do remove and add for updating the mapping on // preference and device - mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfNewMainDevice); - } else { - log("addMemberDevicesIntoMainDevice: Set new main device"); - for (CachedBluetoothDevice memberDeviceItem : memberDevicesList) { - if (memberDeviceItem.equals(newMainDevice)) { + mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfPreferredMainDevice); + hasChanged = true; + } + + // If the mCachedDevices List at CachedBluetoothDeviceManager has multiple items which are + // the same groupId, then combine them and also keep the preferred main device as main + // device. + List<CachedBluetoothDevice> topLevelOfGroupDevicesList = mCachedDevices.stream() + .filter(device -> device.getGroupId() == groupId) + .collect(Collectors.toList()); + boolean haveMultiMainDevicesInAllOfDevicesList = topLevelOfGroupDevicesList.size() > 1; + // Update the new main of CachedBluetoothDevice, since it may be changed in above step. + final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager(); + preferredMainDevice = deviceManager.findDevice(bluetoothDeviceOfPreferredMainDevice); + if (haveMultiMainDevicesInAllOfDevicesList) { + // put another devices into main device. + for (CachedBluetoothDevice deviceItem : topLevelOfGroupDevicesList) { + if (deviceItem.getDevice() == null || deviceItem.getDevice().equals( + bluetoothDeviceOfPreferredMainDevice)) { continue; } - Set<CachedBluetoothDevice> memberSet = memberDeviceItem.getMemberDevice(); - if (!memberSet.isEmpty()) { - for (CachedBluetoothDevice memberSetItem : memberSet) { - if (!memberSetItem.equals(newMainDevice)) { - newMainDevice.addMemberDevice(memberSetItem); - } + + Set<CachedBluetoothDevice> memberSet = deviceItem.getMemberDevice(); + for (CachedBluetoothDevice memberSetItem : memberSet) { + if (!memberSetItem.equals(preferredMainDevice)) { + preferredMainDevice.addMemberDevice(memberSetItem); } - memberSet.clear(); } - - newMainDevice.addMemberDevice(memberDeviceItem); - mCachedDevices.remove(memberDeviceItem); - mBtManager.getEventManager().dispatchDeviceRemoved(memberDeviceItem); - } - - if (!mCachedDevices.contains(newMainDevice)) { - mCachedDevices.add(newMainDevice); - mBtManager.getEventManager().dispatchDeviceAdded(newMainDevice); + memberSet.clear(); + preferredMainDevice.addMemberDevice(deviceItem); + mCachedDevices.remove(deviceItem); + mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem); + hasChanged = true; } } - log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " - + mCachedDevices); + if (hasChanged) { + log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " + + mCachedDevices); + } + return hasChanged; } private void log(String msg) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index 4354e0c6e952..e5e57824f6ef 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -224,15 +224,9 @@ public class HearingAidDeviceManager { // It is necessary to do remove and add for updating the mapping on // preference and device mBtManager.getEventManager().dispatchDeviceAdded(mainDevice); - // Only need to set first device of a set. AudioDeviceInfo for - // GET_DEVICES_OUTPUTS will not change device. - setAudioRoutingConfig(cachedDevice); } return true; } - // Only need to set first device of a set. AudioDeviceInfo for GET_DEVICES_OUTPUTS - // will not change device. - setAudioRoutingConfig(cachedDevice); break; case BluetoothProfile.STATE_DISCONNECTED: mainDevice = findMainDevice(cachedDevice); @@ -258,13 +252,20 @@ public class HearingAidDeviceManager { return true; } - // Only need to clear when last device of a set get disconnected - clearAudioRoutingConfig(); break; } return false; } + void onActiveDeviceChanged(CachedBluetoothDevice device) { + if (device.isActiveDevice(BluetoothProfile.HEARING_AID) || device.isActiveDevice( + BluetoothProfile.LE_AUDIO)) { + setAudioRoutingConfig(device); + } else { + clearAudioRoutingConfig(); + } + } + private void setAudioRoutingConfig(CachedBluetoothDevice device) { AudioDeviceAttributes hearingDeviceAttributes = mRoutingHelper.getMatchedHearingDeviceAttributes(device); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index 43e3a32c97b6..0e7b79ba297a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -37,6 +37,7 @@ import android.bluetooth.BluetoothPbapClient; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothSap; import android.bluetooth.BluetoothUuid; +import android.bluetooth.BluetoothVolumeControl; import android.content.Context; import android.content.Intent; import android.os.ParcelUuid; @@ -240,8 +241,8 @@ public class LocalBluetoothProfileManager { Log.d(TAG, "Adding local Volume Control profile"); } mVolumeControlProfile = new VolumeControlProfile(mContext, mDeviceManager, this); - // Note: no event handler for VCP, only for being connectable. - mProfileNameMap.put(VolumeControlProfile.NAME, mVolumeControlProfile); + addProfile(mVolumeControlProfile, VolumeControlProfile.NAME, + BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED); } if (mLeAudioProfile == null && supportedList.contains(BluetoothProfile.LE_AUDIO)) { if (DEBUG) { diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java index 2555e2b7eb9c..765566391ef2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java @@ -16,12 +16,11 @@ package com.android.settingslib.fuelgauge; -import static android.os.BatteryManager.BATTERY_HEALTH_OVERHEAT; -import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN; import static android.os.BatteryManager.BATTERY_STATUS_FULL; import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN; -import static android.os.BatteryManager.EXTRA_HEALTH; -import static android.os.BatteryManager.EXTRA_LEVEL; +import static android.os.BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE; +import static android.os.BatteryManager.CHARGING_POLICY_DEFAULT; +import static android.os.BatteryManager.EXTRA_CHARGING_STATUS; import static android.os.BatteryManager.EXTRA_MAX_CHARGING_CURRENT; import static android.os.BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE; import static android.os.BatteryManager.EXTRA_PLUGGED; @@ -51,7 +50,7 @@ public class BatteryStatus { public final int status; public final int level; public final int plugged; - public final int health; + public final int chargingStatus; public final int maxChargingWattage; public final boolean present; public final Optional<Boolean> incompatibleCharger; @@ -62,12 +61,12 @@ public class BatteryStatus { ? null : new BatteryStatus(batteryChangedIntent, incompatibleCharger); } - public BatteryStatus(int status, int level, int plugged, int health, + public BatteryStatus(int status, int level, int plugged, int chargingStatus, int maxChargingWattage, boolean present) { this.status = status; this.level = level; this.plugged = plugged; - this.health = health; + this.chargingStatus = chargingStatus; this.maxChargingWattage = maxChargingWattage; this.present = present; this.incompatibleCharger = Optional.empty(); @@ -86,7 +85,8 @@ public class BatteryStatus { status = batteryChangedIntent.getIntExtra(EXTRA_STATUS, BATTERY_STATUS_UNKNOWN); plugged = batteryChangedIntent.getIntExtra(EXTRA_PLUGGED, 0); level = getBatteryLevel(batteryChangedIntent); - health = batteryChangedIntent.getIntExtra(EXTRA_HEALTH, BATTERY_HEALTH_UNKNOWN); + chargingStatus = batteryChangedIntent.getIntExtra(EXTRA_CHARGING_STATUS, + CHARGING_POLICY_DEFAULT); present = batteryChangedIntent.getBooleanExtra(EXTRA_PRESENT, true); this.incompatibleCharger = incompatibleCharger; @@ -143,9 +143,9 @@ public class BatteryStatus { return level < LOW_BATTERY_THRESHOLD; } - /** Whether battery is overheated. */ - public boolean isOverheated() { - return health == BATTERY_HEALTH_OVERHEAT; + /** Whether battery defender is enabled. */ + public boolean isBatteryDefender() { + return chargingStatus == CHARGING_POLICY_ADAPTIVE_LONGLIFE; } /** Return current chargin speed is fast, slow or normal. */ @@ -163,7 +163,8 @@ public class BatteryStatus { @Override public String toString() { return "BatteryStatus{status=" + status + ",level=" + level + ",plugged=" + plugged - + ",health=" + health + ",maxChargingWattage=" + maxChargingWattage + "}"; + + ",chargingStatus=" + chargingStatus + ",maxChargingWattage=" + maxChargingWattage + + "}"; } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java index c036fdb7982f..632120e77c87 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java @@ -15,10 +15,14 @@ */ package com.android.settingslib.media; -import static android.media.MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK; -import static android.media.MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK; import static android.media.MediaRoute2Info.TYPE_GROUP; +import static android.media.MediaRoute2Info.TYPE_REMOTE_CAR; +import static android.media.MediaRoute2Info.TYPE_REMOTE_COMPUTER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_GAME_CONSOLE; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTWATCH; import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET_DOCKED; import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; import android.content.Context; @@ -31,8 +35,6 @@ import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; -import java.util.List; - /** * InfoMediaDevice extends MediaDevice to represents wifi device. */ @@ -69,13 +71,12 @@ public class InfoMediaDevice extends MediaDevice { @Override public Drawable getIconWithoutBackground() { - return mContext.getDrawable(getDrawableResIdByFeature()); + return mContext.getDrawable(getDrawableResIdByType()); } @VisibleForTesting - // MediaRoute2Info.getType was made public on API 34, but exists since API 30. @SuppressWarnings("NewApi") - int getDrawableResId() { + int getDrawableResIdByType() { int resId; switch (mRouteInfo.getType()) { case TYPE_GROUP: @@ -84,6 +85,24 @@ public class InfoMediaDevice extends MediaDevice { case TYPE_REMOTE_TV: resId = R.drawable.ic_media_display_device; break; + case TYPE_REMOTE_TABLET: + resId = R.drawable.ic_media_tablet; + break; + case TYPE_REMOTE_TABLET_DOCKED: + resId = R.drawable.ic_dock_device; + break; + case TYPE_REMOTE_COMPUTER: + resId = R.drawable.ic_media_computer; + break; + case TYPE_REMOTE_GAME_CONSOLE: + resId = R.drawable.ic_media_game_console; + break; + case TYPE_REMOTE_CAR: + resId = R.drawable.ic_media_car; + break; + case TYPE_REMOTE_SMARTWATCH: + resId = R.drawable.ic_media_smartwatch; + break; case TYPE_REMOTE_SPEAKER: default: resId = R.drawable.ic_media_speaker_device; @@ -92,21 +111,6 @@ public class InfoMediaDevice extends MediaDevice { return resId; } - @VisibleForTesting - int getDrawableResIdByFeature() { - int resId; - final List<String> features = mRouteInfo.getFeatures(); - if (features.contains(FEATURE_REMOTE_GROUP_PLAYBACK)) { - resId = R.drawable.ic_media_group_device; - } else if (features.contains(FEATURE_REMOTE_VIDEO_PLAYBACK)) { - resId = R.drawable.ic_media_display_device; - } else { - resId = R.drawable.ic_media_speaker_device; - } - - return resId; - } - @Override public String getId() { return MediaDeviceUtils.getId(mRouteInfo); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index 82c6f1134637..3fcb7f398185 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -23,7 +23,13 @@ import static android.media.MediaRoute2Info.TYPE_GROUP; import static android.media.MediaRoute2Info.TYPE_HDMI; import static android.media.MediaRoute2Info.TYPE_HEARING_AID; import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_CAR; +import static android.media.MediaRoute2Info.TYPE_REMOTE_COMPUTER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_GAME_CONSOLE; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTWATCH; import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET_DOCKED; import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; import static android.media.MediaRoute2Info.TYPE_UNKNOWN; import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; @@ -531,7 +537,6 @@ public class InfoMediaManager extends MediaManager { @SuppressWarnings("NewApi") @VisibleForTesting void addMediaDevice(MediaRoute2Info route) { - //TODO(b/258141461): Attach flag and disable reason in MediaDevice final int deviceType = route.getType(); MediaDevice mediaDevice = null; switch (deviceType) { @@ -539,7 +544,12 @@ public class InfoMediaManager extends MediaManager { case TYPE_REMOTE_TV: case TYPE_REMOTE_SPEAKER: case TYPE_GROUP: - //TODO(b/148765806): use correct device type once api is ready. + case TYPE_REMOTE_TABLET: + case TYPE_REMOTE_TABLET_DOCKED: + case TYPE_REMOTE_COMPUTER: + case TYPE_REMOTE_GAME_CONSOLE: + case TYPE_REMOTE_CAR: + case TYPE_REMOTE_SMARTWATCH: mediaDevice = new InfoMediaDevice(mContext, mRouterManager, route, mPackageName, mPreferenceItemMap.get(route.getId())); break; diff --git a/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt index d55a027c2374..b386e5e12504 100644 --- a/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt +++ b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt @@ -36,6 +36,9 @@ data class UdfpsOverlayParams( /** Same as [sensorBounds], but in native resolution. */ val nativeSensorBounds = Rect(sensorBounds).apply { scale(1f / scaleFactor) } + /** Same as [overlayBounds], but in native resolution. */ + val nativeOverlayBounds = Rect(overlayBounds).apply { scale(1f / scaleFactor) } + /** See [android.view.DisplayInfo.logicalWidth] */ val logicalDisplayWidth = if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java new file mode 100644 index 000000000000..e61c8f5ab152 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java @@ -0,0 +1,388 @@ +/* + * 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.settingslib.users; + +import android.annotation.IntDef; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.util.UserIcons; +import com.android.settingslib.R; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.drawable.CircleFramedDrawable; +import com.android.settingslib.utils.CustomDialogHelper; +import com.android.settingslib.utils.ThreadUtils; + +import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This class encapsulates a Dialog for editing the user nickname and photo. + */ +public class CreateUserDialogController { + + private static final String KEY_AWAITING_RESULT = "awaiting_result"; + private static final String KEY_CURRENT_STATE = "current_state"; + private static final String KEY_SAVED_PHOTO = "pending_photo"; + private static final String KEY_SAVED_NAME = "saved_name"; + private static final String KEY_IS_ADMIN = "admin_status"; + private static final String KEY_ADD_USER_LONG_MESSAGE_DISPLAYED = + "key_add_user_long_message_displayed"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({EXIT_DIALOG, INITIAL_DIALOG, GRANT_ADMIN_DIALOG, + EDIT_NAME_DIALOG, CREATE_USER_AND_CLOSE}) + public @interface AddUserState {} + + private static final int EXIT_DIALOG = -1; + private static final int INITIAL_DIALOG = 0; + private static final int GRANT_ADMIN_DIALOG = 1; + private static final int EDIT_NAME_DIALOG = 2; + private static final int CREATE_USER_AND_CLOSE = 3; + + private @AddUserState int mCurrentState; + + private CustomDialogHelper mCustomDialogHelper; + + private EditUserPhotoController mEditUserPhotoController; + private Bitmap mSavedPhoto; + private String mSavedName; + private Drawable mSavedDrawable; + private Boolean mIsAdmin; + private Dialog mUserCreationDialog; + private View mGrantAdminView; + private View mEditUserInfoView; + private EditText mUserNameView; + private Activity mActivity; + private ActivityStarter mActivityStarter; + private boolean mWaitingForActivityResult; + private NewUserData mSuccessCallback; + + private final String mFileAuthority; + + public CreateUserDialogController(String fileAuthority) { + mFileAuthority = fileAuthority; + } + + /** + * Resets saved values. + */ + public void clear() { + mUserCreationDialog = null; + mCustomDialogHelper = null; + mEditUserPhotoController = null; + mSavedPhoto = null; + mSavedName = null; + mSavedDrawable = null; + mIsAdmin = null; + mActivity = null; + mActivityStarter = null; + mGrantAdminView = null; + mEditUserInfoView = null; + mUserNameView = null; + mSuccessCallback = null; + mCurrentState = INITIAL_DIALOG; + } + + /** + * Notifies that the containing activity or fragment was reinitialized. + */ + public void onRestoreInstanceState(Bundle savedInstanceState) { + String pendingPhoto = savedInstanceState.getString(KEY_SAVED_PHOTO); + if (pendingPhoto != null) { + ThreadUtils.postOnBackgroundThread(() -> { + mSavedPhoto = EditUserPhotoController.loadNewUserPhotoBitmap( + new File(pendingPhoto)); + }); + } + mCurrentState = savedInstanceState.getInt(KEY_CURRENT_STATE); + if (savedInstanceState.containsKey(KEY_IS_ADMIN)) { + mIsAdmin = savedInstanceState.getBoolean(KEY_IS_ADMIN); + } + mSavedName = savedInstanceState.getString(KEY_SAVED_NAME); + mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false); + } + + /** + * Notifies that the containing activity or fragment is saving its state for later use. + */ + public void onSaveInstanceState(Bundle savedInstanceState) { + if (mUserCreationDialog != null && mEditUserPhotoController != null) { + // Bitmap cannot be stored into bundle because it may exceed parcel limit + // Store it in a temporary file instead + ThreadUtils.postOnBackgroundThread(() -> { + File file = mEditUserPhotoController.saveNewUserPhotoBitmap(); + if (file != null) { + savedInstanceState.putString(KEY_SAVED_PHOTO, file.getPath()); + } + }); + } + if (mIsAdmin != null) { + savedInstanceState.putBoolean(KEY_IS_ADMIN, Boolean.TRUE.equals(mIsAdmin)); + } + savedInstanceState.putString(KEY_SAVED_NAME, mUserNameView.getText().toString().trim()); + savedInstanceState.putInt(KEY_CURRENT_STATE, mCurrentState); + savedInstanceState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult); + } + + /** + * Notifies that an activity has started. + */ + public void startingActivityForResult() { + mWaitingForActivityResult = true; + } + + /** + * Notifies that the result from activity has been received. + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + mWaitingForActivityResult = false; + if (mEditUserPhotoController != null) { + mEditUserPhotoController.onActivityResult(requestCode, resultCode, data); + } + } + + /** + * Creates an add user dialog with option to set the user's name and photo and choose their + * admin status. + */ + public Dialog createDialog(Activity activity, + ActivityStarter activityStarter, boolean isMultipleAdminEnabled, + NewUserData successCallback, Runnable cancelCallback) { + mActivity = activity; + mCustomDialogHelper = new CustomDialogHelper(activity); + mSuccessCallback = successCallback; + mActivityStarter = activityStarter; + addCustomViews(isMultipleAdminEnabled); + mUserCreationDialog = mCustomDialogHelper.getDialog(); + updateLayout(); + mUserCreationDialog.setOnDismissListener(view -> { + cancelCallback.run(); + clear(); + }); + mUserCreationDialog.setCanceledOnTouchOutside(true); + return mUserCreationDialog; + } + + private void addCustomViews(boolean isMultipleAdminEnabled) { + addGrantAdminView(); + addUserInfoEditView(); + mCustomDialogHelper.setPositiveButton(R.string.next, view -> { + mCurrentState++; + if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) { + mCurrentState++; + } + updateLayout(); + }); + mCustomDialogHelper.setNegativeButton(R.string.back, view -> { + mCurrentState--; + if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) { + mCurrentState--; + } + updateLayout(); + }); + return; + } + + private void updateLayout() { + switch (mCurrentState) { + case INITIAL_DIALOG: + mEditUserInfoView.setVisibility(View.GONE); + mGrantAdminView.setVisibility(View.GONE); + final SharedPreferences preferences = mActivity.getPreferences( + Context.MODE_PRIVATE); + final boolean longMessageDisplayed = preferences.getBoolean( + KEY_ADD_USER_LONG_MESSAGE_DISPLAYED, false); + final int messageResId = longMessageDisplayed + ? R.string.user_add_user_message_short + : R.string.user_add_user_message_long; + if (!longMessageDisplayed) { + preferences.edit().putBoolean( + KEY_ADD_USER_LONG_MESSAGE_DISPLAYED, + true).apply(); + } + Drawable icon = mActivity.getDrawable(R.drawable.ic_person_add); + mCustomDialogHelper.setVisibility(mCustomDialogHelper.ICON, true) + .setVisibility(mCustomDialogHelper.TITLE, true) + .setVisibility(mCustomDialogHelper.MESSAGE, true) + .setIcon(icon) + .setButtonEnabled(true) + .setTitle(R.string.user_add_user_title) + .setMessage(messageResId) + .setNegativeButtonText(R.string.cancel) + .setPositiveButtonText(R.string.next); + break; + case GRANT_ADMIN_DIALOG: + mEditUserInfoView.setVisibility(View.GONE); + mGrantAdminView.setVisibility(View.VISIBLE); + mCustomDialogHelper + .setVisibility(mCustomDialogHelper.ICON, true) + .setVisibility(mCustomDialogHelper.TITLE, true) + .setVisibility(mCustomDialogHelper.MESSAGE, true) + .setIcon(mActivity.getDrawable(R.drawable.ic_admin_panel_settings)) + .setTitle(R.string.user_grant_admin_title) + .setMessage(R.string.user_grant_admin_message) + .setNegativeButtonText(R.string.back) + .setPositiveButtonText(R.string.next); + if (mIsAdmin == null) { + mCustomDialogHelper.setButtonEnabled(false); + } + break; + case EDIT_NAME_DIALOG: + mCustomDialogHelper + .setVisibility(mCustomDialogHelper.ICON, false) + .setVisibility(mCustomDialogHelper.TITLE, false) + .setVisibility(mCustomDialogHelper.MESSAGE, false) + .setNegativeButtonText(R.string.back) + .setPositiveButtonText(R.string.done); + mEditUserInfoView.setVisibility(View.VISIBLE); + mGrantAdminView.setVisibility(View.GONE); + break; + case CREATE_USER_AND_CLOSE: + Drawable newUserIcon = mEditUserPhotoController != null + ? mEditUserPhotoController.getNewUserPhotoDrawable() + : null; + + String newName = mUserNameView.getText().toString().trim(); + String defaultName = mActivity.getString(R.string.user_new_user_name); + String userName = !newName.isEmpty() ? newName : defaultName; + + if (mSuccessCallback != null) { + mSuccessCallback.onSuccess(userName, newUserIcon, + Boolean.TRUE.equals(mIsAdmin)); + } + mCustomDialogHelper.getDialog().dismiss(); + clear(); + break; + case EXIT_DIALOG: + mCustomDialogHelper.getDialog().dismiss(); + break; + default: + if (mCurrentState < EXIT_DIALOG) { + mCurrentState = EXIT_DIALOG; + updateLayout(); + } else { + mCurrentState = CREATE_USER_AND_CLOSE; + updateLayout(); + } + break; + } + } + + private Drawable getUserIcon(Drawable defaultUserIcon) { + if (mSavedPhoto != null) { + mSavedDrawable = CircleFramedDrawable.getInstance(mActivity, mSavedPhoto); + return mSavedDrawable; + } + return defaultUserIcon; + } + + private void addUserInfoEditView() { + mEditUserInfoView = View.inflate(mActivity, R.layout.edit_user_info_dialog_content, null); + mCustomDialogHelper.addCustomView(mEditUserInfoView); + setUserName(); + ImageView userPhotoView = mEditUserInfoView.findViewById(R.id.user_photo); + + // if oldUserIcon param is null then we use a default gray user icon + Drawable defaultUserIcon = UserIcons.getDefaultUserIcon( + mActivity.getResources(), UserHandle.USER_NULL, false); + // in case a new photo was selected and the activity got recreated we have to load the image + Drawable userIcon = getUserIcon(defaultUserIcon); + userPhotoView.setImageDrawable(userIcon); + + if (isChangePhotoRestrictedByBase(mActivity)) { + // some users can't change their photos so we need to remove the suggestive icon + mEditUserInfoView.findViewById(R.id.add_a_photo_icon).setVisibility(View.GONE); + } else { + RestrictedLockUtils.EnforcedAdmin adminRestriction = + getChangePhotoAdminRestriction(mActivity); + if (adminRestriction != null) { + userPhotoView.setOnClickListener(view -> + RestrictedLockUtils.sendShowAdminSupportDetailsIntent( + mActivity, adminRestriction)); + } else { + mEditUserPhotoController = createEditUserPhotoController(userPhotoView); + } + } + } + + private void setUserName() { + mUserNameView = mEditUserInfoView.findViewById(R.id.user_name); + if (mSavedName == null) { + mUserNameView.setText(R.string.user_new_user_name); + } else { + mUserNameView.setText(mSavedName); + } + } + + private void addGrantAdminView() { + mGrantAdminView = View.inflate(mActivity, R.layout.grant_admin_dialog_content, null); + mCustomDialogHelper.addCustomView(mGrantAdminView); + RadioGroup radioGroup = mGrantAdminView.findViewById(R.id.choose_admin); + radioGroup.setOnCheckedChangeListener((group, checkedId) -> { + mCustomDialogHelper.setButtonEnabled(true); + mIsAdmin = checkedId == R.id.grant_admin_yes; + } + ); + if (Boolean.TRUE.equals(mIsAdmin)) { + RadioButton button = radioGroup.findViewById(R.id.grant_admin_yes); + button.setChecked(true); + } else if (Boolean.FALSE.equals(mIsAdmin)) { + RadioButton button = radioGroup.findViewById(R.id.grant_admin_no); + button.setChecked(true); + } + } + + @VisibleForTesting + boolean isChangePhotoRestrictedByBase(Context context) { + return RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId()); + } + + @VisibleForTesting + RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) { + return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId()); + } + + @VisibleForTesting + EditUserPhotoController createEditUserPhotoController(ImageView userPhotoView) { + return new EditUserPhotoController(mActivity, mActivityStarter, userPhotoView, + mSavedPhoto, mSavedDrawable, mFileAuthority); + } + + public boolean isActive() { + return mCustomDialogHelper != null && mCustomDialogHelper.getDialog() != null; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java new file mode 100644 index 000000000000..3d18b59258b3 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java @@ -0,0 +1,34 @@ +/* + * 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.settingslib.users; + +import android.graphics.drawable.Drawable; + +/** + * Defines a callback when a new user data is filled out. + */ +public interface NewUserData { + + /** + * Consumes data relevant to new user that needs to be created. + * @param userName New user name. + * @param userImage New user icon. + * @param isNewUserAdmin A boolean that indicated whether new user has admin status. + */ + void onSuccess(String userName, Drawable userImage, Boolean isNewUserAdmin); + +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java index 3361a66e958d..8c316d1c4f21 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java @@ -46,6 +46,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import java.util.ArrayList; +import java.util.Collections; import java.util.List; @RunWith(RobolectricTestRunner.class) @@ -396,6 +397,21 @@ public class BluetoothEventManagerTest { } @Test + public void dispatchActiveDeviceChanged_callExpectedOnActiveDeviceChanged() { + mBluetoothEventManager.registerCallback(mBluetoothCallback); + when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn( + Collections.singletonList(mCachedDevice1)); + + mBluetoothEventManager.dispatchActiveDeviceChanged(mCachedDevice1, + BluetoothProfile.HEARING_AID); + + verify(mCachedDeviceManager).onActiveDeviceChanged(mCachedDevice1); + verify(mBluetoothCallback).onActiveDeviceChanged(mCachedDevice1, + BluetoothProfile.HEARING_AID); + } + + @Test public void showUnbondMessage_reasonAuthTimeout_showCorrectedErrorCode() { mIntent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED); mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java index 4b3820eb0444..7e7c76e6ecba 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java @@ -17,7 +17,9 @@ package com.android.settingslib.bluetooth; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -604,4 +606,20 @@ public class CachedBluetoothDeviceManagerTest { verify(mDevice2).setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); verify(mDevice2).createBond(BluetoothDevice.TRANSPORT_LE); } + + @Test + public void onActiveDeviceChanged_validHiSyncId_callExpectedFunction() { + mHearingAidDeviceManager = spy(new HearingAidDeviceManager(mContext, mLocalBluetoothManager, + mCachedDeviceManager.mCachedDevices)); + doNothing().when(mHearingAidDeviceManager).onActiveDeviceChanged(any()); + mCachedDeviceManager.mHearingAidDeviceManager = mHearingAidDeviceManager; + when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + CachedBluetoothDevice cachedDevice1 = mCachedDeviceManager.addDevice(mDevice1); + cachedDevice1.setHearingAidInfo( + new HearingAidInfo.Builder().setHiSyncId(HISYNCID1).build()); + + mCachedDeviceManager.onActiveDeviceChanged(cachedDevice1); + + verify(mHearingAidDeviceManager).onActiveDeviceChanged(cachedDevice1); + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java new file mode 100644 index 000000000000..9b41bc4f8b00 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -0,0 +1,345 @@ +/* + * 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.settingslib.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothCsipSetCoordinator; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.os.Parcel; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class CsipDeviceManagerTest { + private final static String DEVICE_NAME_1 = "TestName_1"; + private final static String DEVICE_NAME_2 = "TestName_2"; + private final static String DEVICE_NAME_3 = "TestName_3"; + private final static String DEVICE_ALIAS_1 = "TestAlias_1"; + private final static String DEVICE_ALIAS_2 = "TestAlias_2"; + private final static String DEVICE_ALIAS_3 = "TestAlias_3"; + private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; + private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; + private final static String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33"; + private final static int GROUP1 = 1; + private final BluetoothClass DEVICE_CLASS_1 = + createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); + private final BluetoothClass DEVICE_CLASS_2 = + createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE); + + @Mock + private LocalBluetoothManager mLocalBluetoothManager; + @Mock + private LocalBluetoothProfileManager mLocalProfileManager; + @Mock + private BluetoothEventManager mBluetoothEventManager; + @Mock + private BluetoothDevice mDevice1; + @Mock + private BluetoothDevice mDevice2; + @Mock + private BluetoothDevice mDevice3; + @Mock + private HeadsetProfile mHfpProfile; + @Mock + private A2dpProfile mA2dpProfile; + @Mock + private LeAudioProfile mLeAudioProfile; + + private CachedBluetoothDevice mCachedDevice1; + private CachedBluetoothDevice mCachedDevice2; + private CachedBluetoothDevice mCachedDevice3; + private CachedBluetoothDeviceManager mCachedDeviceManager; + private CsipDeviceManager mCsipDeviceManager; + private Context mContext; + private List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>(); + + + private BluetoothClass createBtClass(int deviceClass) { + Parcel p = Parcel.obtain(); + p.writeInt(deviceClass); + p.setDataPosition(0); // reset position of parcel before passing to constructor + + BluetoothClass bluetoothClass = BluetoothClass.CREATOR.createFromParcel(p); + p.recycle(); + return bluetoothClass; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + when(mDevice1.getAddress()).thenReturn(DEVICE_ADDRESS_1); + when(mDevice2.getAddress()).thenReturn(DEVICE_ADDRESS_2); + when(mDevice3.getAddress()).thenReturn(DEVICE_ADDRESS_3); + when(mDevice1.getName()).thenReturn(DEVICE_NAME_1); + when(mDevice2.getName()).thenReturn(DEVICE_NAME_2); + when(mDevice3.getName()).thenReturn(DEVICE_NAME_3); + when(mDevice1.getAlias()).thenReturn(DEVICE_ALIAS_1); + when(mDevice2.getAlias()).thenReturn(DEVICE_ALIAS_2); + when(mDevice3.getAlias()).thenReturn(DEVICE_ALIAS_3); + when(mDevice1.getBluetoothClass()).thenReturn(DEVICE_CLASS_1); + when(mDevice2.getBluetoothClass()).thenReturn(DEVICE_CLASS_2); + when(mDevice3.getBluetoothClass()).thenReturn(DEVICE_CLASS_2); + when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalProfileManager); + when(mLocalProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); + when(mLocalProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); + when(mLocalProfileManager.getHeadsetProfile()).thenReturn(mHfpProfile); + + when(mLeAudioProfile.getConnectedGroupLeadDevice(anyInt())).thenReturn(null); + mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, mLocalBluetoothManager); + when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); + + mCachedDevices = mCachedDeviceManager.mCachedDevices; + mCsipDeviceManager = mCachedDeviceManager.mCsipDeviceManager; + + // Setup the default for testing + mCachedDevice1 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1)); + mCachedDevice2 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2)); + mCachedDevice3 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3)); + + mCachedDevice1.setGroupId(GROUP1); + mCachedDevice2.setGroupId(GROUP1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + mCachedDevices.add(mCachedDevice1); + mCachedDevices.add(mCachedDevice3); + + List<LocalBluetoothProfile> profiles = new ArrayList<LocalBluetoothProfile>(); + profiles.add(mHfpProfile); + profiles.add(mA2dpProfile); + profiles.add(mLeAudioProfile); + when(mCachedDevice1.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice1.isConnected()).thenReturn(true); + + profiles.clear(); + profiles.add(mLeAudioProfile); + when(mCachedDevice2.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice2.isConnected()).thenReturn(true); + + profiles.clear(); + profiles.add(mHfpProfile); + profiles.add(mA2dpProfile); + when(mCachedDevice3.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice3.isConnected()).thenReturn(true); + + } + + @Test + public void onProfileConnectionStateChangedIfProcessed_profileIsConnecting_returnOff() { + assertThat(mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, + BluetoothProfile.STATE_CONNECTING)).isFalse(); + } + + @Test + public void onProfileConnectionStateChangedIfProcessed_profileIsDisconnecting_returnOff() { + assertThat(mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, + BluetoothProfile.STATE_DISCONNECTING)).isFalse(); + } + + @Test + public void updateRelationshipOfGroupDevices_invalidGroupId_returnOff() { + assertThat(mCsipDeviceManager.updateRelationshipOfGroupDevices( + BluetoothCsipSetCoordinator.GROUP_ID_INVALID)).isFalse(); + } + + @Test + public void getGroupDevicesFromAllOfDevicesList_invalidGroupId_returnEmpty() { + assertThat(mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList( + BluetoothCsipSetCoordinator.GROUP_ID_INVALID).isEmpty()).isTrue(); + } + + @Test + public void getGroupDevicesFromAllOfDevicesList_validGroupId_returnGroupDevices() { + List<CachedBluetoothDevice> expectedList = new ArrayList<>(); + expectedList.add(mCachedDevice1); + expectedList.add(mCachedDevice2); + + assertThat(mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1)) + .isEqualTo(expectedList); + } + + @Test + public void getPreferredMainDevice_dualModeDevice_returnDualModeDevice() { + CachedBluetoothDevice expectedDevice = mCachedDevice1; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void getPreferredMainDevice_noConnectedDualModeDevice_returnLeadDevice() { + when(mCachedDevice1.isConnected()).thenReturn(false); + when(mLeAudioProfile.getConnectedGroupLeadDevice(anyInt())).thenReturn(mDevice2); + CachedBluetoothDevice expectedDevice = mCachedDevice2; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void getPreferredMainDevice_noConnectedDualModeDeviceNoLeadDevice_returnConnectedOne() { + when(mCachedDevice1.isConnected()).thenReturn(false); + when(mCachedDevice2.isConnected()).thenReturn(true); + CachedBluetoothDevice expectedDevice = mCachedDevice2; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void getPreferredMainDevice_noConnectedDevice_returnDualModeDevice() { + when(mCachedDevice1.isConnected()).thenReturn(false); + when(mCachedDevice2.isConnected()).thenReturn(false); + CachedBluetoothDevice expectedDevice = mCachedDevice1; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void getPreferredMainDevice_noConnectedDeviceNoDualMode_returnFirstOneDevice() { + when(mCachedDevice1.isConnected()).thenReturn(false); + when(mCachedDevice2.isConnected()).thenReturn(false); + List<LocalBluetoothProfile> profiles = new ArrayList<LocalBluetoothProfile>(); + profiles.add(mLeAudioProfile); + when(mCachedDevice1.getConnectableProfiles()).thenReturn(profiles); + CachedBluetoothDevice expectedDevice = mCachedDevice1; + + assertThat( + mCsipDeviceManager.getPreferredMainDevice(GROUP1, + mCsipDeviceManager.getGroupDevicesFromAllOfDevicesList(GROUP1))) + .isEqualTo(expectedDevice); + } + + @Test + public void addMemberDevicesIntoMainDevice_noPreferredDevice_returnFalseAndNoChangeList() { + CachedBluetoothDevice preferredDevice = null; + List<CachedBluetoothDevice> expectedList = new ArrayList<>(); + for (CachedBluetoothDevice item : mCachedDevices) { + expectedList.add(item); + } + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isFalse(); + for (CachedBluetoothDevice expectedItem : expectedList) { + assertThat(mCachedDevices.contains(expectedItem)).isTrue(); + } + } + + @Test + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndNoOtherInList_noChangeList() + { + // Condition: The preferredDevice is main and no other main device in top list + // Expected Result: return false and the list is no changed + CachedBluetoothDevice preferredDevice = mCachedDevice1; + List<CachedBluetoothDevice> expectedList = new ArrayList<>(); + for (CachedBluetoothDevice item : mCachedDevices) { + expectedList.add(item); + } + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isFalse(); + for (CachedBluetoothDevice expectedItem : expectedList) { + assertThat(mCachedDevices.contains(expectedItem)).isTrue(); + } + } + + @Test + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_returnTrue() { + // Condition: The preferredDevice is main and there is another main device in top list + // Expected Result: return true and there is the preferredDevice in top list + CachedBluetoothDevice preferredDevice = mCachedDevice1; + mCachedDevice1.getMemberDevice().clear(); + mCachedDevices.clear(); + mCachedDevices.add(preferredDevice); + mCachedDevices.add(mCachedDevice2); + mCachedDevices.add(mCachedDevice3); + + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isTrue(); + assertThat(mCachedDevices.contains(preferredDevice)).isTrue(); + assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse(); + assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); + assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2); + } + + @Test + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMember_returnTrue() { + // Condition: The preferredDevice is member + // Expected Result: return true and there is the preferredDevice in top list + CachedBluetoothDevice preferredDevice = mCachedDevice2; + BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isTrue(); + // expected main is mCachedDevice1 which is the main of preferredDevice, since system + // switch the relationship between preferredDevice and the main of preferredDevice + assertThat(mCachedDevices.contains(mCachedDevice1)).isTrue(); + assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse(); + assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue(); + assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice2); + assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); + } + + @Test + public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_returnTrue() { + // Condition: The preferredDevice is member and there are two main device in top list + // Expected Result: return true and there is the preferredDevice in top list + CachedBluetoothDevice preferredDevice = mCachedDevice2; + BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice(); + mCachedDevice3.setGroupId(GROUP1); + + assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) + .isTrue(); + // expected main is mCachedDevice1 which is the main of preferredDevice, since system + // switch the relationship between preferredDevice and the main of preferredDevice + assertThat(mCachedDevices.contains(mCachedDevice1)).isTrue(); + assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse(); + assertThat(mCachedDevices.contains(mCachedDevice3)).isFalse(); + assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice2); + assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice3); + assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice3); + assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index a83913693458..0d5de88cc394 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -478,37 +478,24 @@ public class HearingAidDeviceManagerTest { } @Test - public void onProfileConnectionStateChanged_connected_callSetStrategies() { + public void onActiveDeviceChanged_connected_callSetStrategies() { when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); + when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); - mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, - BluetoothProfile.STATE_CONNECTED); + mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1); verify(mHelper, atLeastOnce()).setPreferredDeviceRoutingStrategies( eq(List.of(mAudioStrategy)), any(AudioDeviceAttributes.class), anyInt()); } @Test - public void onProfileConnectionStateChanged_disconnected_callSetStrategiesWithAutoValue() { + public void onActiveDeviceChanged_disconnected_callSetStrategiesWithAutoValue() { when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); + when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false); - mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, - BluetoothProfile.STATE_DISCONNECTED); - - verify(mHelper, atLeastOnce()).setPreferredDeviceRoutingStrategies( - eq(List.of(mAudioStrategy)), /* hearingDevice= */ isNull(), - eq(HearingAidAudioRoutingConstants.RoutingValue.AUTO)); - } - @Test - public void onProfileConnectionStateChanged_unpairing_callSetStrategiesWithAutoValue() { - when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn( - mHearingDeviceAttribute); - - when(mCachedDevice1.getUnpairing()).thenReturn(true); - mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, - BluetoothProfile.STATE_DISCONNECTED); + mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1); verify(mHelper, atLeastOnce()).setPreferredDeviceRoutingStrategies( eq(List.of(mAudioStrategy)), /* hearingDevice= */ isNull(), diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java index c45b7f333fa1..67a045e9a449 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaDeviceTest.java @@ -16,11 +16,14 @@ package com.android.settingslib.media; -import static android.media.MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK; -import static android.media.MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK; -import static android.media.MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK; import static android.media.MediaRoute2Info.TYPE_GROUP; +import static android.media.MediaRoute2Info.TYPE_REMOTE_CAR; +import static android.media.MediaRoute2Info.TYPE_REMOTE_COMPUTER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_GAME_CONSOLE; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTWATCH; import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET_DOCKED; import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; import static com.google.common.truth.Truth.assertThat; @@ -41,8 +44,6 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import java.util.ArrayList; - @RunWith(RobolectricTestRunner.class) public class InfoMediaDeviceTest { @@ -100,40 +101,47 @@ public class InfoMediaDeviceTest { public void getDrawableResId_returnCorrectResId() { when(mRouteInfo.getType()).thenReturn(TYPE_REMOTE_TV); - assertThat(mInfoMediaDevice.getDrawableResId()).isEqualTo( + assertThat(mInfoMediaDevice.getDrawableResIdByType()).isEqualTo( R.drawable.ic_media_display_device); when(mRouteInfo.getType()).thenReturn(TYPE_REMOTE_SPEAKER); - assertThat(mInfoMediaDevice.getDrawableResId()).isEqualTo( + assertThat(mInfoMediaDevice.getDrawableResIdByType()).isEqualTo( R.drawable.ic_media_speaker_device); when(mRouteInfo.getType()).thenReturn(TYPE_GROUP); - assertThat(mInfoMediaDevice.getDrawableResId()).isEqualTo(R.drawable.ic_media_group_device); - } + assertThat(mInfoMediaDevice.getDrawableResIdByType()).isEqualTo( + R.drawable.ic_media_group_device); - @Test - public void getDrawableResIdByFeature_returnCorrectResId() { - final ArrayList<String> features = new ArrayList<>(); - features.add(FEATURE_REMOTE_VIDEO_PLAYBACK); - when(mRouteInfo.getFeatures()).thenReturn(features); + when(mRouteInfo.getType()).thenReturn(TYPE_REMOTE_TABLET); - assertThat(mInfoMediaDevice.getDrawableResIdByFeature()).isEqualTo( - R.drawable.ic_media_display_device); + assertThat(mInfoMediaDevice.getDrawableResIdByType()).isEqualTo( + R.drawable.ic_media_tablet); - features.clear(); - features.add(FEATURE_REMOTE_AUDIO_PLAYBACK); - when(mRouteInfo.getFeatures()).thenReturn(features); + when(mRouteInfo.getType()).thenReturn(TYPE_REMOTE_TABLET_DOCKED); - assertThat(mInfoMediaDevice.getDrawableResIdByFeature()).isEqualTo( - R.drawable.ic_media_speaker_device); + assertThat(mInfoMediaDevice.getDrawableResIdByType()).isEqualTo( + R.drawable.ic_dock_device); - features.clear(); - features.add(FEATURE_REMOTE_GROUP_PLAYBACK); - when(mRouteInfo.getFeatures()).thenReturn(features); + when(mRouteInfo.getType()).thenReturn(TYPE_REMOTE_COMPUTER); - assertThat(mInfoMediaDevice.getDrawableResIdByFeature()).isEqualTo( - R.drawable.ic_media_group_device); + assertThat(mInfoMediaDevice.getDrawableResIdByType()).isEqualTo( + R.drawable.ic_media_computer); + + when(mRouteInfo.getType()).thenReturn(TYPE_REMOTE_GAME_CONSOLE); + + assertThat(mInfoMediaDevice.getDrawableResIdByType()).isEqualTo( + R.drawable.ic_media_game_console); + + when(mRouteInfo.getType()).thenReturn(TYPE_REMOTE_CAR); + + assertThat(mInfoMediaDevice.getDrawableResIdByType()).isEqualTo( + R.drawable.ic_media_car); + + when(mRouteInfo.getType()).thenReturn(TYPE_REMOTE_SMARTWATCH); + + assertThat(mInfoMediaDevice.getDrawableResIdByType()).isEqualTo( + R.drawable.ic_media_smartwatch); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java new file mode 100644 index 000000000000..e989ed27508b --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java @@ -0,0 +1,264 @@ +/* + * 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.settingslib.users; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.RadioButton; + +import androidx.fragment.app.FragmentActivity; + +import com.android.settingslib.R; +import com.android.settingslib.RestrictedLockUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ActivityController; + +@RunWith(RobolectricTestRunner.class) +public class CreateUserDialogControllerTest { + + @Mock + private ActivityStarter mActivityStarter; + + private boolean mPhotoRestrictedByBase; + private Activity mActivity; + private TestCreateUserDialogController mUnderTest; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mActivity = spy(ActivityController.of(new FragmentActivity()).get()); + mActivity.setTheme(R.style.Theme_AppCompat_DayNight); + mUnderTest = new TestCreateUserDialogController(); + mPhotoRestrictedByBase = false; + } + + @Test + public void positiveButton_grantAdminStage_noValue_OkButtonShouldBeDisabled() { + Runnable cancelCallback = mock(Runnable.class); + + final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, null, + cancelCallback); + dialog.show(); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(false); + ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + dialog.dismiss(); + } + + @Test + public void positiveButton_MultipleAdminDisabled_shouldSkipGrantAdminStage() { + Runnable cancelCallback = mock(Runnable.class); + + final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, false, null, + cancelCallback); + dialog.show(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + Button back = dialog.findViewById(R.id.button_cancel); + back.performClick(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + dialog.dismiss(); + } + + @Test + public void editUserInfoController_shouldOnlyBeVisibleOnLastStage() { + Runnable cancelCallback = mock(Runnable.class); + final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, null, + cancelCallback); + dialog.show(); + assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()).isEqualTo(View.GONE); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true); + assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()).isEqualTo(View.GONE); + next.performClick(); + assertThat(dialog.findViewById(R.id.user_info_scroll).getVisibility()) + .isEqualTo(View.VISIBLE); + dialog.dismiss(); + } + + @Test + public void positiveButton_MultipleAdminEnabled_shouldShowGrantAdminStage() { + Runnable cancelCallback = mock(Runnable.class); + + final AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, null, + cancelCallback); + dialog.show(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + assertThat(dialog.findViewById(R.id.button_ok).isEnabled()).isEqualTo(true); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()) + .isEqualTo(View.VISIBLE); + ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true); + next.performClick(); + assertThat(dialog.findViewById(R.id.grant_admin_view).getVisibility()).isEqualTo(View.GONE); + dialog.dismiss(); + } + + @Test + public void cancelCallback_isCalled_whenCancelled() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, successCallback, + cancelCallback); + dialog.show(); + dialog.cancel(); + verifyNoInteractions(successCallback); + verify(cancelCallback, times(1)) + .run(); + } + + @Test + public void cancelCallback_isCalled_whenNegativeButtonClickedOnFirstStage() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, successCallback, + cancelCallback); + dialog.show(); + Button back = dialog.findViewById(R.id.button_cancel); + back.performClick(); + verifyNoInteractions(successCallback); + verify(cancelCallback, times(1)) + .run(); + } + + @Test + public void cancelCallback_isNotCalled_whenNegativeButtonClickedOnSecondStage() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, successCallback, + cancelCallback); + dialog.show(); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + Button back = dialog.findViewById(R.id.button_cancel); + back.performClick(); + verifyNoInteractions(successCallback); + verifyNoInteractions(cancelCallback); + dialog.dismiss(); + } + + @Test + public void successCallback_isCalled_setNameAndAdminStatus() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, true, successCallback, + cancelCallback); + // No photo chosen + when(mUnderTest.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null); + dialog.show(); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + ((RadioButton) dialog.findViewById(R.id.grant_admin_yes)).setChecked(true); + next.performClick(); + String expectedNewName = "Test"; + EditText editText = dialog.findViewById(R.id.user_name); + editText.setText(expectedNewName); + next.performClick(); + verify(successCallback, times(1)) + .onSuccess(expectedNewName, null, true); + } + + @Test + public void successCallback_isCalled_setName_MultipleAdminDisabled() { + NewUserData successCallback = mock(NewUserData.class); + Runnable cancelCallback = mock(Runnable.class); + + AlertDialog dialog = (AlertDialog) mUnderTest.createDialog(mActivity, + mActivityStarter, false, successCallback, + cancelCallback); + // No photo chosen + when(mUnderTest.getPhotoController().getNewUserPhotoDrawable()).thenReturn(null); + dialog.show(); + Button next = dialog.findViewById(R.id.button_ok); + next.performClick(); + String expectedNewName = "Test"; + EditText editText = dialog.findViewById(R.id.user_name); + editText.setText(expectedNewName); + next.performClick(); + verify(successCallback, times(1)) + .onSuccess(expectedNewName, null, false); + } + + private class TestCreateUserDialogController extends CreateUserDialogController { + private EditUserPhotoController mPhotoController; + + TestCreateUserDialogController() { + super("file_authority"); + } + + private EditUserPhotoController getPhotoController() { + return mPhotoController; + } + + @Override + EditUserPhotoController createEditUserPhotoController(ImageView userPhotoView) { + mPhotoController = mock(EditUserPhotoController.class, Answers.RETURNS_DEEP_STUBS); + return mPhotoController; + } + @Override + RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) { + return null; + } + + @Override + boolean isChangePhotoRestrictedByBase(Context context) { + return mPhotoRestrictedByBase; + } + } +} diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 9d3620eb29a9..ef4b81491ce1 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -116,6 +116,7 @@ public class SettingsBackupTest { Settings.Global.ADD_USERS_WHEN_LOCKED, Settings.Global.AIRPLANE_MODE_ON, Settings.Global.AIRPLANE_MODE_RADIOS, + Settings.Global.ALLOW_WORK_PROFILE_TELEPHONY_FOR_NON_DPM_ROLE_HOLDERS, Settings.Global.SATELLITE_MODE_RADIOS, Settings.Global.SATELLITE_MODE_ENABLED, Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS, diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 8b3fd41b62a6..43f98c38715d 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -837,6 +837,8 @@ <uses-permission android:name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" /> <!-- Permission required for GTS test - GtsCredentialsTestCases --> <uses-permission android:name="android.permission.LAUNCH_CREDENTIAL_SELECTOR" /> + <!-- Permission required for CTS test IntentRedirectionTest --> + <uses-permission android:name="android.permission.QUERY_CLONED_APPS" /> <application android:label="@string/app_label" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt deleted file mode 100644 index 197b217f96eb..000000000000 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteTransitionAdapter.kt +++ /dev/null @@ -1,455 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.animation - -import android.annotation.SuppressLint -import android.app.WindowConfiguration -import android.graphics.Point -import android.graphics.Rect -import android.os.IBinder -import android.os.RemoteException -import android.util.ArrayMap -import android.util.Log -import android.util.RotationUtils -import android.view.IRemoteAnimationFinishedCallback -import android.view.IRemoteAnimationRunner -import android.view.RemoteAnimationAdapter -import android.view.RemoteAnimationTarget -import android.view.SurfaceControl -import android.view.WindowManager -import android.window.IRemoteTransition -import android.window.IRemoteTransitionFinishedCallback -import android.window.RemoteTransition -import android.window.TransitionInfo - -class RemoteTransitionAdapter { - companion object { - /** - * Almost a copy of Transitions#setupStartState. - * - * TODO: remove when there is proper cross-process transaction sync. - */ - @SuppressLint("NewApi") - private fun setupLeash( - leash: SurfaceControl, - change: TransitionInfo.Change, - layer: Int, - info: TransitionInfo, - t: SurfaceControl.Transaction - ) { - val isOpening = - info.type == WindowManager.TRANSIT_OPEN || - info.type == WindowManager.TRANSIT_TO_FRONT - // Put animating stuff above this line and put static stuff below it. - val zSplitLine = info.changes.size - // changes should be ordered top-to-bottom in z - val mode = change.mode - - var rootIdx = info.findRootIndex(change.endDisplayId) - if (rootIdx < 0) rootIdx = 0 - // Launcher animates leaf tasks directly, so always reparent all task leashes to root. - t.reparent(leash, info.getRoot(rootIdx).leash) - t.setPosition( - leash, - (change.startAbsBounds.left - info.getRoot(rootIdx).offset.x).toFloat(), - (change.startAbsBounds.top - info.getRoot(rootIdx).offset.y).toFloat() - ) - t.show(leash) - // Put all the OPEN/SHOW on top - if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) { - if (isOpening) { - t.setLayer(leash, zSplitLine + info.changes.size - layer) - if ( - change.flags and TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT == 0 - ) { - // if transferred, it should be left visible. - t.setAlpha(leash, 0f) - } - } else { - // put on bottom and leave it visible - t.setLayer(leash, zSplitLine - layer) - } - } else if ( - mode == WindowManager.TRANSIT_CLOSE || mode == WindowManager.TRANSIT_TO_BACK - ) { - if (isOpening) { - // put on bottom and leave visible - t.setLayer(leash, zSplitLine - layer) - } else { - // put on top - t.setLayer(leash, zSplitLine + info.changes.size - layer) - } - } else { // CHANGE - t.setLayer(leash, zSplitLine + info.changes.size - layer) - } - } - - @SuppressLint("NewApi") - private fun createLeash( - info: TransitionInfo, - change: TransitionInfo.Change, - order: Int, - t: SurfaceControl.Transaction - ): SurfaceControl { - // TODO: once we can properly sync transactions across process, then get rid of this. - if (change.parent != null && change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) { - // Special case for wallpaper atm. Normally these are left alone; but, a quirk of - // making leashes means we have to handle them specially. - return change.leash - } - val leashSurface = - SurfaceControl.Builder() - .setName(change.leash.toString() + "_transition-leash") - .setContainerLayer() - .setParent( - if (change.parent == null) { - var rootIdx = info.findRootIndex(change.endDisplayId) - if (rootIdx < 0) rootIdx = 0 - info.getRoot(rootIdx).leash - } else info.getChange(change.parent!!)!!.leash - ) - .build() - // Copied Transitions setup code (which expects bottom-to-top order, so we swap here) - setupLeash(leashSurface, change, info.changes.size - order, info, t) - t.reparent(change.leash, leashSurface) - t.setAlpha(change.leash, 1.0f) - t.show(change.leash) - t.setPosition(change.leash, 0f, 0f) - t.setLayer(change.leash, 0) - return leashSurface - } - - private fun newModeToLegacyMode(newMode: Int): Int { - return when (newMode) { - WindowManager.TRANSIT_OPEN, - WindowManager.TRANSIT_TO_FRONT -> RemoteAnimationTarget.MODE_OPENING - WindowManager.TRANSIT_CLOSE, - WindowManager.TRANSIT_TO_BACK -> RemoteAnimationTarget.MODE_CLOSING - else -> RemoteAnimationTarget.MODE_CHANGING - } - } - - private fun rectOffsetTo(rect: Rect, offset: Point): Rect { - val out = Rect(rect) - out.offsetTo(offset.x, offset.y) - return out - } - - fun createTarget( - change: TransitionInfo.Change, - order: Int, - info: TransitionInfo, - t: SurfaceControl.Transaction - ): RemoteAnimationTarget { - val target = - RemoteAnimationTarget( - /* taskId */ if (change.taskInfo != null) change.taskInfo!!.taskId else -1, - /* mode */ newModeToLegacyMode(change.mode), - /* leash */ createLeash(info, change, order, t), - /* isTranslucent */ (change.flags and TransitionInfo.FLAG_TRANSLUCENT != 0 || - change.flags and TransitionInfo.FLAG_SHOW_WALLPAPER != 0), - /* clipRect */ null, - /* contentInsets */ Rect(0, 0, 0, 0), - /* prefixOrderIndex */ order, - /* position */ null, - /* localBounds */ rectOffsetTo(change.endAbsBounds, change.endRelOffset), - /* screenSpaceBounds */ Rect(change.endAbsBounds), - /* windowConfig */ if (change.taskInfo != null) - change.taskInfo!!.configuration.windowConfiguration - else WindowConfiguration(), - /* isNotInRecents */ if (change.taskInfo != null) !change.taskInfo!!.isRunning - else true, - /* startLeash */ null, - /* startBounds */ Rect(change.startAbsBounds), - /* taskInfo */ change.taskInfo, - /* allowEnterPip */ change.allowEnterPip, - /* windowType */ WindowManager.LayoutParams.INVALID_WINDOW_TYPE - ) - target.backgroundColor = change.backgroundColor - return target - } - - /** - * Represents a TransitionInfo object as an array of old-style targets - * - * @param wallpapers If true, this will return wallpaper targets; otherwise it returns - * non-wallpaper targets. - * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should - * be populated by this function. If null, it is ignored. - */ - fun wrapTargets( - info: TransitionInfo, - wallpapers: Boolean, - t: SurfaceControl.Transaction, - leashMap: ArrayMap<SurfaceControl, SurfaceControl>? - ): Array<RemoteAnimationTarget> { - val out = ArrayList<RemoteAnimationTarget>() - for (i in info.changes.indices) { - val change = info.changes[i] - if (change.hasFlags(TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { - // For embedded container, when the parent Task is also in the transition, we - // should only animate the parent Task. - if (change.parent != null) continue - // For embedded container without parent, we should only animate if it fills - // the Task. Otherwise we may animate only partial of the Task. - if (!change.hasFlags(TransitionInfo.FLAG_FILLS_TASK)) continue - } - // Check if it is wallpaper - if (wallpapers != change.hasFlags(TransitionInfo.FLAG_IS_WALLPAPER)) continue - out.add(createTarget(change, info.changes.size - i, info, t)) - if (leashMap != null) { - leashMap[change.leash] = out[out.size - 1].leash - } - } - return out.toTypedArray() - } - - @JvmStatic - fun adaptRemoteRunner(runner: IRemoteAnimationRunner): IRemoteTransition.Stub { - return object : IRemoteTransition.Stub() { - override fun startAnimation( - token: IBinder, - info: TransitionInfo, - t: SurfaceControl.Transaction, - finishCallback: IRemoteTransitionFinishedCallback - ) { - val leashMap = ArrayMap<SurfaceControl, SurfaceControl>() - val appsCompat = wrapTargets(info, false /* wallpapers */, t, leashMap) - val wallpapersCompat = wrapTargets(info, true /* wallpapers */, t, leashMap) - // TODO(bc-unlock): Build wrapped object for non-apps target. - val nonAppsCompat = arrayOfNulls<RemoteAnimationTarget>(0) - - // TODO(b/177438007): Move this set-up logic into launcher's animation impl. - var isReturnToHome = false - var launcherTask: TransitionInfo.Change? = null - var wallpaper: TransitionInfo.Change? = null - var launcherLayer = 0 - var rotateDelta = 0 - var displayW = 0f - var displayH = 0f - for (i in info.changes.indices.reversed()) { - val change = info.changes[i] - if ( - change.taskInfo != null && - change.taskInfo!!.activityType == - WindowConfiguration.ACTIVITY_TYPE_HOME - ) { - isReturnToHome = - (change.mode == WindowManager.TRANSIT_OPEN || - change.mode == WindowManager.TRANSIT_TO_FRONT) - launcherTask = change - launcherLayer = info.changes.size - i - } else if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) { - wallpaper = change - } - if ( - change.parent == null && - change.endRotation >= 0 && - change.endRotation != change.startRotation - ) { - rotateDelta = change.endRotation - change.startRotation - displayW = change.endAbsBounds.width().toFloat() - displayH = change.endAbsBounds.height().toFloat() - } - } - - // Prepare for rotation if there is one - val counterLauncher = CounterRotator() - val counterWallpaper = CounterRotator() - if (launcherTask != null && rotateDelta != 0 && launcherTask.parent != null) { - counterLauncher.setup( - t, - info.getChange(launcherTask.parent!!)!!.leash, - rotateDelta, - displayW, - displayH - ) - if (counterLauncher.surface != null) { - t.setLayer(counterLauncher.surface!!, launcherLayer) - } - } - if (isReturnToHome) { - if (counterLauncher.surface != null) { - t.setLayer(counterLauncher.surface!!, info.changes.size * 3) - } - // Need to "boost" the closing things since that's what launcher expects. - for (i in info.changes.indices.reversed()) { - val change = info.changes[i] - val leash = leashMap[change.leash] - val mode = info.changes[i].mode - // Only deal with independent layers - if (!TransitionInfo.isIndependent(change, info)) continue - if ( - mode == WindowManager.TRANSIT_CLOSE || - mode == WindowManager.TRANSIT_TO_BACK - ) { - t.setLayer(leash!!, info.changes.size * 3 - i) - counterLauncher.addChild(t, leash) - } - } - // Make wallpaper visible immediately since sysui apparently won't do this. - for (i in wallpapersCompat.indices.reversed()) { - t.show(wallpapersCompat[i].leash) - t.setAlpha(wallpapersCompat[i].leash, 1f) - } - } else { - if (launcherTask != null) { - counterLauncher.addChild(t, leashMap[launcherTask.leash]) - } - if (wallpaper != null && rotateDelta != 0 && wallpaper.parent != null) { - counterWallpaper.setup( - t, - info.getChange(wallpaper.parent!!)!!.leash, - rotateDelta, - displayW, - displayH - ) - if (counterWallpaper.surface != null) { - t.setLayer(counterWallpaper.surface!!, -1) - counterWallpaper.addChild(t, leashMap[wallpaper.leash]) - } - } - } - t.apply() - val animationFinishedCallback = - object : IRemoteAnimationFinishedCallback { - override fun onAnimationFinished() { - val finishTransaction = SurfaceControl.Transaction() - counterLauncher.cleanUp(finishTransaction) - counterWallpaper.cleanUp(finishTransaction) - // Release surface references now. This is apparently to free GPU - // memory while doing quick operations (eg. during CTS). - info.releaseAllSurfaces() - for (i in leashMap.size - 1 downTo 0) { - leashMap.valueAt(i).release() - } - try { - finishCallback.onTransitionFinished( - null /* wct */, - finishTransaction - ) - finishTransaction.close() - } catch (e: RemoteException) { - Log.e( - "ActivityOptionsCompat", - "Failed to call app controlled" + - " animation finished callback", - e - ) - } - } - - override fun asBinder(): IBinder? { - return null - } - } - // TODO(bc-unlcok): Pass correct transit type. - runner.onAnimationStart( - WindowManager.TRANSIT_OLD_NONE, - appsCompat, - wallpapersCompat, - nonAppsCompat, - animationFinishedCallback - ) - } - - override fun mergeAnimation( - token: IBinder, - info: TransitionInfo, - t: SurfaceControl.Transaction, - mergeTarget: IBinder, - finishCallback: IRemoteTransitionFinishedCallback - ) { - // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, - // ignore any incoming merges. - // Clean up stuff though cuz GC takes too long for benchmark tests. - t.close() - info.releaseAllSurfaces() - } - } - } - - @JvmStatic - fun adaptRemoteAnimation( - adapter: RemoteAnimationAdapter, - debugName: String - ): RemoteTransition { - return RemoteTransition( - adaptRemoteRunner(adapter.runner), - adapter.callingApplication, - debugName - ) - } - } - - /** Utility class that takes care of counter-rotating surfaces during a transition animation. */ - class CounterRotator { - /** Gets the surface with the counter-rotation. */ - var surface: SurfaceControl? = null - private set - - /** - * Sets up this rotator. - * - * @param rotateDelta is the forward rotation change (the rotation the display is making). - * @param parentW (and H) Is the size of the rotating parent. - */ - fun setup( - t: SurfaceControl.Transaction, - parent: SurfaceControl, - rotateDelta: Int, - parentW: Float, - parentH: Float - ) { - if (rotateDelta == 0) return - val surface = - SurfaceControl.Builder() - .setName("Transition Unrotate") - .setContainerLayer() - .setParent(parent) - .build() - // Rotate forward to match the new rotation (rotateDelta is the forward rotation the - // parent already took). Child surfaces will be in the old rotation relative to the new - // parent rotation, so we need to forward-rotate the child surfaces to match. - RotationUtils.rotateSurface(t, surface, rotateDelta) - val tmpPt = Point(0, 0) - // parentW/H are the size in the END rotation, the rotation utilities expect the - // starting size. So swap them if necessary - val flipped = rotateDelta % 2 != 0 - val pw = if (flipped) parentH else parentW - val ph = if (flipped) parentW else parentH - RotationUtils.rotatePoint(tmpPt, rotateDelta, pw.toInt(), ph.toInt()) - t.setPosition(surface, tmpPt.x.toFloat(), tmpPt.y.toFloat()) - t.show(surface) - } - - /** Adds a surface that needs to be counter-rotate. */ - fun addChild(t: SurfaceControl.Transaction, child: SurfaceControl?) { - if (surface == null) return - t.reparent(child!!, surface) - } - - /** - * Clean-up. Since finishTransaction should reset all change leashes, we only need to remove - * the counter rotation surface. - */ - fun cleanUp(finishTransaction: SurfaceControl.Transaction) { - if (surface == null) return - finishTransaction.remove(surface!!) - } - } -} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt new file mode 100644 index 000000000000..5224c51bb7c3 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.grid + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.constrainWidth +import androidx.compose.ui.unit.dp +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.roundToInt + +/** + * Renders a grid with [columns] columns. + * + * Child composables will be arranged row by row. + * + * Each column is spaced from the columns to its left and right by [horizontalSpacing]. Each cell + * inside a column is spaced from the cells above and below it with [verticalSpacing]. + */ +@Composable +fun VerticalGrid( + columns: Int, + modifier: Modifier = Modifier, + verticalSpacing: Dp = 0.dp, + horizontalSpacing: Dp = 0.dp, + content: @Composable () -> Unit, +) { + Grid( + primarySpaces = columns, + isVertical = true, + modifier = modifier, + verticalSpacing = verticalSpacing, + horizontalSpacing = horizontalSpacing, + content = content, + ) +} + +/** + * Renders a grid with [rows] rows. + * + * Child composables will be arranged column by column. + * + * Each column is spaced from the columns to its left and right by [horizontalSpacing]. Each cell + * inside a column is spaced from the cells above and below it with [verticalSpacing]. + */ +@Composable +fun HorizontalGrid( + rows: Int, + modifier: Modifier = Modifier, + verticalSpacing: Dp = 0.dp, + horizontalSpacing: Dp = 0.dp, + content: @Composable () -> Unit, +) { + Grid( + primarySpaces = rows, + isVertical = false, + modifier = modifier, + verticalSpacing = verticalSpacing, + horizontalSpacing = horizontalSpacing, + content = content, + ) +} + +@Composable +private fun Grid( + primarySpaces: Int, + isVertical: Boolean, + modifier: Modifier = Modifier, + verticalSpacing: Dp, + horizontalSpacing: Dp, + content: @Composable () -> Unit, +) { + check(primarySpaces > 0) { + "Must provide a positive number of ${if (isVertical) "columns" else "rows"}" + } + + val sizeCache = remember { + object { + var rowHeights = intArrayOf() + var columnWidths = intArrayOf() + } + } + + Layout( + modifier = modifier, + content = content, + ) { measurables, constraints -> + val cells = measurables.size + val columns: Int + val rows: Int + if (isVertical) { + columns = primarySpaces + rows = ceil(cells.toFloat() / primarySpaces).toInt() + } else { + columns = ceil(cells.toFloat() / primarySpaces).toInt() + rows = primarySpaces + } + + if (sizeCache.rowHeights.size != rows) { + sizeCache.rowHeights = IntArray(rows) { 0 } + } + if (sizeCache.columnWidths.size != columns) { + sizeCache.columnWidths = IntArray(columns) { 0 } + } + + val totalHorizontalSpacingBetweenChildren = + ((columns - 1) * horizontalSpacing.toPx()).roundToInt() + val totalVerticalSpacingBetweenChildren = ((rows - 1) * verticalSpacing.toPx()).roundToInt() + val childConstraints = + Constraints().apply { + if (constraints.maxWidth != Constraints.Infinity) { + constrainWidth( + (constraints.maxWidth - totalHorizontalSpacingBetweenChildren) / columns + ) + } + if (constraints.maxHeight != Constraints.Infinity) { + constrainWidth( + (constraints.maxHeight - totalVerticalSpacingBetweenChildren) / rows + ) + } + } + + val placeables = buildList { + for (cellIndex in measurables.indices) { + val column: Int + val row: Int + if (isVertical) { + column = cellIndex % columns + row = cellIndex / columns + } else { + column = cellIndex / rows + row = cellIndex % rows + } + + val placeable = measurables[cellIndex].measure(childConstraints) + sizeCache.rowHeights[row] = max(sizeCache.rowHeights[row], placeable.height) + sizeCache.columnWidths[column] = + max(sizeCache.columnWidths[column], placeable.width) + add(placeable) + } + } + + var totalWidth = totalHorizontalSpacingBetweenChildren + for (column in sizeCache.columnWidths.indices) { + totalWidth += sizeCache.columnWidths[column] + } + + var totalHeight = totalVerticalSpacingBetweenChildren + for (row in sizeCache.rowHeights.indices) { + totalHeight += sizeCache.rowHeights[row] + } + + layout(totalWidth, totalHeight) { + var y = 0 + repeat(rows) { row -> + var x = 0 + var maxChildHeight = 0 + repeat(columns) { column -> + val cellIndex = row * columns + column + if (cellIndex < cells) { + val placeable = placeables[cellIndex] + placeable.placeRelative(x, y) + x += placeable.width + horizontalSpacing.roundToPx() + maxChildHeight = max(maxChildHeight, placeable.height) + } + } + y += maxChildHeight + verticalSpacing.roundToPx() + } + } + } +} diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/shared/page/SceneModule.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/shared/page/SceneModule.kt new file mode 100644 index 000000000000..18c9513acf2a --- /dev/null +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/scene/shared/page/SceneModule.kt @@ -0,0 +1,26 @@ +/* + * 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.scene.shared.page + +import com.android.systemui.scene.shared.model.Scene +import dagger.Module +import dagger.multibindings.Multibinds + +@Module +interface SceneModule { + @Multibinds fun scenes(): Set<Scene> +} diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt new file mode 100644 index 000000000000..530706e47dcc --- /dev/null +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ui/composable/SceneModule.kt @@ -0,0 +1,45 @@ +/* + * 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.scene.ui.composable + +import com.android.systemui.bouncer.ui.composable.BouncerScene +import com.android.systemui.keyguard.ui.composable.LockScreenScene +import com.android.systemui.qs.ui.composable.QuickSettingsScene +import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.shade.ui.composable.ShadeScene +import dagger.Module +import dagger.Provides + +@Module +object SceneModule { + @Provides + fun scenes( + bouncer: BouncerScene, + gone: GoneScene, + lockScreen: LockScreenScene, + qs: QuickSettingsScene, + shade: ShadeScene, + ): Set<Scene> { + return setOf( + bouncer, + gone, + lockScreen, + qs, + shade, + ) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt new file mode 100644 index 000000000000..6f6d0f97b542 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -0,0 +1,135 @@ +/* + * 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.bouncer.ui.composable + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.composable.ComposableScene +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** The bouncer scene displays authentication challenges like PIN, password, or pattern. */ +@SysUISingleton +class BouncerScene +@Inject +constructor( + private val viewModel: BouncerViewModel, +) : ComposableScene { + override val key = SceneKey.Bouncer + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + MutableStateFlow<Map<UserAction, SceneModel>>( + mapOf( + UserAction.Back to SceneModel(SceneKey.Lockscreen), + ) + ) + .asStateFlow() + + @Composable override fun Content(modifier: Modifier) = BouncerScene(viewModel, modifier) +} + +@Composable +private fun BouncerScene( + viewModel: BouncerViewModel, + modifier: Modifier = Modifier, +) { + val message: String by viewModel.message.collectAsState() + val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState() + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(60.dp), + modifier = + modifier.background(MaterialTheme.colorScheme.surface).fillMaxSize().padding(32.dp) + ) { + Crossfade( + targetState = message, + label = "Bouncer message", + ) { + Text( + text = it, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) + } + + Box(Modifier.weight(1f)) { + when (val nonNullViewModel = authMethodViewModel) { + is PinBouncerViewModel -> + PinBouncer( + viewModel = nonNullViewModel, + modifier = Modifier.align(Alignment.Center), + ) + is PasswordBouncerViewModel -> + PasswordBouncer( + viewModel = nonNullViewModel, + modifier = Modifier.align(Alignment.Center), + ) + is PatternBouncerViewModel -> + PatternBouncer( + viewModel = nonNullViewModel, + modifier = + Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false) + .align(Alignment.BottomCenter), + ) + else -> Unit + } + } + + Button( + onClick = viewModel::onEmergencyServicesButtonClicked, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + ), + ) { + Text( + text = stringResource(com.android.internal.R.string.lockscreen_emergency_call), + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt new file mode 100644 index 000000000000..4e85621e9e23 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.ui.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel + +/** UI for the input part of a password-requiring version of the bouncer. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun PasswordBouncer( + viewModel: PasswordBouncerViewModel, + modifier: Modifier = Modifier, +) { + val focusRequester = remember { FocusRequester() } + val password: String by viewModel.password.collectAsState() + + LaunchedEffect(Unit) { + // When the UI comes up, request focus on the TextField to bring up the software keyboard. + focusRequester.requestFocus() + // Also, report that the UI is shown to let the view-model runs some logic. + viewModel.onShown() + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + ) { + val color = MaterialTheme.colorScheme.onSurfaceVariant + val lineWidthPx = with(LocalDensity.current) { 2.dp.toPx() } + + TextField( + value = password, + onValueChange = viewModel::onPasswordInputChanged, + visualTransformation = PasswordVisualTransformation(), + singleLine = true, + textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = + KeyboardActions( + onDone = { viewModel.onAuthenticateKeyPressed() }, + ), + modifier = + Modifier.focusRequester(focusRequester).drawBehind { + drawLine( + color = color, + start = Offset(x = 0f, y = size.height - lineWidthPx), + end = Offset(size.width, y = size.height - lineWidthPx), + strokeWidth = lineWidthPx, + ) + }, + ) + + Spacer(Modifier.height(100.dp)) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt new file mode 100644 index 000000000000..383c748f5dd8 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt @@ -0,0 +1,281 @@ +/* + * 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.bouncer.ui.composable + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.sqrt +import kotlinx.coroutines.launch + +/** + * UI for the input part of a pattern-requiring version of the bouncer. + * + * The user can press, hold, and drag their pointer to select dots along a grid of dots. + */ +@Composable +internal fun PatternBouncer( + viewModel: PatternBouncerViewModel, + modifier: Modifier = Modifier, +) { + // Report that the UI is shown to let the view-model run some logic. + LaunchedEffect(Unit) { viewModel.onShown() } + + val colCount = viewModel.columnCount + val rowCount = viewModel.rowCount + + val dotColor = MaterialTheme.colorScheme.secondary + val dotRadius = with(LocalDensity.current) { 8.dp.toPx() } + val lineColor = MaterialTheme.colorScheme.primary + val lineStrokeWidth = dotRadius * 2 + with(LocalDensity.current) { 4.dp.toPx() } + + var containerSize: IntSize by remember { mutableStateOf(IntSize(0, 0)) } + val horizontalSpacing = containerSize.width / colCount + val verticalSpacing = containerSize.height / rowCount + val spacing = min(horizontalSpacing, verticalSpacing).toFloat() + val verticalOffset = containerSize.height - spacing * rowCount + + // All dots that should be rendered on the grid. + val dots: List<PatternDotViewModel> by viewModel.dots.collectAsState() + // The most recently selected dot, if the user is currently dragging. + val currentDot: PatternDotViewModel? by viewModel.currentDot.collectAsState() + // The dots selected so far, if the user is currently dragging. + val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState() + + // Map of animatables for the scale of each dot, keyed by dot. + val scales = remember(dots) { dots.associateWith { Animatable(1f) } } + // Map of animatables for the lines that connect between selected dots, keyed by the destination + // dot of the line. + val lines = remember(dots) { dots.associateWith { Animatable(1f) } } + + val scope = rememberCoroutineScope() + + // When the current dot is changed, we need to update our animations. + LaunchedEffect(currentDot) { + // Make sure that the current dot is scaled up while the other dots are scaled back down. + scales.entries.forEach { (dot, animatable) -> + val isSelected = dot == currentDot + launch { + animatable.animateTo(if (isSelected) 2f else 1f) + if (isSelected) { + animatable.animateTo(1f) + } + } + } + + // Make sure that all dot-connecting lines are decaying, if they're not already animating. + selectedDots.forEach { + lines[it]?.let { line -> + if (!line.isRunning) { + scope.launch { + line.animateTo( + targetValue = 0f, + animationSpec = tween(durationMillis = 500), + ) + } + } + } + } + } + + // This is the position of the input pointer. + var inputPosition: Offset? by remember { mutableStateOf(null) } + + Canvas( + modifier + // Need to clip to bounds to make sure that the lines don't follow the input pointer + // when it leaves the bounds of the dot grid. + .clipToBounds() + .onSizeChanged { containerSize = it } + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { start -> + inputPosition = start + viewModel.onDragStart() + }, + onDragEnd = { + inputPosition = null + lines.values.forEach { animatable -> + scope.launch { animatable.animateTo(1f) } + } + viewModel.onDragEnd() + }, + ) { change, _ -> + inputPosition = change.position + viewModel.onDrag( + xPx = change.position.x, + yPx = change.position.y, + containerSizePx = containerSize.width, + verticalOffsetPx = verticalOffset, + ) + } + } + ) { + // Draw lines between dots. + selectedDots.forEachIndexed { index, dot -> + if (index > 0) { + val previousDot = selectedDots[index - 1] + drawLine( + from = previousDot, + to = dot, + alpha = { distance -> lineAlpha(spacing, distance) }, + spacing = spacing, + verticalOffset = verticalOffset, + lineColor = lineColor, + lineStrokeWidth = lineStrokeWidth, + ) + } + } + + // Draw the line between the most recently-selected dot and the input pointer position. + inputPosition?.let { lineEnd -> + currentDot?.let { dot -> + drawLine( + from = dot, + to = lineEnd, + alpha = { distance -> lineAlpha(spacing, distance) }, + spacing = spacing, + verticalOffset = verticalOffset, + lineColor = lineColor, + lineStrokeWidth = lineStrokeWidth, + ) + } + } + + // Draw each dot on the grid. + dots.forEach { dot -> + drawDot( + dot = dot, + scaleFactor = { scales[dot]?.value ?: 1f }, + spacing = spacing, + verticalOffset = verticalOffset, + dotColor = dotColor, + dotRadius = dotRadius, + ) + } + } +} + +/** Draws the given [dot]. */ +private fun DrawScope.drawDot( + dot: PatternDotViewModel, + scaleFactor: () -> Float, + spacing: Float, + verticalOffset: Float, + dotColor: Color, + dotRadius: Float, +) { + drawCircle( + color = dotColor, + radius = dotRadius * scaleFactor.invoke(), + center = pixelOffset(dot, spacing, verticalOffset), + ) +} + +/** Draws a line from the [from] origin dot to the [to] destination dot. */ +private fun DrawScope.drawLine( + from: PatternDotViewModel, + to: PatternDotViewModel, + alpha: (distance: Float) -> Float, + spacing: Float, + verticalOffset: Float, + lineColor: Color, + lineStrokeWidth: Float, +) { + drawLine( + from = from, + to = pixelOffset(to, spacing, verticalOffset), + alpha = alpha, + spacing = spacing, + verticalOffset = verticalOffset, + lineColor = lineColor, + lineStrokeWidth = lineStrokeWidth, + ) +} + +/** Draws a line from the [from] origin dot to the [to] destination. */ +private fun DrawScope.drawLine( + from: PatternDotViewModel, + to: Offset, + alpha: (distance: Float) -> Float, + spacing: Float, + verticalOffset: Float, + lineColor: Color, + lineStrokeWidth: Float, +) { + val fromAsOffset = pixelOffset(from, spacing, verticalOffset) + val distance = sqrt((to.y - fromAsOffset.y).pow(2) + (to.x - fromAsOffset.x).pow(2)) + + drawLine( + color = lineColor, + start = fromAsOffset, + end = to, + strokeWidth = lineStrokeWidth, + cap = StrokeCap.Round, + alpha = alpha.invoke(distance), + ) +} + +/** Returns an [Offset] representation of the given [dot] in pixel coordinates. */ +private fun pixelOffset( + dot: PatternDotViewModel, + spacing: Float, + verticalOffset: Float, +): Offset { + return Offset( + x = dot.x * spacing + spacing / 2, + y = dot.y * spacing + spacing / 2 + verticalOffset, + ) +} + +/** + * Returns the alpha for a line between dots where dots are [spacing] apart from each other on the + * dot grid and the line ends [distance] away from the origin dot. + * + * The reason [distance] can be different from [spacing] is that all lines originate in dots but one + * line might end where the user input pointer is, which isn't always a dot position. + */ +private fun lineAlpha(spacing: Float, distance: Float): Float { + // Custom curve for the alpha of a line as a function of its distance from its source dot. The + // farther the user input pointer goes from the line, the more opaque the line gets. + return ((distance / spacing - 0.3f) * 4f).coerceIn(0f, 1f) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt new file mode 100644 index 000000000000..9c210c225ab3 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt @@ -0,0 +1,247 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalAnimationApi::class) + +package com.android.systemui.bouncer.ui.composable + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android.compose.grid.VerticalGrid +import com.android.systemui.R +import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.ui.compose.Icon +import kotlin.math.max + +@Composable +internal fun PinBouncer( + viewModel: PinBouncerViewModel, + modifier: Modifier = Modifier, +) { + // Report that the UI is shown to let the view-model run some logic. + LaunchedEffect(Unit) { viewModel.onShown() } + + // The length of the PIN input received so far, so we know how many dots to render. + val pinLength: Pair<Int, Int> by viewModel.pinLengths.collectAsState() + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.heightIn(min = 16.dp).animateContentSize(), + ) { + // TODO(b/281871687): add support for dot shapes. + val (previousPinLength, currentPinLength) = pinLength + val dotCount = max(previousPinLength, currentPinLength) + 1 + repeat(dotCount) { index -> + AnimatedVisibility( + visible = index < currentPinLength, + enter = fadeIn() + scaleIn() + slideInHorizontally(), + exit = fadeOut() + scaleOut() + slideOutHorizontally(), + ) { + Box( + modifier = + Modifier.size(16.dp) + .background( + MaterialTheme.colorScheme.onSurfaceVariant, + CircleShape, + ) + ) + } + } + } + + Spacer(Modifier.height(100.dp)) + + VerticalGrid( + columns = 3, + verticalSpacing = 12.dp, + horizontalSpacing = 20.dp, + ) { + repeat(9) { index -> + val digit = index + 1 + PinButton( + onClicked = { viewModel.onPinButtonClicked(digit) }, + ) { contentColor -> + PinDigit(digit, contentColor) + } + } + + PinButton( + onClicked = { viewModel.onBackspaceButtonClicked() }, + onLongPressed = { viewModel.onBackspaceButtonLongPressed() }, + isHighlighted = true, + ) { contentColor -> + PinIcon( + Icon.Resource( + res = R.drawable.ic_backspace_24dp, + contentDescription = + ContentDescription.Resource(R.string.keyboardview_keycode_delete), + ), + contentColor, + ) + } + + PinButton( + onClicked = { viewModel.onPinButtonClicked(0) }, + ) { contentColor -> + PinDigit(0, contentColor) + } + + PinButton( + onClicked = { viewModel.onAuthenticateButtonClicked() }, + isHighlighted = true, + ) { contentColor -> + PinIcon( + Icon.Resource( + res = R.drawable.ic_keyboard_tab_36dp, + contentDescription = + ContentDescription.Resource(R.string.keyboardview_keycode_enter), + ), + contentColor, + ) + } + } + } +} + +@Composable +private fun PinDigit( + digit: Int, + contentColor: Color, +) { + // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes it + // into Text, use that here, to animate more efficiently. + Text( + text = digit.toString(), + style = MaterialTheme.typography.headlineLarge, + color = contentColor, + ) +} + +@Composable +private fun PinIcon( + icon: Icon, + contentColor: Color, +) { + Icon( + icon = icon, + tint = contentColor, + ) +} + +@Composable +private fun PinButton( + onClicked: () -> Unit, + modifier: Modifier = Modifier, + onLongPressed: (() -> Unit)? = null, + isHighlighted: Boolean = false, + content: @Composable (contentColor: Color) -> Unit, +) { + var isPressed: Boolean by remember { mutableStateOf(false) } + val cornerRadius: Dp by + animateDpAsState( + if (isPressed) 24.dp else PinButtonSize / 2, + label = "PinButton round corners", + ) + val containerColor: Color by + animateColorAsState( + when { + isPressed -> MaterialTheme.colorScheme.primaryContainer + isHighlighted -> MaterialTheme.colorScheme.secondaryContainer + else -> MaterialTheme.colorScheme.surface + }, + label = "Pin button container color", + ) + val contentColor: Color by + animateColorAsState( + when { + isPressed -> MaterialTheme.colorScheme.onPrimaryContainer + isHighlighted -> MaterialTheme.colorScheme.onSecondaryContainer + else -> MaterialTheme.colorScheme.onSurface + }, + label = "Pin button container color", + ) + + Box( + contentAlignment = Alignment.Center, + modifier = + modifier + .size(PinButtonSize) + .drawBehind { + drawRoundRect( + color = containerColor, + cornerRadius = CornerRadius(cornerRadius.toPx()), + ) + } + .pointerInput(Unit) { + detectTapGestures( + onPress = { + isPressed = true + tryAwaitRelease() + isPressed = false + }, + onTap = { onClicked() }, + onLongPress = onLongPressed?.let { { onLongPressed() } }, + ) + }, + ) { + content(contentColor) + } +} + +private val PinButtonSize = 84.dp 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 new file mode 100644 index 000000000000..ab7bc26d59e1 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.composable.ComposableScene +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** The lock screen scene shows when the device is locked. */ +@SysUISingleton +class LockscreenScene +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val viewModel: LockscreenSceneViewModel, +) : ComposableScene { + override val key = SceneKey.Lockscreen + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + viewModel.upDestinationSceneKey + .map { pageKey -> destinationScenes(up = pageKey) } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value) + ) + + @Composable + override fun Content( + modifier: Modifier, + ) { + LockscreenScene( + viewModel = viewModel, + modifier = modifier, + ) + } + + private fun destinationScenes( + up: SceneKey, + ): Map<UserAction, SceneModel> { + return mapOf( + UserAction.Swipe(Direction.UP) to SceneModel(up), + UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.Shade) + ) + } +} + +@Composable +private fun LockscreenScene( + viewModel: LockscreenSceneViewModel, + modifier: Modifier = Modifier, +) { + // TODO(b/280879610): implement the real UI. + + val lockButtonIcon: Icon by viewModel.lockButtonIcon.collectAsState() + + Box(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center) + ) { + Text("Lockscreen", style = MaterialTheme.typography.headlineMedium) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button(onClick = { viewModel.onLockButtonClicked() }) { Icon(lockButtonIcon) } + + Button(onClick = { viewModel.onContentClicked() }) { Text("Open some content") } + } + } + } +} 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 new file mode 100644 index 000000000000..130395a38512 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.composable.ComposableScene +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** The Quick Settings (AKA "QS") scene shows the quick setting tiles. */ +@SysUISingleton +class QuickSettingsScene +@Inject +constructor( + private val viewModel: QuickSettingsSceneViewModel, +) : ComposableScene { + override val key = SceneKey.QuickSettings + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + MutableStateFlow<Map<UserAction, SceneModel>>( + mapOf( + UserAction.Swipe(Direction.UP) to SceneModel(SceneKey.Shade), + ) + ) + .asStateFlow() + + @Composable + override fun Content( + modifier: Modifier, + ) { + QuickSettingsScene( + viewModel = viewModel, + modifier = modifier, + ) + } +} + +@Composable +private fun QuickSettingsScene( + viewModel: QuickSettingsSceneViewModel, + modifier: Modifier = Modifier, +) { + // TODO(b/280887232): implement the real UI. + + Box(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center) + ) { + Text("Quick settings", style = MaterialTheme.typography.headlineMedium) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button(onClick = { viewModel.onContentClicked() }) { Text("Open some content") } + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt new file mode 100644 index 000000000000..a21366695f66 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt @@ -0,0 +1,26 @@ +/* + * 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.scene.ui.composable + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.android.systemui.scene.shared.model.Scene + +/** Compose-capable extension of [Scene]. */ +interface ComposableScene : Scene { + @Composable fun Content(modifier: Modifier) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt new file mode 100644 index 000000000000..007055221691 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -0,0 +1,69 @@ +/* + * 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.scene.ui.composable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * "Gone" is not a real scene but rather the absence of scenes when we want to skip showing any + * content from the scene framework. + */ +@SysUISingleton +class GoneScene @Inject constructor() : ComposableScene { + override val key = SceneKey.Gone + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + MutableStateFlow<Map<UserAction, SceneModel>>( + mapOf( + UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.Shade), + ) + ) + .asStateFlow() + + @Composable + override fun Content( + modifier: Modifier, + ) { + /* + * TODO(b/279501596): once we start testing with the real Content Dynamics Framework, + * replace this with an error to make sure it doesn't get rendered. + */ + Box(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center) + ) { + Text("Gone", style = MaterialTheme.typography.headlineMedium) + } + } + } +} 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 new file mode 100644 index 000000000000..f8a73d5294bc --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -0,0 +1,141 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalAnimationApi::class) + +package com.android.systemui.scene.ui.composable + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import java.util.Locale + +/** + * Renders a container of a collection of "scenes" that the user can switch between using certain + * user actions (for instance, swiping up and down) or that can be switched automatically based on + * application business logic in response to certain events (for example, the device unlocking). + * + * It's possible for the application to host several such scene containers, the configuration system + * allows configuring each container with its own set of scenes. Scenes can be present in multiple + * containers. + * + * @param viewModel The UI state holder for this container. + * @param sceneByKey Mapping of [ComposableScene] by [SceneKey], ordered by z-order such that the + * last scene is rendered on top of all other scenes. It's critical that this map contains exactly + * and only the scenes on this container. In other words: (a) there should be no scene in this map + * that is not in the configuration for this container and (b) all scenes in the configuration + * must have entries in this map. + * @param modifier A modifier. + */ +@Composable +fun SceneContainer( + viewModel: SceneContainerViewModel, + sceneByKey: Map<SceneKey, ComposableScene>, + modifier: Modifier = Modifier, +) { + val currentScene: SceneModel by viewModel.currentScene.collectAsState() + + AnimatedContent( + targetState = currentScene.key, + label = "scene container", + modifier = modifier, + ) { currentSceneKey -> + sceneByKey.forEach { (key, composableScene) -> + if (key == currentSceneKey) { + Scene( + scene = composableScene, + onSceneChanged = viewModel::setCurrentScene, + modifier = Modifier.fillMaxSize(), + ) + } + } + } +} + +/** Renders the given [ComposableScene]. */ +@Composable +private fun Scene( + scene: ComposableScene, + onSceneChanged: (SceneModel) -> Unit, + modifier: Modifier = Modifier, +) { + // TODO(b/280880714): replace with the real UI and make sure to call onTransitionProgress. + Box(modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center), + ) { + scene.Content( + modifier = Modifier, + ) + + val destinationScenes: Map<UserAction, SceneModel> by + scene.destinationScenes().collectAsState() + val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)] + val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)] + val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)] + val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)] + val backDestinationScene = destinationScenes[UserAction.Back] + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + DirectionalButton(Direction.LEFT, swipeLeftDestinationScene, onSceneChanged) + DirectionalButton(Direction.UP, swipeUpDestinationScene, onSceneChanged) + DirectionalButton(Direction.RIGHT, swipeRightDestinationScene, onSceneChanged) + DirectionalButton(Direction.DOWN, swipeDownDestinationScene, onSceneChanged) + } + + if (backDestinationScene != null) { + BackHandler { onSceneChanged.invoke(backDestinationScene) } + } + } + } +} + +@Composable +private fun DirectionalButton( + direction: Direction, + destinationScene: SceneModel?, + onSceneChanged: (SceneModel) -> Unit, + modifier: Modifier = Modifier, +) { + Button( + onClick = { destinationScene?.let { onSceneChanged.invoke(it) } }, + enabled = destinationScene != null, + modifier = modifier, + ) { + Text(direction.name.lowercase(Locale.getDefault())) + } +} 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 new file mode 100644 index 000000000000..5a092041df93 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -0,0 +1,108 @@ +/* + * 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.shade.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.scene.shared.model.Direction +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.UserAction +import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */ +@SysUISingleton +class ShadeScene +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val viewModel: ShadeSceneViewModel, +) : ComposableScene { + override val key = SceneKey.Shade + + override fun destinationScenes(): StateFlow<Map<UserAction, SceneModel>> = + viewModel.upDestinationSceneKey + .map { sceneKey -> destinationScenes(up = sceneKey) } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value), + ) + + @Composable + override fun Content( + modifier: Modifier, + ) { + ShadeScene( + viewModel = viewModel, + modifier = modifier, + ) + } + + private fun destinationScenes( + up: SceneKey, + ): Map<UserAction, SceneModel> { + return mapOf( + UserAction.Swipe(Direction.UP) to SceneModel(up), + UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.QuickSettings), + ) + } +} + +@Composable +private fun ShadeScene( + viewModel: ShadeSceneViewModel, + modifier: Modifier = Modifier, +) { + // TODO(b/280887022): implement the real UI. + + Box(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.align(Alignment.Center) + ) { + Text("Shade", style = MaterialTheme.typography.headlineMedium) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = { viewModel.onContentClicked() }, + ) { + Text("Open some content") + } + } + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index aa1bb3f640ee..7c76281e4bdd 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -404,14 +404,12 @@ open class ClockRegistry( } scope.launch(bgDispatcher) { - Log.i(TAG, "verifyLoadedProviders: ${availableClocks.size}") if (keepAllLoaded) { // Enforce that all plugins are loaded if requested for ((_, info) in availableClocks) { info.manager?.loadPlugin() } isVerifying.set(false) - Log.i(TAG, "verifyLoadedProviders: keepAllLoaded=true, load all") return@launch } @@ -422,21 +420,16 @@ open class ClockRegistry( info.manager?.unloadPlugin() } isVerifying.set(false) - Log.i(TAG, "verifyLoadedProviders: currentClock unavailable, unload all") return@launch } val currentManager = currentClock.manager currentManager?.loadPlugin() - Log.i(TAG, "verifyLoadedProviders: load ${currentClock.metadata.clockId}") for ((_, info) in availableClocks) { val manager = info.manager if (manager != null && manager.isLoaded && currentManager != manager) { - Log.i(TAG, "verifyLoadedProviders: unload ${info.metadata.clockId}") manager.unloadPlugin() - } else { - Log.i(TAG, "verifyLoadedProviders: skip unload of ${info.metadata.clockId}") } } isVerifying.set(false) diff --git a/packages/SystemUI/docs/device-entry/keyguard.md b/packages/SystemUI/docs/device-entry/keyguard.md index 8634c950f96c..1898b97ee1ca 100644 --- a/packages/SystemUI/docs/device-entry/keyguard.md +++ b/packages/SystemUI/docs/device-entry/keyguard.md @@ -20,6 +20,10 @@ Begins with the device in low power mode, with the display active for [AOD][3] o An indication to power off the device most likely comes from one of two signals: the user presses the power button or the screen timeout has passed. This may [lock the device](#How-the-device-locks) +#### Long-pressing on keyguard + +OEMs may choose to enable a long-press action that displays a button at the bottom of lockscreen. This button links to lockscreen customization. This can be achieved by overriding the `long_press_keyguard_customize_lockscreen_enabled` resource in `packages/SystemUI/res/values/config.xml`. + #### On Lockscreen #### On Lockscreen, occluded by an activity diff --git a/packages/SystemUI/docs/device-entry/quickaffordance.md b/packages/SystemUI/docs/device-entry/quickaffordance.md index d662649ac419..afcf846ba2e5 100644 --- a/packages/SystemUI/docs/device-entry/quickaffordance.md +++ b/packages/SystemUI/docs/device-entry/quickaffordance.md @@ -17,7 +17,9 @@ Tests belong in the `packages/SystemUI/tests/src/com/android/systemui/keyguard/d By default, AOSP ships with a "bottom right" and a "bottom left" slot, each with a slot capacity of `1`, allowing only one Quick Affordance on each side of the lock screen. ### Customizing Slots -OEMs may choose to override the IDs and number of slots and/or override the default capacities. This can be achieved by overridding the `config_keyguardQuickAffordanceSlots` resource in `packages/SystemUI/res/values/config.xml`. +OEMs may choose to enable customization of slots. An entry point in settings will appear when overriding the `custom_lockscreen_shortcuts_enabled` resource in `packages/SystemUI/res/values/config.xml`. + +OEMs may also choose to override the IDs and number of slots and/or override the default capacities. This can be achieved by overridding the `config_keyguardQuickAffordanceSlots` resource in `packages/SystemUI/res/values/config.xml`. ### Default Quick Affordances OEMs may also choose to predefine default Quick Affordances for each slot. To achieve this, a developer may override the `config_keyguardQuickAffordanceDefaults` resource in `packages/SystemUI/res/values/config.xml`. Note that defaults only work until the user of the device selects a different quick affordance for that slot, even if they select the "None" option. diff --git a/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt index af1a11f6597a..2007e7606ab8 100644 --- a/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt +++ b/packages/SystemUI/log/src/com/android/systemui/log/LogBuffer.kt @@ -271,11 +271,13 @@ constructor( } private fun echoToSystrace(message: LogMessage, strMessage: String) { - Trace.instantForTrack( - Trace.TRACE_TAG_APP, - "UI Events", - "$name - ${message.level.shortString} ${message.tag}: $strMessage" - ) + if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) { + Trace.instantForTrack( + Trace.TRACE_TAG_APP, + "UI Events", + "$name - ${message.level.shortString} ${message.tag}: $strMessage" + ) + } } private fun echoToLogcat(message: LogMessage, strMessage: String) { diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java index 33c7c117dc95..2baeaf67df93 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java @@ -61,7 +61,6 @@ public interface ActivityStarter { */ void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, int flags); void startActivity(Intent intent, boolean dismissShade); - default void startActivity(Intent intent, boolean dismissShade, @Nullable ActivityLaunchAnimator.Controller animationController) { startActivity(intent, dismissShade, animationController, @@ -105,6 +104,11 @@ public interface ActivityStarter { /** Starts an activity and dismisses keyguard. */ void startActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned, + boolean dismissShade); + + /** Starts an activity and dismisses keyguard. */ + void startActivityDismissingKeyguard(Intent intent, + boolean onlyProvisioned, boolean dismissShade, boolean disallowEnterPictureInPictureWhileLaunching, Callback callback, diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt new file mode 100644 index 000000000000..50b3f78a49bc --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/TableLogBufferBase.kt @@ -0,0 +1,58 @@ +/* + * 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.plugins.log + +/** + * Base interface for a logger that logs changes in table format. + * + * This is a plugin interface for classes outside of SystemUI core. + */ +interface TableLogBufferBase { + /** + * Logs a String? change. + * + * For Java overloading. + */ + fun logChange(prefix: String, columnName: String, value: String?) { + logChange(prefix, columnName, value, isInitial = false) + } + + /** Logs a String? change. */ + fun logChange(prefix: String, columnName: String, value: String?, isInitial: Boolean) + + /** + * Logs a Boolean change. + * + * For Java overloading. + */ + fun logChange(prefix: String, columnName: String, value: Boolean) { + logChange(prefix, columnName, value, isInitial = false) + } + + /** Logs a Boolean change. */ + fun logChange(prefix: String, columnName: String, value: Boolean, isInitial: Boolean) + + /** + * Logs an Int? change. + * + * For Java overloading. + */ + fun logChange(prefix: String, columnName: String, value: Int?) { + logChange(prefix, columnName, value, isInitial = false) + } + + /** Logs an Int? change. */ + fun logChange(prefix: String, columnName: String, value: Int?, isInitial: Boolean) +} diff --git a/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml b/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml new file mode 100644 index 000000000000..f7dac13888c0 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml @@ -0,0 +1,35 @@ +<?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 + --> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + + <com.android.keyguard.BouncerKeyguardMessageArea + android:id="@+id/bouncer_primary_message_area" + style="@style/Keyguard.Bouncer.PrimaryMessage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/keyguard_lock_padding" + android:focusable="true" + /> + + <com.android.keyguard.BouncerKeyguardMessageArea + android:id="@+id/bouncer_secondary_message_area" + style="@style/Keyguard.Bouncer.SecondaryMessage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/secondary_message_padding" + android:focusable="true" /> + +</merge> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml index 4b9470728dc6..3fc0965f4a81 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -25,7 +25,7 @@ android:layout_height="wrap_content" android:clipChildren="false" android:layout_gravity="center_horizontal|top"> - <FrameLayout + <com.android.keyguard.KeyguardClockFrame android:id="@+id/lockscreen_clock_view" android:layout_width="wrap_content" android:layout_height="@dimen/small_clock_height" @@ -34,7 +34,7 @@ android:clipChildren="false" android:paddingStart="@dimen/clock_padding_start" android:visibility="invisible" /> - <FrameLayout + <com.android.keyguard.KeyguardClockFrame android:id="@+id/lockscreen_clock_view_large" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_emergency_carrier_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_emergency_carrier_area.xml index 8bb78775e727..371670cba1ba 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_emergency_carrier_area.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_emergency_carrier_area.xml @@ -39,7 +39,8 @@ android:ellipsize="marquee" android:visibility="gone" android:gravity="center" - androidprv:allCaps="@bool/kg_use_all_caps" /> + androidprv:allCaps="@bool/kg_use_all_caps" + androidprv:debugLocation="Emergency" /> <com.android.keyguard.EmergencyButton android:id="@+id/emergency_call_button" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml index 6ec65cebf840..3412a303c2a6 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml @@ -29,6 +29,13 @@ > <include layout="@layout/keyguard_bouncer_message_area"/> + <com.android.systemui.keyguard.bouncer.ui.BouncerMessageView + android:id="@+id/bouncer_message_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + /> + <Space android:layout_width="match_parent" android:layout_height="0dp" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml index c772c9649cc7..78a7c171ab14 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml @@ -33,6 +33,12 @@ android:clipToPadding="false"> <include layout="@layout/keyguard_bouncer_message_area"/> + <com.android.systemui.keyguard.bouncer.ui.BouncerMessageView + android:id="@+id/bouncer_message_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" /> + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/pattern_container" android:layout_width="match_parent" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml index f61df05eb8ca..330676b3dd5f 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml @@ -29,6 +29,12 @@ androidprv:layout_maxWidth="@dimen/keyguard_security_width"> <include layout="@layout/keyguard_bouncer_message_area"/> +<com.android.systemui.keyguard.bouncer.ui.BouncerMessageView + android:id="@+id/bouncer_message_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" /> + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/pin_container" android:layout_width="match_parent" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml index 647abee9a99b..557fbf21d5c4 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml @@ -30,7 +30,7 @@ android:clipChildren="false" android:layout_width="0dp" android:layout_height="wrap_content"> - <LinearLayout + <com.android.keyguard.KeyguardStatusContainer android:id="@+id/status_view_container" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -48,5 +48,5 @@ android:layout_height="wrap_content" android:padding="@dimen/qs_media_padding" /> - </LinearLayout> + </com.android.keyguard.KeyguardStatusContainer> </com.android.keyguard.KeyguardStatusView> diff --git a/packages/SystemUI/res-keyguard/values/config.xml b/packages/SystemUI/res-keyguard/values/config.xml index a25ab5109fa8..d50355140bed 100644 --- a/packages/SystemUI/res-keyguard/values/config.xml +++ b/packages/SystemUI/res-keyguard/values/config.xml @@ -28,6 +28,8 @@ <!-- Will display the bouncer on one side of the display, and the current user icon and user switcher on the other side --> <bool name="config_enableBouncerUserSwitcher">false</bool> + <!-- Will enable custom clocks on the lockscreen --> + <bool name="config_enableLockScreenCustomClocks">true</bool> <!-- Time to be considered a consecutive fingerprint failure in ms --> <integer name="fp_consecutive_failure_time_ms">3500</integer> </resources> diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml index 4d289ebeb30b..88f7bcd5d907 100644 --- a/packages/SystemUI/res-keyguard/values/styles.xml +++ b/packages/SystemUI/res-keyguard/values/styles.xml @@ -23,6 +23,26 @@ <style name="Keyguard.TextView" parent="@android:style/Widget.DeviceDefault.TextView"> <item name="android:textSize">@dimen/kg_status_line_font_size</item> <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + <style name="Keyguard.Bouncer.PrimaryMessage" parent="Theme.SystemUI"> + <item name="android:textSize">18sp</item> + <item name="android:lineHeight">24dp</item> + <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + <item name="android:singleLine">true</item> + <item name="android:textAlignment">center</item> + <item name="android:ellipsize">marquee</item> + </style> + <style name="Keyguard.Bouncer.SecondaryMessage" parent="Theme.SystemUI"> + <item name="android:textSize">14sp</item> + <item name="android:lineHeight">20dp</item> + <item name="android:maxLines">@integer/bouncer_secondary_message_lines</item> + <item name="android:lines">@integer/bouncer_secondary_message_lines</item> + <item name="android:textAlignment">center</item> + <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item> + <item name="android:ellipsize">end</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item> </style> <style name="Keyguard.TextView.EmergencyButton" parent="Theme.SystemUI"> <item name="android:textColor">?androidprv:attr/materialColorOnTertiaryFixed</item> diff --git a/packages/SystemUI/res/drawable/dream_overlay_bottom_affordance_bg.xml b/packages/SystemUI/res/drawable/dream_overlay_bottom_affordance_bg.xml index 3b67ddd02d78..bab604d7297e 100644 --- a/packages/SystemUI/res/drawable/dream_overlay_bottom_affordance_bg.xml +++ b/packages/SystemUI/res/drawable/dream_overlay_bottom_affordance_bg.xml @@ -18,9 +18,7 @@ --> <shape xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorSurface"/> <size android:width="@dimen/dream_overlay_bottom_affordance_height" android:height="@dimen/dream_overlay_bottom_affordance_width"/> diff --git a/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml index 43cf00332f63..adeb81fcf820 100644 --- a/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml +++ b/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml @@ -18,8 +18,8 @@ <item android:id="@android:id/background"> <shape> <corners - android:bottomRightRadius="28dp" - android:topRightRadius="28dp" + android:bottomRightRadius="@dimen/media_output_dialog_active_background_radius" + android:topRightRadius="@dimen/media_output_dialog_active_background_radius" /> <solid android:color="@android:color/transparent" /> <size diff --git a/packages/SystemUI/res/drawable/media_output_item_background_active.xml b/packages/SystemUI/res/drawable/media_output_item_background_active.xml index 839db4aae470..826e0caf638f 100644 --- a/packages/SystemUI/res/drawable/media_output_item_background_active.xml +++ b/packages/SystemUI/res/drawable/media_output_item_background_active.xml @@ -17,6 +17,6 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners - android:radius="28dp"/> + android:radius="@dimen/media_output_dialog_active_background_radius"/> <solid android:color="@color/media_dialog_item_background" /> </shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml index c12bfcca4eff..0e6b2812a8a9 100644 --- a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml +++ b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml @@ -32,8 +32,8 @@ <com.airbnb.lottie.LottieAnimationView android:id="@+id/rear_display_folded_animation" android:importantForAccessibility="no" - android:layout_width="@dimen/rear_display_animation_width" - android:layout_height="@dimen/rear_display_animation_height" + android:layout_width="@dimen/rear_display_animation_width_opened" + android:layout_height="@dimen/rear_display_animation_height_opened" android:layout_gravity="center" android:contentDescription="@string/rear_display_accessibility_unfolded_animation" android:scaleType="fitXY" @@ -49,8 +49,8 @@ android:text="@string/rear_display_unfolded_bottom_sheet_title" android:textAppearance="@style/TextAppearance.Dialog.Title" android:lineSpacingExtra="2sp" - android:paddingTop="@dimen/rear_display_title_top_padding" - android:paddingBottom="@dimen/rear_display_title_bottom_padding" + android:paddingTop="@dimen/rear_display_title_top_padding_opened" + android:paddingBottom="@dimen/rear_display_title_bottom_padding_opened" android:gravity="center_horizontal|center_vertical" /> diff --git a/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml index 0cd062383570..e8a48c7c01a4 100644 --- a/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml +++ b/packages/SystemUI/res/layout/dream_overlay_home_controls_chip.xml @@ -21,8 +21,6 @@ android:layout_width="@dimen/dream_overlay_bottom_affordance_width" android:layout_gravity="bottom|start" android:padding="@dimen/dream_overlay_bottom_affordance_padding" - android:background="@drawable/dream_overlay_bottom_affordance_bg" android:scaleType="fitCenter" android:tint="?android:attr/textColorPrimary" - android:src="@drawable/controls_icon" android:contentDescription="@string/quick_controls_title" /> diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index e9acf3fa10c8..a3a7135dabad 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -59,55 +59,44 @@ </LinearLayout> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:layout_gravity="bottom" - android:layout_marginHorizontal="@dimen/keyguard_affordance_horizontal_offset" + <com.android.systemui.animation.view.LaunchableImageView + android:id="@+id/start_button" + android:layout_height="@dimen/keyguard_affordance_fixed_height" + android:layout_width="@dimen/keyguard_affordance_fixed_width" + android:layout_gravity="start|bottom" + android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset" android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset" - android:gravity="bottom" - > - - <com.android.systemui.animation.view.LaunchableImageView - android:id="@+id/start_button" - android:layout_height="@dimen/keyguard_affordance_fixed_height" - android:layout_width="@dimen/keyguard_affordance_fixed_width" - android:scaleType="fitCenter" - android:padding="@dimen/keyguard_affordance_fixed_padding" - android:tint="?android:attr/textColorPrimary" - android:background="@drawable/keyguard_bottom_affordance_bg" - android:foreground="@drawable/keyguard_bottom_affordance_selected_border" - android:visibility="invisible" /> - - <FrameLayout - android:layout_width="0dp" - android:layout_weight="1" - android:layout_height="wrap_content" - android:paddingHorizontal="24dp" - > - <include - android:id="@+id/keyguard_settings_button" - layout="@layout/keyguard_settings_popup_menu" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:visibility="gone" - /> - </FrameLayout> + android:scaleType="fitCenter" + android:padding="@dimen/keyguard_affordance_fixed_padding" + android:tint="?android:attr/textColorPrimary" + android:background="@drawable/keyguard_bottom_affordance_bg" + android:foreground="@drawable/keyguard_bottom_affordance_selected_border" + android:visibility="invisible" /> - <com.android.systemui.animation.view.LaunchableImageView - android:id="@+id/end_button" - android:layout_height="@dimen/keyguard_affordance_fixed_height" - android:layout_width="@dimen/keyguard_affordance_fixed_width" - android:scaleType="fitCenter" - android:padding="@dimen/keyguard_affordance_fixed_padding" - android:tint="?android:attr/textColorPrimary" - android:background="@drawable/keyguard_bottom_affordance_bg" - android:foreground="@drawable/keyguard_bottom_affordance_selected_border" - android:visibility="invisible" /> + <com.android.systemui.animation.view.LaunchableImageView + android:id="@+id/end_button" + android:layout_height="@dimen/keyguard_affordance_fixed_height" + android:layout_width="@dimen/keyguard_affordance_fixed_width" + android:layout_gravity="end|bottom" + android:layout_marginEnd="@dimen/keyguard_affordance_horizontal_offset" + android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset" + android:scaleType="fitCenter" + android:padding="@dimen/keyguard_affordance_fixed_padding" + android:tint="?android:attr/textColorPrimary" + android:background="@drawable/keyguard_bottom_affordance_bg" + android:foreground="@drawable/keyguard_bottom_affordance_selected_border" + android:visibility="invisible" /> - </LinearLayout> + <include + android:id="@+id/keyguard_settings_button" + layout="@layout/keyguard_settings_popup_menu" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|center" + android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset" + android:layout_marginHorizontal="@dimen/keyguard_affordance_horizontal_offset" + android:visibility="gone" + /> <FrameLayout android:id="@+id/overlay_container" diff --git a/packages/SystemUI/res/layout/keyguard_status_bar.xml b/packages/SystemUI/res/layout/keyguard_status_bar.xml index 8b8594032816..64c4effece1b 100644 --- a/packages/SystemUI/res/layout/keyguard_status_bar.xml +++ b/packages/SystemUI/res/layout/keyguard_status_bar.xml @@ -78,6 +78,7 @@ android:textColor="?attr/wallpaperTextColorSecondary" android:singleLine="true" systemui:showMissingSim="true" - systemui:showAirplaneMode="true" /> + systemui:showAirplaneMode="true" + systemui:debugLocation="Keyguard" /> </com.android.systemui.statusbar.phone.KeyguardStatusBarView> diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml index e182a6ae78de..9c1dc6448b4b 100644 --- a/packages/SystemUI/res/layout/media_output_dialog.xml +++ b/packages/SystemUI/res/layout/media_output_dialog.xml @@ -31,7 +31,7 @@ android:paddingStart="16dp" android:paddingTop="16dp" android:paddingEnd="16dp" - android:paddingBottom="24dp" + android:paddingBottom="16dp" android:orientation="horizontal"> <ImageView android:id="@+id/header_icon" @@ -113,6 +113,8 @@ <androidx.recyclerview.widget.RecyclerView android:id="@+id/list_result" android:scrollbars="vertical" + android:paddingTop="8dp" + android:clipToPadding="false" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout> diff --git a/packages/SystemUI/res/layout/media_output_list_item_advanced.xml b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml index a65051206e7b..054193a5b323 100644 --- a/packages/SystemUI/res/layout/media_output_list_item_advanced.xml +++ b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml @@ -50,12 +50,16 @@ android:id="@+id/icon_area" android:layout_width="64dp" android:layout_height="64dp" + android:focusable="false" + android:importantForAccessibility="no" android:background="@drawable/media_output_title_icon_area" android:layout_gravity="center_vertical|start"> <ImageView android:id="@+id/title_icon" android:layout_width="24dp" android:layout_height="24dp" + android:focusable="false" + android:importantForAccessibility="no" android:animateLayoutChanges="true" android:layout_gravity="center"/> <TextView diff --git a/packages/SystemUI/res/layout/ongoing_call_chip.xml b/packages/SystemUI/res/layout/ongoing_call_chip.xml index 238fc8438331..6a0217ec5fe8 100644 --- a/packages/SystemUI/res/layout/ongoing_call_chip.xml +++ b/packages/SystemUI/res/layout/ongoing_call_chip.xml @@ -23,7 +23,7 @@ android:layout_gravity="center_vertical|start" android:layout_marginStart="5dp" > - <com.android.systemui.animation.view.LaunchableLinearLayout + <com.android.systemui.statusbar.phone.ongoingcall.OngoingCallBackgroundContainer android:id="@+id/ongoing_call_chip_background" android:layout_width="wrap_content" android:layout_height="@dimen/ongoing_appops_chip_height" @@ -55,5 +55,5 @@ android:textColor="?android:attr/colorPrimary" /> - </com.android.systemui.animation.view.LaunchableLinearLayout> + </com.android.systemui.statusbar.phone.ongoingcall.OngoingCallBackgroundContainer> </FrameLayout> diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml index db8191baa790..a8febe79acae 100644 --- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml +++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml @@ -112,9 +112,13 @@ android:focusable="true"> <TextView + android:id="@+id/magnifier_horizontal_lock_title" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:singleLine="true" + android:scrollHorizontally="true" + android:ellipsize="marquee" android:text="@string/accessibility_allow_diagonal_scrolling" android:textAppearance="@style/TextAppearance.MagnificationSetting.Title" android:layout_gravity="center_vertical" /> diff --git a/packages/SystemUI/res/raw/sfps_pulse.json b/packages/SystemUI/res/raw/sfps_pulse.json index c4903a2857a1..2a72dfb46a5e 100644 --- a/packages/SystemUI/res/raw/sfps_pulse.json +++ b/packages/SystemUI/res/raw/sfps_pulse.json @@ -1 +1 @@ -{"v":"5.7.6","fr":60,"ip":0,"op":300,"w":42,"h":80,"nm":"Fingerprint Pulse Motion","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[28,40,0],"to":[0.751,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":30,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":60,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":90,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":120,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":150,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":180,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":210,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":240,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":270,"s":[32.503,40,0],"to":[0,0,0],"ti":[0.751,0,0]},{"t":300,"s":[28,40,0]}],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.566,0],[-3.929,-5.503],[-2.751,-6.68],[3.929,0],[-2.751,6.68],[-3.929,5.503]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[30.218,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".blue600","cl":"blue600","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[41.878,40,0],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[55,55],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 2","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[28.253,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[20]},{"t":30,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[55,55]},{"t":30,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-30,"op":30,"st":-30,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[20]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[55,55]},{"t":60,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[20]},{"t":90,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[55,55]},{"t":90,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":90,"st":30,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[20]},{"t":120,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[55,55]},{"t":120,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":120,"st":60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[20]},{"t":150,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[55,55]},{"t":150,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":150,"st":90,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":160,"s":[20]},{"t":180,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":120,"s":[55,55]},{"t":180,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":120,"op":180,"st":120,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":190,"s":[20]},{"t":210,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":150,"s":[55,55]},{"t":210,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":150,"op":210,"st":150,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":220,"s":[20]},{"t":240,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":180,"s":[55,55]},{"t":240,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":240,"st":180,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[20]},{"t":270,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":210,"s":[55,55]},{"t":270,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":210,"op":270,"st":210,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[20]},{"t":300,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":240,"s":[55,55]},{"t":300,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":300,"st":240,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":310,"s":[20]},{"t":330,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":270,"s":[55,55]},{"t":330,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":330,"st":270,"bm":0}],"markers":[]}
\ No newline at end of file +{"v":"5.7.14","fr":60,"ip":0,"op":300,"w":42,"h":80,"nm":"sfps_pulse_motion_04","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.617,"y":0.539},"o":{"x":0.477,"y":0},"t":0,"s":[28,40,0],"to":[0.365,0,0],"ti":[-1.009,0,0]},{"i":{"x":0.436,"y":1},"o":{"x":0.17,"y":0.547},"t":10,"s":[30.576,40,0],"to":[1.064,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":30,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":60,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":90,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":120,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":150,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":180,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":210,"s":[32.503,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":240,"s":[28,40,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":270,"s":[32.503,40,0],"to":[0,0,0],"ti":[0.751,0,0]},{"t":300,"s":[28,40,0]}],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.566,0],[-3.929,-5.503],[-2.751,-6.68],[3.929,0],[-2.751,6.68],[-3.929,5.503]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[30.218,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".blue600","cl":"blue600","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[41.878,40,0],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[55,55],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 2","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[28.253,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[75]},{"t":30,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[55,55]},{"t":30,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-30,"op":30,"st":-30,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[75]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[55,55]},{"t":60,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[75]},{"t":90,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[55,55]},{"t":90,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":90,"st":30,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[75]},{"t":120,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[55,55]},{"t":120,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":120,"st":60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[75]},{"t":150,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[55,55]},{"t":150,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":150,"st":90,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":160,"s":[75]},{"t":180,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":120,"s":[55,55]},{"t":180,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":120,"op":180,"st":120,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":190,"s":[75]},{"t":210,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":150,"s":[55,55]},{"t":210,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":150,"op":210,"st":150,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":220,"s":[75]},{"t":240,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":180,"s":[55,55]},{"t":240,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":240,"st":180,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[75]},{"t":270,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":210,"s":[55,55]},{"t":270,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":210,"op":270,"st":210,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[75]},{"t":300,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":240,"s":[55,55]},{"t":300,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":300,"st":240,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":310,"s":[75]},{"t":330,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25.587,40,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":270,"s":[55,55]},{"t":330,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":330,"st":270,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/sfps_pulse_landscape.json b/packages/SystemUI/res/raw/sfps_pulse_landscape.json index 8c91762d7286..42b92eb8001f 100644 --- a/packages/SystemUI/res/raw/sfps_pulse_landscape.json +++ b/packages/SystemUI/res/raw/sfps_pulse_landscape.json @@ -1 +1 @@ -{"v":"5.7.6","fr":60,"ip":0,"op":300,"w":80,"h":42,"nm":"Fingerprint Pulse Motion Portrait","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[40,14,0],"to":[0,-0.751,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":30,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":60,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":90,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":120,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":150,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":180,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":210,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":240,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":270,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,-0.751,0]},{"t":300,"s":[40,14,0]}],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.566,0],[-3.929,-5.503],[-2.751,-6.68],[3.929,0],[-2.751,6.68],[-3.929,5.503]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[30.218,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".blue600","cl":"blue600","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,0.122,0],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[55,55],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 2","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[28.253,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[20]},{"t":30,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[55,55]},{"t":30,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-30,"op":30,"st":-30,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[20]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[55,55]},{"t":60,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[20]},{"t":90,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[55,55]},{"t":90,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":90,"st":30,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[20]},{"t":120,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[55,55]},{"t":120,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":120,"st":60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[20]},{"t":150,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[55,55]},{"t":150,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":150,"st":90,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":160,"s":[20]},{"t":180,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":120,"s":[55,55]},{"t":180,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":120,"op":180,"st":120,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":190,"s":[20]},{"t":210,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":150,"s":[55,55]},{"t":210,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":150,"op":210,"st":150,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":220,"s":[20]},{"t":240,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":180,"s":[55,55]},{"t":240,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":240,"st":180,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[20]},{"t":270,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":210,"s":[55,55]},{"t":270,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":210,"op":270,"st":210,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[20]},{"t":300,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":240,"s":[55,55]},{"t":300,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":300,"st":240,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":310,"s":[20]},{"t":330,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":270,"s":[55,55]},{"t":330,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":330,"st":270,"bm":0}],"markers":[]}
\ No newline at end of file +{"v":"5.7.13","fr":60,"ip":0,"op":300,"w":80,"h":42,"nm":"sfps_pulse_motion_portrait","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[40,14,0],"to":[0,-0.751,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":30,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":60,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":90,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":120,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":150,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":180,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":210,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.3,"y":0},"t":240,"s":[40,14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":270,"s":[40,9.497,0],"to":[0,0,0],"ti":[0,-0.751,0]},{"t":300,"s":[40,14,0]}],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.566,0],[-3.929,-5.503],[-2.751,-6.68],[3.929,0],[-2.751,6.68],[-3.929,5.503]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[30.218,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".blue600","cl":"blue600","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,0.122,0],"ix":2,"l":2},"a":{"a":0,"k":[28.253,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[55,55],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 2","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.101960785687,0.450980395079,0.909803926945,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[28.253,40],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[75]},{"t":30,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":-30,"s":[55,55]},{"t":30,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-30,"op":30,"st":-30,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":40,"s":[75]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[55,55]},{"t":60,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[75]},{"t":90,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[55,55]},{"t":90,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":90,"st":30,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":100,"s":[75]},{"t":120,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":60,"s":[55,55]},{"t":120,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":120,"st":60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":130,"s":[75]},{"t":150,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[55,55]},{"t":150,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":90,"op":150,"st":90,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":160,"s":[75]},{"t":180,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":120,"s":[55,55]},{"t":180,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":120,"op":180,"st":120,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":190,"s":[75]},{"t":210,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":150,"s":[55,55]},{"t":210,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":150,"op":210,"st":150,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":220,"s":[75]},{"t":240,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":180,"s":[55,55]},{"t":240,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":180,"op":240,"st":180,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[75]},{"t":270,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":210,"s":[55,55]},{"t":270,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":210,"op":270,"st":210,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":280,"s":[75]},{"t":300,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":240,"s":[55,55]},{"t":300,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":240,"op":300,"st":240,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".blue400","cl":"blue400","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":310,"s":[75]},{"t":330,"s":[0]}],"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[40,16.413,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":270,"s":[55,55]},{"t":330,"s":[80,80]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.40000000596,0.615686297417,0.964705884457,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.338,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":270,"op":330,"st":270,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 908aac4a7b7f..f277e8a6f02f 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -67,6 +67,12 @@ <dimen name="controls_header_horizontal_padding">12dp</dimen> <dimen name="controls_content_margin_horizontal">16dp</dimen> + <!-- Rear Display Education dimens --> + <dimen name="rear_display_animation_width">246dp</dimen> + <dimen name="rear_display_animation_height">180dp</dimen> + <dimen name="rear_display_title_top_padding">4dp</dimen> + <dimen name="rear_display_title_bottom_padding">0dp</dimen> + <!-- Bouncer user switcher margins --> <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index 8d8fdf01c0fb..bd86e51ccf8f 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -146,6 +146,7 @@ <attr name="allCaps" format="boolean" /> <attr name="showMissingSim" format="boolean" /> <attr name="showAirplaneMode" format="boolean" /> + <attr name="debugLocation" format="string" /> </declare-styleable> <declare-styleable name="IlluminationDrawable"> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 26d687588263..c3651cfa36e7 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -44,6 +44,12 @@ <!-- orientation of the dead zone when touches have recently occurred elsewhere on screen --> <integer name="navigation_bar_deadzone_orientation">0</integer> + <!-- Whether or not lockscreen shortcuts can be customized --> + <bool name="custom_lockscreen_shortcuts_enabled">false</bool> + + <!-- Whether or not long-pressing on keyguard will display to customize lockscreen --> + <bool name="long_press_keyguard_customize_lockscreen_enabled">false</bool> + <bool name="config_dead_zone_flash">false</bool> <!-- Whether to enable dimming navigation buttons when wallpaper is not visible, should be @@ -577,6 +583,9 @@ <!-- Whether to show the side fps hint while on bouncer --> <bool name="config_show_sidefps_hint_on_bouncer">true</bool> + <!-- Max number of lines we want to show for the bouncer secondary message --> + <integer name="bouncer_secondary_message_lines">2</integer> + <!-- Whether to use the split 2-column notification shade --> <bool name="config_use_split_notification_shade">false</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index bf0b8a660432..271fab17e0af 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -45,6 +45,9 @@ <dimen name="navigation_edge_action_drag_threshold">16dp</dimen> <!-- The threshold to progress back animation for edge swipe --> <dimen name="navigation_edge_action_progress_threshold">412dp</dimen> + <!-- This value is used to calculate the target if the screen is wider than the + navigation_edge_action_progress_threshold. See BackAnimation#setSwipeThresholds --> + <item name="back_progress_non_linear_factor" format="float" type="dimen">0.2</item> <!-- The minimum display position of the arrow on the screen --> <dimen name="navigation_edge_arrow_min_y">64dp</dimen> <!-- The amount by which the arrow is shifted to avoid the finger--> @@ -617,8 +620,8 @@ <dimen name="qs_header_height">120dp</dimen> <dimen name="qs_header_row_min_height">48dp</dimen> - <dimen name="qs_header_non_clickable_element_height">24dp</dimen> - <dimen name="new_qs_header_non_clickable_element_height">24dp</dimen> + <dimen name="qs_header_non_clickable_element_height">24sp</dimen> + <dimen name="new_qs_header_non_clickable_element_height">24sp</dimen> <dimen name="qs_footer_padding">20dp</dimen> <dimen name="qs_security_footer_height">88dp</dimen> @@ -807,6 +810,7 @@ <!-- The width/height of the unlock icon view on keyguard. --> <dimen name="keyguard_lock_height">42dp</dimen> <dimen name="keyguard_lock_padding">20dp</dimen> + <dimen name="secondary_message_padding">8dp</dimen> <dimen name="keyguard_security_container_padding_top">20dp</dimen> @@ -1042,7 +1046,7 @@ <dimen name="display_cutout_margin_consumption">0px</dimen> <!-- Height of the Ongoing App Ops chip --> - <dimen name="ongoing_appops_chip_height">24dp</dimen> + <dimen name="ongoing_appops_chip_height">24sp</dimen> <!-- Side padding between background of Ongoing App Ops chip and content --> <dimen name="ongoing_appops_chip_side_padding">8dp</dimen> <!-- Margin between icons of Ongoing App Ops chip --> @@ -1315,9 +1319,10 @@ <dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen> <dimen name="media_output_dialog_app_tier_icon_size">20dp</dimen> <dimen name="media_output_dialog_background_radius">16dp</dimen> - <dimen name="media_output_dialog_active_background_radius">28dp</dimen> + <dimen name="media_output_dialog_active_background_radius">30dp</dimen> <dimen name="media_output_dialog_default_margin_end">16dp</dimen> <dimen name="media_output_dialog_selectable_margin_end">80dp</dimen> + <dimen name="media_output_dialog_list_padding_top">8dp</dimen> <!-- Distance that the full shade transition takes in order to complete by tapping on a button like "expand". --> @@ -1641,6 +1646,19 @@ <dimen name="dream_overlay_bottom_affordance_height">64dp</dimen> <dimen name="dream_overlay_bottom_affordance_width">64dp</dimen> <dimen name="dream_overlay_bottom_affordance_radius">32dp</dimen> + <dimen name="dream_overlay_bottom_affordance_key_text_shadow_dx">0.5dp</dimen> + <dimen name="dream_overlay_bottom_affordance_key_text_shadow_dy">0.5dp</dimen> + <dimen name="dream_overlay_bottom_affordance_key_text_shadow_radius">1dp</dimen> + <item name="dream_overlay_bottom_affordance_key_shadow_alpha" format="float" type="dimen"> + 0.35 + </item> + <dimen name="dream_overlay_bottom_affordance_ambient_text_shadow_dx">0.5dp</dimen> + <dimen name="dream_overlay_bottom_affordance_ambient_text_shadow_dy">0.5dp</dimen> + <dimen name="dream_overlay_bottom_affordance_ambient_text_shadow_radius">2dp</dimen> + <item name="dream_overlay_bottom_affordance_ambient_shadow_alpha" format="float" type="dimen"> + 0.4 + </item> + <dimen name="dream_overlay_bottom_affordance_inset">1dp</dimen> <dimen name="dream_overlay_bottom_affordance_padding">14dp</dimen> <dimen name="dream_overlay_complication_clock_time_text_size">86dp</dimen> <dimen name="dream_overlay_complication_clock_subtitle_text_size">24sp</dimen> @@ -1772,8 +1790,12 @@ <!-- Rear Display Education dimens --> <dimen name="rear_display_animation_width">273dp</dimen> <dimen name="rear_display_animation_height">200dp</dimen> + <dimen name="rear_display_animation_width_opened">273dp</dimen> + <dimen name="rear_display_animation_height_opened">200dp</dimen> <dimen name="rear_display_title_top_padding">24dp</dimen> <dimen name="rear_display_title_bottom_padding">16dp</dimen> + <dimen name="rear_display_title_top_padding_opened">24dp</dimen> + <dimen name="rear_display_title_bottom_padding_opened">16dp</dimen> <!-- Bouncer user switcher margins --> <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cbc73faa46af..7dc8afe08b61 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2583,7 +2583,7 @@ <!-- Title for media controls [CHAR_LIMIT=50] --> <string name="controls_media_title">Media</string> <!-- Explanation for closing controls associated with a specific media session [CHAR_LIMIT=50] --> - <string name="controls_media_close_session">Hide this media control for <xliff:g id="app_name" example="YouTube Music">%1$s</xliff:g>?</string> + <string name="controls_media_close_session">Hide this media control for <xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g>?</string> <!-- Explanation that controls associated with a specific media session are active [CHAR_LIMIT=50] --> <string name="controls_media_active_session">The current media session cannot be hidden.</string> <!-- Label for a button that will hide media controls [CHAR_LIMIT=30] --> @@ -2596,6 +2596,8 @@ <string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string> <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] --> <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string> + <!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] --> + <string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string> <!-- Description for button in media controls. Pressing button starts playback [CHAR_LIMIT=NONE] --> <string name="controls_media_button_play">Play</string> @@ -3101,17 +3103,20 @@ <!-- Switch to work profile dialer app for placing a call dialog. --> <!-- Text for Switch to work profile dialog's Title. Switch to work profile dialog guide users to make call from work profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=60] --> - <string name="call_from_work_profile_title">Can\'t call from this profile</string> + <string name="call_from_work_profile_title">Can\'t call from a personal app</string> <!-- Text for switch to work profile for call dialog to guide users to make call from work profile dialer app as it's not possible to make call from current profile due to an admin policy. [CHAR LIMIT=NONE] --> - <string name="call_from_work_profile_text">Your work policy allows you to make phone calls only from the work profile</string> + <string name="call_from_work_profile_text">Your organization only allows you to make calls from work apps</string> <!-- Label for the button to switch to work profile for placing call. Switch to work profile dialog guide users to make call from work profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] --> <string name="call_from_work_profile_action">Switch to work profile</string> + <!-- Label for the button to switch to work profile for placing call. Switch to work profile dialog guide users to make call from work + profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] --> + <string name="install_dialer_on_work_profile_action">Install a work phone app</string> <!-- Label for the close button on switch to work profile dialog. Switch to work profile dialog guide users to make call from work profile dialer app as it's not possible to make call from current profile due to an admin policy.[CHAR LIMIT=60] --> - <string name="call_from_work_profile_close">Close</string> + <string name="call_from_work_profile_close">Cancel</string> <!-- Label for a menu item in a menu that is shown when the user wishes to customize the lock screen. diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java index 6bf1ce57b01e..9bead9410a51 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java @@ -234,10 +234,27 @@ public abstract class Condition { } protected final String getTag() { + if (isOverridingCondition()) { + return mTag + "[OVRD]"; + } + return mTag; } /** + * Returns the state of the condition. + * - "Invalid", condition hasn't been set / not monitored + * - "True", condition has been met + * - "False", condition has not been met + */ + protected final String getState() { + if (!isConditionSet()) { + return "Invalid"; + } + return isConditionMet() ? "True" : "False"; + } + + /** * Creates a new condition which will only be true when both this condition and all the provided * conditions are true. */ diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java index 43df08df9f45..1f61c64dd057 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java @@ -22,6 +22,7 @@ import android.util.Log; import androidx.annotation.NonNull; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.log.TableLogBufferBase; import java.util.ArrayList; import java.util.Collections; @@ -41,6 +42,7 @@ public class Monitor { private final String mTag = getClass().getSimpleName(); private final Executor mExecutor; private final Set<Condition> mPreconditions; + private final TableLogBufferBase mLogBuffer; private final HashMap<Condition, ArraySet<Subscription.Token>> mConditions = new HashMap<>(); private final HashMap<Subscription.Token, SubscriptionState> mSubscriptions = new HashMap<>(); @@ -160,11 +162,23 @@ public class Monitor { * Main constructor, allowing specifying preconditions. */ public Monitor(Executor executor, Set<Condition> preconditions) { + this(executor, preconditions, null); + } + + /** + * Main constructor, allowing specifying preconditions and a log buffer for logging. + */ + public Monitor(Executor executor, Set<Condition> preconditions, TableLogBufferBase logBuffer) { mExecutor = executor; mPreconditions = preconditions; + mLogBuffer = logBuffer; } private void updateConditionMetState(Condition condition) { + if (mLogBuffer != null) { + mLogBuffer.logChange(/* prefix= */ "", condition.getTag(), condition.getState()); + } + final ArraySet<Subscription.Token> subscriptions = mConditions.get(condition); // It's possible the condition was removed between the time the callback occurred and diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl index b8bddd149d9a..117cf78a4fe3 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl @@ -34,12 +34,12 @@ interface ISystemUiProxy { /** * Begins screen pinning on the provided {@param taskId}. */ - void startScreenPinning(int taskId) = 1; + oneway void startScreenPinning(int taskId) = 1; /** * Notifies SystemUI that Overview is shown. */ - void onOverviewShown(boolean fromHome) = 6; + oneway void onOverviewShown(boolean fromHome) = 6; /** * Proxies motion events from the homescreen UI to the status bar. Only called when @@ -48,57 +48,57 @@ interface ISystemUiProxy { * * Normal gesture: DOWN, MOVE/POINTER_DOWN/POINTER_UP)*, UP or CANCLE */ - void onStatusBarMotionEvent(in MotionEvent event) = 9; + oneway void onStatusBarMotionEvent(in MotionEvent event) = 9; /** * Proxies the assistant gesture's progress started from navigation bar. */ - void onAssistantProgress(float progress) = 12; + oneway void onAssistantProgress(float progress) = 12; /** * Proxies the assistant gesture fling velocity (in pixels per millisecond) upon completion. * Velocity is 0 for drag gestures. */ - void onAssistantGestureCompletion(float velocity) = 18; + oneway void onAssistantGestureCompletion(float velocity) = 18; /** * Start the assistant. */ - void startAssistant(in Bundle bundle) = 13; + oneway void startAssistant(in Bundle bundle) = 13; /** * Notifies that the accessibility button in the system's navigation area has been clicked */ - void notifyAccessibilityButtonClicked(int displayId) = 15; + oneway void notifyAccessibilityButtonClicked(int displayId) = 15; /** * Notifies that the accessibility button in the system's navigation area has been long clicked */ - void notifyAccessibilityButtonLongClicked() = 16; + oneway void notifyAccessibilityButtonLongClicked() = 16; /** * Ends the system screen pinning. */ - void stopScreenPinning() = 17; + oneway void stopScreenPinning() = 17; /** * Notifies that quickstep will switch to a new task * @param rotation indicates which Surface.Rotation the gesture was started in */ - void notifyPrioritizedRotation(int rotation) = 25; + oneway void notifyPrioritizedRotation(int rotation) = 25; /** * Notifies to expand notification panel. */ - void expandNotificationPanel() = 29; + oneway void expandNotificationPanel() = 29; /** * Notifies SystemUI to invoke Back. */ - void onBackPressed() = 44; + oneway void onBackPressed() = 44; /** Sets home rotation enabled. */ - void setHomeRotationEnabled(boolean enabled) = 45; + oneway void setHomeRotationEnabled(boolean enabled) = 45; /** Notifies when taskbar status updated */ oneway void notifyTaskbarStatus(boolean visible, boolean stashed) = 47; @@ -113,17 +113,17 @@ interface ISystemUiProxy { /** * Notifies SystemUI to invoke IME Switcher. */ - void onImeSwitcherPressed() = 49; + oneway void onImeSwitcherPressed() = 49; /** * Notifies to toggle notification panel. */ - void toggleNotificationPanel() = 50; + oneway void toggleNotificationPanel() = 50; /** * Handle the screenshot request. */ - void takeScreenshot(in ScreenshotRequest request) = 51; + oneway void takeScreenshot(in ScreenshotRequest request) = 51; // Next id = 52 } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt index 482158e80d0f..9a0044761504 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt @@ -21,9 +21,9 @@ import android.graphics.Color import android.graphics.Point import android.graphics.Rect import android.graphics.RectF +import android.util.Log import android.view.View import androidx.annotation.VisibleForTesting -import com.android.systemui.shared.navigationbar.RegionSamplingHelper import java.io.PrintWriter import java.util.concurrent.Executor @@ -31,20 +31,21 @@ import java.util.concurrent.Executor open class RegionSampler @JvmOverloads constructor( - val sampledView: View?, + val sampledView: View, mainExecutor: Executor?, val bgExecutor: Executor?, val regionSamplingEnabled: Boolean, + val isLockscreen: Boolean = false, + val wallpaperManager: WallpaperManager? = WallpaperManager.getInstance(sampledView.context), val updateForegroundColor: UpdateColorCallback, - val wallpaperManager: WallpaperManager? = WallpaperManager.getInstance(sampledView?.context) ) : WallpaperManager.LocalWallpaperColorConsumer { private var regionDarkness = RegionDarkness.DEFAULT private var samplingBounds = Rect() private val tmpScreenLocation = IntArray(2) - @VisibleForTesting var regionSampler: RegionSamplingHelper? = null private var lightForegroundColor = Color.WHITE private var darkForegroundColor = Color.BLACK - private val displaySize = Point() + @VisibleForTesting val displaySize = Point() + private var initialSampling: WallpaperColors? = null /** * Sets the colors to be used for Dark and Light Foreground. @@ -57,6 +58,36 @@ constructor( darkForegroundColor = darkColor } + private val layoutChangedListener = + object : View.OnLayoutChangeListener { + + override fun onLayoutChange( + view: View?, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + + // don't pass in negative bounds when region is in transition state + if (sampledView.locationOnScreen[0] < 0 || sampledView.locationOnScreen[1] < 0) { + return + } + + val currentViewRect = Rect(left, top, right, bottom) + val oldViewRect = Rect(oldLeft, oldTop, oldRight, oldBottom) + + if (currentViewRect != oldViewRect) { + stopRegionSampler() + startRegionSampler() + } + } + } + /** * Determines which foreground color to use based on region darkness. * @@ -84,40 +115,57 @@ constructor( /** Start region sampler */ fun startRegionSampler() { - if (!regionSamplingEnabled || sampledView == null) { + + if (!regionSamplingEnabled) { + if (DEBUG) Log.d(TAG, "startRegionSampler() | RegionSampling flag not enabled") return } - val sampledRegion = calculateSampledRegion(sampledView) - val regions = ArrayList<RectF>() - val sampledRegionWithOffset = convertBounds(sampledRegion) + sampledView.addOnLayoutChangeListener(layoutChangedListener) + + val screenLocationBounds = calculateScreenLocation(sampledView) + if (screenLocationBounds == null) { + if (DEBUG) Log.d(TAG, "startRegionSampler() | passed in null region") + return + } + if (screenLocationBounds.isEmpty) { + if (DEBUG) Log.d(TAG, "startRegionSampler() | passed in empty region") + return + } + val sampledRegionWithOffset = convertBounds(screenLocationBounds) if ( sampledRegionWithOffset.left < 0.0 || sampledRegionWithOffset.right > 1.0 || sampledRegionWithOffset.top < 0.0 || sampledRegionWithOffset.bottom > 1.0 ) { - android.util.Log.e( - "RegionSampler", - "view out of bounds: $sampledRegion | " + - "screen width: ${displaySize.x}, screen height: ${displaySize.y}", - Exception() - ) + if (DEBUG) + Log.d( + TAG, + "startRegionSampler() | view out of bounds: $screenLocationBounds | " + + "screen width: ${displaySize.x}, screen height: ${displaySize.y}", + Exception() + ) return } + val regions = ArrayList<RectF>() regions.add(sampledRegionWithOffset) - wallpaperManager?.removeOnColorsChangedListener(this) - wallpaperManager?.addOnColorsChangedListener(this, regions) + wallpaperManager?.addOnColorsChangedListener( + this, + regions, + if (isLockscreen) WallpaperManager.FLAG_LOCK else WallpaperManager.FLAG_SYSTEM + ) - // TODO(b/265969235): conditionally set FLAG_LOCK or FLAG_SYSTEM once HS smartspace - // implemented bgExecutor?.execute( Runnable { - val initialSampling = - wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK) + initialSampling = + wallpaperManager?.getWallpaperColors( + if (isLockscreen) WallpaperManager.FLAG_LOCK + else WallpaperManager.FLAG_SYSTEM + ) onColorsChanged(sampledRegionWithOffset, initialSampling) } ) @@ -126,6 +174,7 @@ constructor( /** Stop region sampler */ fun stopRegionSampler() { wallpaperManager?.removeOnColorsChangedListener(this) + sampledView.removeOnLayoutChangeListener(layoutChangedListener) } /** Dump region sampler */ @@ -138,22 +187,23 @@ constructor( pw.println("passed-in sampledView: $sampledView") pw.println("calculated samplingBounds: $samplingBounds") pw.println( - "sampledView width: ${sampledView?.width}, sampledView height: ${sampledView?.height}" + "sampledView width: ${sampledView.width}, sampledView height: ${sampledView.height}" ) pw.println("screen width: ${displaySize.x}, screen height: ${displaySize.y}") pw.println( - "sampledRegionWithOffset: ${convertBounds(calculateSampledRegion(sampledView!!))}" + "sampledRegionWithOffset: ${convertBounds( + calculateScreenLocation(sampledView) ?: RectF())}" ) - // TODO(b/265969235): mock initialSampling based on if component is on HS or LS wallpaper - // HS Smartspace - wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_SYSTEM) - // LS Smartspace, clock - wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK) pw.println( - "initialSampling for lockscreen: " + - "${wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK)}" + "initialSampling for ${if (isLockscreen) "lockscreen" else "homescreen" }" + + ": $initialSampling" ) } - fun calculateSampledRegion(sampledView: View): RectF { + fun calculateScreenLocation(sampledView: View): RectF? { + + if (!sampledView.isLaidOut) return null + val screenLocation = tmpScreenLocation /** * The method getLocationOnScreen is used to obtain the view coordinates relative to its @@ -181,7 +231,8 @@ constructor( */ fun convertBounds(originalBounds: RectF): RectF { - // TODO(b/265969235): GRAB # PAGES + CURRENT WALLPAPER PAGE # FROM LAUNCHER + // TODO(b/265969235): GRAB # PAGES + CURRENT WALLPAPER PAGE # FROM LAUNCHER (--> HS + // Smartspace always on 1st page) // TODO(b/265968912): remove hard-coded value once LS wallpaper supported val wallpaperPageNum = 0 val numScreens = 1 @@ -214,6 +265,11 @@ constructor( ) updateForegroundColor() } + + companion object { + private const val TAG = "RegionSampler" + private const val DEBUG = false + } } typealias UpdateColorCallback = () -> Unit diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt index 19d0a3d6bf32..6b9274c437c1 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowIconDrawable.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.shared.shadow +import android.content.res.ColorStateList import android.graphics.BlendMode import android.graphics.Canvas import android.graphics.Color @@ -106,6 +107,14 @@ class DoubleShadowIconDrawable( mIconDrawable.draw(canvas) } + override fun getIntrinsicHeight(): Int { + return mCanvasSize + } + + override fun getIntrinsicWidth(): Int { + return mCanvasSize + } + override fun getOpacity(): Int { return PixelFormat.TRANSPARENT } @@ -121,4 +130,8 @@ class DoubleShadowIconDrawable( override fun setTint(color: Int) { mIconDrawable.setTint(color) } + + override fun setTintList(tint: ColorStateList?) { + mIconDrawable.setTintList(tint) + } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java index 8dcd2aaac9ef..1e7222de91d9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java @@ -41,6 +41,7 @@ import android.window.TransitionInfo; import com.android.wm.shell.util.CounterRotator; public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner.Stub { + private static final String TAG = "RemoteAnimRunnerCompat"; public abstract void onAnimationStart(@WindowManager.TransitionOldType int transit, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, @@ -58,20 +59,24 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner try { finishedCallback.onAnimationFinished(); } catch (RemoteException e) { - Log.e("ActivityOptionsCompat", "Failed to call app controlled animation" - + " finished callback", e); + Log.e(TAG, "Failed to call app controlled animation finished callback", e); } }); } public IRemoteTransition toRemoteTransition() { + return wrap(this); + } + + /** Wraps a remote animation runner in a remote-transition. */ + public static IRemoteTransition.Stub wrap(IRemoteAnimationRunner runner) { return new IRemoteTransition.Stub() { final ArrayMap<IBinder, Runnable> mFinishRunnables = new ArrayMap<>(); @Override public void startAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, - IRemoteTransitionFinishedCallback finishCallback) { + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>(); final RemoteAnimationTarget[] apps = RemoteAnimationTargetCompat.wrapApps(info, t, leashMap); @@ -115,8 +120,14 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner final CounterRotator counterLauncher = new CounterRotator(); final CounterRotator counterWallpaper = new CounterRotator(); if (launcherTask != null && rotateDelta != 0 && launcherTask.getParent() != null) { - counterLauncher.setup(t, info.getChange(launcherTask.getParent()).getLeash(), - rotateDelta, displayW, displayH); + final TransitionInfo.Change parent = info.getChange(launcherTask.getParent()); + if (parent != null) { + counterLauncher.setup(t, parent.getLeash(), rotateDelta, displayW, + displayH); + } else { + Log.e(TAG, "Malformed: " + launcherTask + " has parent=" + + launcherTask.getParent() + " but it's not in info."); + } if (counterLauncher.getSurface() != null) { t.setLayer(counterLauncher.getSurface(), launcherLayer); } @@ -150,8 +161,14 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner counterLauncher.addChild(t, leashMap.get(launcherTask.getLeash())); } if (wallpaper != null && rotateDelta != 0 && wallpaper.getParent() != null) { - counterWallpaper.setup(t, info.getChange(wallpaper.getParent()).getLeash(), - rotateDelta, displayW, displayH); + final TransitionInfo.Change parent = info.getChange(wallpaper.getParent()); + if (parent != null) { + counterWallpaper.setup(t, parent.getLeash(), rotateDelta, displayW, + displayH); + } else { + Log.e(TAG, "Malformed: " + wallpaper + " has parent=" + + wallpaper.getParent() + " but it's not in info."); + } if (counterWallpaper.getSurface() != null) { t.setLayer(counterWallpaper.getSurface(), -1); counterWallpaper.addChild(t, leashMap.get(wallpaper.getLeash())); @@ -175,20 +192,27 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner finishCallback.onTransitionFinished(null /* wct */, finishTransaction); finishTransaction.close(); } catch (RemoteException e) { - Log.e("ActivityOptionsCompat", "Failed to call app controlled animation" - + " finished callback", e); + Log.e(TAG, "Failed to call app controlled animation finished callback", e); } }; synchronized (mFinishRunnables) { mFinishRunnables.put(token, animationFinishedCallback); } // TODO(bc-unlcok): Pass correct transit type. - onAnimationStart(TRANSIT_OLD_NONE, - apps, wallpapers, nonApps, () -> { - synchronized (mFinishRunnables) { - if (mFinishRunnables.remove(token) == null) return; + runner.onAnimationStart(TRANSIT_OLD_NONE, + apps, wallpapers, nonApps, new IRemoteAnimationFinishedCallback() { + @Override + public void onAnimationFinished() { + synchronized (mFinishRunnables) { + if (mFinishRunnables.remove(token) == null) return; + } + animationFinishedCallback.run(); + } + + @Override + public IBinder asBinder() { + return null; } - animationFinishedCallback.run(); }); } @@ -206,7 +230,7 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner t.close(); info.releaseAllSurfaces(); if (finishRunnable == null) return; - onAnimationCancelled(); + runner.onAnimationCancelled(); finishRunnable.run(); } }; diff --git a/packages/SystemUI/shared/src/com/android/systemui/utils/TraceUtils.kt b/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt index 64234c205617..64234c205617 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/utils/TraceUtils.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt diff --git a/packages/SystemUI/src-debug/com/android/systemui/util/StartBinderLoggerModule.kt b/packages/SystemUI/src-debug/com/android/systemui/util/StartBinderLoggerModule.kt new file mode 100644 index 000000000000..d48c5b98165c --- /dev/null +++ b/packages/SystemUI/src-debug/com/android/systemui/util/StartBinderLoggerModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.util + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +abstract class StartBinderLoggerModule { + @Binds + @IntoMap + @ClassKey(BinderLogger::class) + abstract fun bindBinderLogger(impl: BinderLogger): CoreStartable +} diff --git a/packages/SystemUI/src-release/com/android/systemui/util/StartBinderLoggerModule.kt b/packages/SystemUI/src-release/com/android/systemui/util/StartBinderLoggerModule.kt new file mode 100644 index 000000000000..6914c57dd915 --- /dev/null +++ b/packages/SystemUI/src-release/com/android/systemui/util/StartBinderLoggerModule.kt @@ -0,0 +1,24 @@ +/* + * 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 + +import dagger.Module + +@Module +abstract class StartBinderLoggerModule { + // Empty because this module is only used on debug builds +} diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt index a82f0e3e2a25..084295343bb0 100644 --- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt +++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt @@ -22,8 +22,6 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.content.Context import android.content.res.ColorStateList -import android.content.res.TypedArray -import android.graphics.Color import android.util.AttributeSet import android.view.View import com.android.app.animation.Interpolators @@ -41,12 +39,28 @@ open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : protected open val SHOW_DURATION_MILLIS = 150L protected open val HIDE_DURATION_MILLIS = 200L + override fun onFinishInflate() { + super.onFinishInflate() + mDefaultColorState = getColorInStyle() + } + + private fun getColorInStyle(): ColorStateList? { + val styledAttributes = + context.obtainStyledAttributes(styleResId, intArrayOf(android.R.attr.textColor)) + var colorStateList: ColorStateList? = null + if (styledAttributes != null) { + colorStateList = styledAttributes.getColorStateList(0) + } + styledAttributes.recycle() + return colorStateList + } + override fun updateTextColor() { var colorState = mDefaultColorState mNextMessageColorState?.defaultColor?.let { color -> if (color != DEFAULT_COLOR) { colorState = mNextMessageColorState - mNextMessageColorState = ColorStateList.valueOf(DEFAULT_COLOR) + mNextMessageColorState = mDefaultColorState ?: ColorStateList.valueOf(DEFAULT_COLOR) } } setTextColor(colorState) @@ -57,15 +71,12 @@ open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : } override fun onThemeChanged() { - val array: TypedArray = mContext.obtainStyledAttributes(intArrayOf(TITLE)) - val newTextColors: ColorStateList = ColorStateList.valueOf(array.getColor(0, Color.RED)) - array.recycle() - mDefaultColorState = newTextColors + mDefaultColorState = getColorInStyle() ?: Utils.getColorAttr(context, TITLE) super.onThemeChanged() } override fun reloadColor() { - mDefaultColorState = Utils.getColorAttr(context, TITLE) + mDefaultColorState = getColorInStyle() ?: Utils.getColorAttr(context, TITLE) super.reloadColor() } diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierText.java b/packages/SystemUI/src/com/android/keyguard/CarrierText.java index e4f6e131258e..87a9b0f17a1c 100644 --- a/packages/SystemUI/src/com/android/keyguard/CarrierText.java +++ b/packages/SystemUI/src/com/android/keyguard/CarrierText.java @@ -33,6 +33,8 @@ public class CarrierText extends TextView { private final boolean mShowAirplaneMode; + private final String mDebugLocation; + public CarrierText(Context context) { this(context, null); } @@ -46,6 +48,7 @@ public class CarrierText extends TextView { useAllCaps = a.getBoolean(R.styleable.CarrierText_allCaps, false); mShowAirplaneMode = a.getBoolean(R.styleable.CarrierText_showAirplaneMode, false); mShowMissingSim = a.getBoolean(R.styleable.CarrierText_showMissingSim, false); + mDebugLocation = a.getString(R.styleable.CarrierText_debugLocation); } finally { a.recycle(); } @@ -70,6 +73,10 @@ public class CarrierText extends TextView { return mShowMissingSim; } + public String getDebugLocation() { + return mDebugLocation; + } + private static class CarrierTextTransformationMethod extends SingleLineTransformationMethod { private final Locale mLocale; private final boolean mAllCaps; diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextController.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextController.java index 997c5275a2fa..33f9ecd03bca 100644 --- a/packages/SystemUI/src/com/android/keyguard/CarrierTextController.java +++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextController.java @@ -53,6 +53,7 @@ public class CarrierTextController extends ViewController<CarrierText> { mCarrierTextManager = carrierTextManagerBuilder .setShowAirplaneMode(mView.getShowAirplaneMode()) .setShowMissingSim(mView.getShowMissingSim()) + .setDebugLocationString(mView.getDebugLocation()) .build(); mKeyguardUpdateMonitor = keyguardUpdateMonitor; } diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java index b15378570358..a724514f6ec9 100644 --- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java +++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java @@ -16,6 +16,11 @@ package com.android.keyguard; +import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ACTIVE_DATA_SUB_CHANGED; +import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ON_SIM_STATE_CHANGED; +import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ON_TELEPHONY_CAPABLE; +import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_REFRESH_CARRIER_INFO; + import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -32,6 +37,7 @@ import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.keyguard.logging.CarrierTextManagerLogger; import com.android.settingslib.WirelessUtils; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; @@ -40,6 +46,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository; import com.android.systemui.telephony.TelephonyListenerManager; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -68,6 +75,7 @@ public class CarrierTextManager { private final AtomicBoolean mNetworkSupported = new AtomicBoolean(); @VisibleForTesting protected KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final CarrierTextManagerLogger mLogger; private final WifiRepository mWifiRepository; private final boolean[] mSimErrorState; private final int mSimSlotsNumber; @@ -97,19 +105,13 @@ public class CarrierTextManager { protected final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() { @Override public void onRefreshCarrierInfo() { - if (DEBUG) { - Log.d(TAG, "onRefreshCarrierInfo(), mTelephonyCapable: " - + Boolean.toString(mTelephonyCapable)); - } + mLogger.logUpdateCarrierTextForReason(REASON_REFRESH_CARRIER_INFO); updateCarrierText(); } @Override public void onTelephonyCapable(boolean capable) { - if (DEBUG) { - Log.d(TAG, "onTelephonyCapable() mTelephonyCapable: " - + Boolean.toString(capable)); - } + mLogger.logUpdateCarrierTextForReason(REASON_ON_TELEPHONY_CAPABLE); mTelephonyCapable = capable; updateCarrierText(); } @@ -121,7 +123,7 @@ public class CarrierTextManager { return; } - if (DEBUG) Log.d(TAG, "onSimStateChanged: " + getStatusForIccState(simState)); + mLogger.logUpdateCarrierTextForReason(REASON_ON_SIM_STATE_CHANGED); if (getStatusForIccState(simState) == CarrierTextManager.StatusMode.SimIoError) { mSimErrorState[slotId] = true; updateCarrierText(); @@ -137,6 +139,7 @@ public class CarrierTextManager { @Override public void onActiveDataSubscriptionIdChanged(int subId) { if (mNetworkSupported.get() && mCarrierTextCallback != null) { + mLogger.logUpdateCarrierTextForReason(REASON_ACTIVE_DATA_SUB_CHANGED); updateCarrierText(); } } @@ -175,7 +178,9 @@ public class CarrierTextManager { WakefulnessLifecycle wakefulnessLifecycle, @Main Executor mainExecutor, @Background Executor bgExecutor, - KeyguardUpdateMonitor keyguardUpdateMonitor) { + KeyguardUpdateMonitor keyguardUpdateMonitor, + CarrierTextManagerLogger logger) { + mContext = context; mIsEmergencyCallCapable = telephonyManager.isVoiceCapable(); @@ -191,6 +196,7 @@ public class CarrierTextManager { mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mLogger = logger; mBgExecutor.execute(() -> { boolean supported = mContext.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_TELEPHONY); @@ -315,7 +321,7 @@ public class CarrierTextManager { subOrderBySlot[i] = -1; } final CharSequence[] carrierNames = new CharSequence[numSubs]; - if (DEBUG) Log.d(TAG, "updateCarrierText(): " + numSubs); + mLogger.logUpdate(numSubs); for (int i = 0; i < numSubs; i++) { int subId = subs.get(i).getSubscriptionId(); @@ -325,9 +331,7 @@ public class CarrierTextManager { int simState = mKeyguardUpdateMonitor.getSimState(subId); CharSequence carrierName = subs.get(i).getCarrierName(); CharSequence carrierTextForSimState = getCarrierTextForSimState(simState, carrierName); - if (DEBUG) { - Log.d(TAG, "Handling (subId=" + subId + "): " + simState + " " + carrierName); - } + mLogger.logUpdateLoopStart(subId, simState, String.valueOf(carrierName)); if (carrierTextForSimState != null) { allSimsMissing = false; carrierNames[i] = carrierTextForSimState; @@ -340,9 +344,7 @@ public class CarrierTextManager { // Wi-Fi is disassociated or disabled if (ss.getRilDataRadioTechnology() != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN || mWifiRepository.isWifiConnectedWithValidSsid()) { - if (DEBUG) { - Log.d(TAG, "SIM ready and in service: subId=" + subId + ", ss=" + ss); - } + mLogger.logUpdateWfcCheck(); anySimReadyAndInService = true; } } @@ -379,7 +381,7 @@ public class CarrierTextManager { if (i.getBooleanExtra(TelephonyManager.EXTRA_SHOW_PLMN, false)) { plmn = i.getStringExtra(TelephonyManager.EXTRA_PLMN); } - if (DEBUG) Log.d(TAG, "Getting plmn/spn sticky brdcst " + plmn + "/" + spn); + mLogger.logUpdateFromStickyBroadcast(plmn, spn); if (Objects.equals(plmn, spn)) { text = plmn; } else { @@ -409,6 +411,7 @@ public class CarrierTextManager { !allSimsMissing, subsIds, airplaneMode); + mLogger.logCallbackSentFromUpdate(info); postToCallback(info); Trace.endSection(); } @@ -645,8 +648,10 @@ public class CarrierTextManager { private final Executor mMainExecutor; private final Executor mBgExecutor; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final CarrierTextManagerLogger mLogger; private boolean mShowAirplaneMode; private boolean mShowMissingSim; + private String mDebugLocation; @Inject public Builder( @@ -658,7 +663,8 @@ public class CarrierTextManager { WakefulnessLifecycle wakefulnessLifecycle, @Main Executor mainExecutor, @Background Executor bgExecutor, - KeyguardUpdateMonitor keyguardUpdateMonitor) { + KeyguardUpdateMonitor keyguardUpdateMonitor, + CarrierTextManagerLogger logger) { mContext = context; mSeparator = resources.getString( com.android.internal.R.string.kg_text_message_separator); @@ -669,6 +675,7 @@ public class CarrierTextManager { mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mLogger = logger; } /** */ @@ -683,14 +690,25 @@ public class CarrierTextManager { return this; } + /** + * To help disambiguate logs, set a location to be used in the LogBuffer calls, e.g.: + * "keyguard" or "keyguard emergency status bar" + */ + public Builder setDebugLocationString(String debugLocationString) { + mDebugLocation = debugLocationString; + return this; + } + /** Create a CarrierTextManager. */ public CarrierTextManager build() { + mLogger.setLocation(mDebugLocation); return new CarrierTextManager( mContext, mSeparator, mShowAirplaneMode, mShowMissingSim, mWifiRepository, mTelephonyManager, mTelephonyListenerManager, mWakefulnessLifecycle, - mMainExecutor, mBgExecutor, mKeyguardUpdateMonitor); + mMainExecutor, mBgExecutor, mKeyguardUpdateMonitor, mLogger); } } + /** * Data structure for passing information to CarrierTextController subscribers */ @@ -716,6 +734,17 @@ public class CarrierTextManager { this.subscriptionIds = subscriptionIds; this.airplaneMode = airplaneMode; } + + @Override + public String toString() { + return "CarrierTextCallbackInfo{" + + "carrierText=" + carrierText + + ", listOfCarriers=" + Arrays.toString(listOfCarriers) + + ", anySimReady=" + anySimReady + + ", subscriptionIds=" + Arrays.toString(subscriptionIds) + + ", airplaneMode=" + airplaneMode + + '}'; + } } /** diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 4bf7be6df5be..a8f280429105 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -21,13 +21,11 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.res.Resources -import android.graphics.Rect import android.text.format.DateFormat import android.util.TypedValue import android.view.View import android.view.View.OnAttachStateChangeListener import android.view.ViewTreeObserver -import android.widget.FrameLayout import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle @@ -55,16 +53,16 @@ import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor -import java.util.Locale -import java.util.TimeZone -import java.util.concurrent.Executor -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch +import java.util.Locale +import java.util.TimeZone +import java.util.concurrent.Executor +import javax.inject.Inject /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by @@ -98,16 +96,12 @@ constructor( value.initialize(resources, dozeAmount, 0f) - if (regionSamplingEnabled) { - clock?.run { - smallClock.view.addOnLayoutChangeListener(mLayoutChangedListener) - largeClock.view.addOnLayoutChangeListener(mLayoutChangedListener) - } - } else { + if (!regionSamplingEnabled) { updateColors() } updateFontSizes() updateTimeListeners() + cachedWeatherData?.let { value.events.onWeatherDataChanged(it) } value.smallClock.view.addOnAttachStateChangeListener( object : OnAttachStateChangeListener { override fun onViewAttachedToWindow(p0: View?) { @@ -139,44 +133,10 @@ constructor( private var disposableHandle: DisposableHandle? = null private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING) - private val mLayoutChangedListener = - object : View.OnLayoutChangeListener { - - override fun onLayoutChange( - view: View?, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - view?.removeOnLayoutChangeListener(this) - - val parent = (view?.parent) as FrameLayout - - // don't pass in negative bounds when clocks are in transition state - if (view.locationOnScreen[0] < 0 || view.locationOnScreen[1] < 0) { - return - } - - val currentViewRect = Rect(left, top, right, bottom) - val oldViewRect = Rect(oldLeft, oldTop, oldRight, oldBottom) - - if ( - currentViewRect.width() != oldViewRect.width() || - currentViewRect.height() != oldViewRect.height() - ) { - updateRegionSampler(view) - } - } - } private fun updateColors() { val wallpaperManager = WallpaperManager.getInstance(context) - if (regionSamplingEnabled && !wallpaperManager.lockScreenWallpaperExists()) { + if (regionSamplingEnabled) { regionSampler?.let { regionSampler -> clock?.let { clock -> if (regionSampler.sampledView == clock.smallClock.view) { @@ -211,6 +171,7 @@ constructor( mainExecutor, bgExecutor, regionSamplingEnabled, + isLockscreen = true, ::updateColors ) ?.apply { startRegionSampler() } @@ -219,10 +180,11 @@ constructor( } protected open fun createRegionSampler( - sampledView: View?, + sampledView: View, mainExecutor: Executor?, bgExecutor: Executor?, regionSamplingEnabled: Boolean, + isLockscreen: Boolean, updateColors: () -> Unit ): RegionSampler? { return RegionSampler( @@ -230,8 +192,8 @@ constructor( mainExecutor, bgExecutor, regionSamplingEnabled, - updateColors - ) + isLockscreen, + ) { updateColors() } } var regionSampler: RegionSampler? = null @@ -239,6 +201,7 @@ constructor( var largeTimeListener: TimeListener? = null val shouldTimeListenerRun: Boolean get() = isKeyguardVisible && dozeAmount < DOZE_TICKRATE_THRESHOLD + private var cachedWeatherData: WeatherData? = null private var smallClockIsDark = true private var largeClockIsDark = true @@ -305,6 +268,7 @@ constructor( } override fun onWeatherDataChanged(data: WeatherData) { + cachedWeatherData = data clock?.run { events.onWeatherDataChanged(data) } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java index b18abf3fd02c..c6d147108611 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java @@ -22,6 +22,8 @@ import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.View; +import androidx.annotation.CallSuper; + import com.android.internal.widget.LockscreenCredential; import com.android.systemui.R; @@ -48,7 +50,9 @@ public abstract class KeyguardAbsKeyInputView extends KeyguardInputView { protected abstract void resetState(); @Override + @CallSuper protected void onFinishInflate() { + super.onFinishInflate(); mEcaView = findViewById(R.id.keyguard_selector_fade_container); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index 510fcbfd8bee..e057188851f6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -37,6 +37,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.classifier.FalsingClassifier; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.flags.FeatureFlags; import java.util.HashMap; import java.util.Map; @@ -77,9 +78,10 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey KeyguardSecurityCallback keyguardSecurityCallback, KeyguardMessageAreaController.Factory messageAreaControllerFactory, LatencyTracker latencyTracker, FalsingCollector falsingCollector, - EmergencyButtonController emergencyButtonController) { + EmergencyButtonController emergencyButtonController, + FeatureFlags featureFlags) { super(view, securityMode, keyguardSecurityCallback, emergencyButtonController, - messageAreaControllerFactory); + messageAreaControllerFactory, featureFlags); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mLatencyTracker = latencyTracker; @@ -179,10 +181,10 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey handleAttemptLockout(deadline); } } + mView.resetPasswordText(true /* animate */, false /* announce deletion if no match */); if (timeoutMs == 0) { mMessageAreaController.setMessage(mView.getWrongPasswordStringId()); } - mView.resetPasswordText(true /* animate */, false /* announce deletion if no match */); startErrorAnimation(); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt new file mode 100644 index 000000000000..635f0fa44234 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt @@ -0,0 +1,52 @@ +package com.android.keyguard + +import android.content.Context +import android.graphics.Canvas +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout + +class KeyguardClockFrame( + context: Context, + attrs: AttributeSet, +) : FrameLayout(context, attrs) { + private var drawAlpha: Int = 255 + + protected override fun onSetAlpha(alpha: Int): Boolean { + drawAlpha = alpha + return true + } + + protected override fun dispatchDraw(canvas: Canvas) { + saveCanvasAlpha(this, canvas, drawAlpha) { super.dispatchDraw(it) } + } + + companion object { + @JvmStatic + fun saveCanvasAlpha(view: View, canvas: Canvas, alpha: Int, drawFunc: (Canvas) -> Unit) { + if (alpha <= 0) { + // Zero Alpha -> skip drawing phase + return + } + + if (alpha >= 255) { + // Max alpha -> no need for layer + drawFunc(canvas) + return + } + + // Find x & y of view on screen + var (x, y) = + run { + val locationOnScreen = IntArray(2) + view.getLocationOnScreen(locationOnScreen) + Pair(locationOnScreen[0].toFloat(), locationOnScreen[1].toFloat()) + } + + val restoreTo = + canvas.saveLayerAlpha(-1f * x, -1f * y, x + view.width, y + view.height, alpha) + drawFunc(canvas) + canvas.restoreToCount(restoreTo) + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 644a9bcdd588..e54d4739dc97 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -5,11 +5,11 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; +import android.graphics.Canvas; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; import android.widget.RelativeLayout; import androidx.annotation.IntDef; @@ -69,12 +69,13 @@ public class KeyguardClockSwitch extends RelativeLayout { /** * Frame for small/large clocks */ - private FrameLayout mSmallClockFrame; - private FrameLayout mLargeClockFrame; + private KeyguardClockFrame mSmallClockFrame; + private KeyguardClockFrame mLargeClockFrame; private ClockController mClock; private View mStatusArea; private int mSmartspaceTopOffset; + private int mDrawAlpha = 255; /** * Maintain state so that a newly connected plugin can be initialized. @@ -121,6 +122,22 @@ public class KeyguardClockSwitch extends RelativeLayout { onDensityOrFontScaleChanged(); } + @Override + protected boolean onSetAlpha(int alpha) { + mDrawAlpha = alpha; + return true; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + KeyguardClockFrame.saveCanvasAlpha( + this, canvas, mDrawAlpha, + c -> { + super.dispatchDraw(c); + return kotlin.Unit.INSTANCE; + }); + } + public void setLogBuffer(LogBuffer logBuffer) { mLogBuffer = logBuffer; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 676f342775ef..d8bf570954df 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -179,20 +179,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } /** - * Set alpha directly to mView will clip clock, so we set alpha to clock face instead - */ - public void setAlpha(float alpha) { - ClockController clock = getClock(); - if (clock != null) { - clock.getLargeClock().getView().setAlpha(alpha); - clock.getSmallClock().getView().setAlpha(alpha); - } - if (mStatusArea != null) { - mStatusArea.setAlpha(alpha); - } - } - - /** * Attach the controller to the view it relates to. */ @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java index ec8fa921c6fa..9f21a31bd2c0 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -19,27 +19,34 @@ import android.app.Presentation; import android.content.Context; import android.graphics.Color; import android.graphics.Rect; +import android.hardware.devicestate.DeviceStateManager; import android.hardware.display.DisplayManager; import android.media.MediaRouter; import android.media.MediaRouter.RouteInfo; import android.os.Bundle; import android.os.Trace; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.view.Display; +import android.view.DisplayAddress; import android.view.DisplayInfo; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.dagger.KeyguardStatusViewComponent; import com.android.systemui.R; +import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.settings.DisplayTracker; +import com.android.systemui.statusbar.policy.KeyguardStateController; import java.util.concurrent.Executor; @@ -47,6 +54,7 @@ import javax.inject.Inject; import dagger.Lazy; +@SysUISingleton public class KeyguardDisplayManager { protected static final String TAG = "KeyguardDisplayManager"; private static final boolean DEBUG = KeyguardConstants.DEBUG; @@ -61,6 +69,9 @@ public class KeyguardDisplayManager { private boolean mShowing; private final DisplayInfo mTmpDisplayInfo = new DisplayInfo(); + private final DeviceStateHelper mDeviceStateHelper; + private final KeyguardStateController mKeyguardStateController; + private final SparseArray<Presentation> mPresentations = new SparseArray<>(); private final DisplayTracker.Callback mDisplayCallback = @@ -92,7 +103,9 @@ public class KeyguardDisplayManager { KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory, DisplayTracker displayTracker, @Main Executor mainExecutor, - @UiBackground Executor uiBgExecutor) { + @UiBackground Executor uiBgExecutor, + DeviceStateHelper deviceStateHelper, + KeyguardStateController keyguardStateController) { mContext = context; mNavigationBarControllerLazy = navigationBarControllerLazy; mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory; @@ -100,6 +113,8 @@ public class KeyguardDisplayManager { mDisplayService = mContext.getSystemService(DisplayManager.class); mDisplayTracker = displayTracker; mDisplayTracker.addDisplayChangeCallback(mDisplayCallback, mainExecutor); + mDeviceStateHelper = deviceStateHelper; + mKeyguardStateController = keyguardStateController; } private boolean isKeyguardShowable(Display display) { @@ -122,6 +137,18 @@ public class KeyguardDisplayManager { } return false; } + if (mKeyguardStateController.isOccluded() + && mDeviceStateHelper.isConcurrentDisplayActive(display)) { + if (DEBUG) { + // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the + // Keyguard state becomes "occluded". In this case, we should not show the + // KeyguardPresentation, since the activity is presenting content onto the + // non-default display. + Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent" + + " display is active"); + } + return false; + } return true; } @@ -260,6 +287,53 @@ public class KeyguardDisplayManager { } + /** + * Helper used to receive device state info from {@link DeviceStateManager}. + */ + static class DeviceStateHelper implements DeviceStateManager.DeviceStateCallback { + + @Nullable + private final DisplayAddress.Physical mRearDisplayPhysicalAddress; + + // TODO(b/271317597): These device states should be defined in DeviceStateManager + private final int mConcurrentState; + private boolean mIsInConcurrentDisplayState; + + @Inject + DeviceStateHelper(Context context, + DeviceStateManager deviceStateManager, + @Main Executor mainExecutor) { + + final String rearDisplayPhysicalAddress = context.getResources().getString( + com.android.internal.R.string.config_rearDisplayPhysicalAddress); + if (TextUtils.isEmpty(rearDisplayPhysicalAddress)) { + mRearDisplayPhysicalAddress = null; + } else { + mRearDisplayPhysicalAddress = DisplayAddress + .fromPhysicalDisplayId(Long.parseLong(rearDisplayPhysicalAddress)); + } + + mConcurrentState = context.getResources().getInteger( + com.android.internal.R.integer.config_deviceStateConcurrentRearDisplay); + deviceStateManager.registerCallback(mainExecutor, this); + } + + @Override + public void onStateChanged(int state) { + // When concurrent state ends, the display also turns off. This is enforced in various + // ExtensionRearDisplayPresentationTest CTS tests. So, we don't need to invoke + // hide() since that will happen through the onDisplayRemoved callback. + mIsInConcurrentDisplayState = state == mConcurrentState; + } + + boolean isConcurrentDisplayActive(Display display) { + return mIsInConcurrentDisplayState + && mRearDisplayPhysicalAddress != null + && mRearDisplayPhysicalAddress.equals(display.getAddress()); + } + } + + @VisibleForTesting static final class KeyguardPresentation extends Presentation { private static final int VIDEO_SAFE_REGION = 80; // Percentage of display width & height diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java index 7eae7295547f..c9d906911dbc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java @@ -21,11 +21,14 @@ import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.View; import android.widget.LinearLayout; +import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import com.android.internal.jank.InteractionJankMonitor; +import com.android.systemui.R; /** * A Base class for all Keyguard password/pattern/pin related inputs. @@ -33,6 +36,9 @@ import com.android.internal.jank.InteractionJankMonitor; public abstract class KeyguardInputView extends LinearLayout { private Runnable mOnFinishImeAnimationRunnable; + @Nullable + private View mBouncerMessageView; + public KeyguardInputView(Context context) { super(context); } @@ -87,6 +93,18 @@ public abstract class KeyguardInputView extends LinearLayout { mOnFinishImeAnimationRunnable = onFinishImeAnimationRunnable; } + @Override + @CallSuper + protected void onFinishInflate() { + super.onFinishInflate(); + mBouncerMessageView = findViewById(R.id.bouncer_message_view); + } + + @Nullable + public final View getBouncerMessageView() { + return mBouncerMessageView; + } + public void runOnFinishImeAnimationRunnable() { if (mOnFinishImeAnimationRunnable != null) { mOnFinishImeAnimationRunnable.run(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index a0f5f3451e94..3c05299f7a95 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -16,6 +16,7 @@ package com.android.keyguard; +import android.annotation.CallSuper; import android.annotation.Nullable; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -31,6 +32,7 @@ import com.android.systemui.R; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.util.ViewController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -53,16 +55,19 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> // (e.g. face unlock). This avoids unwanted asynchronous events from messing with the // state for the current security method. private KeyguardSecurityCallback mNullCallback = new KeyguardSecurityCallback() {}; + private final FeatureFlags mFeatureFlags; protected KeyguardInputViewController(T view, SecurityMode securityMode, KeyguardSecurityCallback keyguardSecurityCallback, EmergencyButtonController emergencyButtonController, - @Nullable KeyguardMessageAreaController.Factory messageAreaControllerFactory) { + @Nullable KeyguardMessageAreaController.Factory messageAreaControllerFactory, + FeatureFlags featureFlags) { super(view); mSecurityMode = securityMode; mKeyguardSecurityCallback = keyguardSecurityCallback; mEmergencyButton = view == null ? null : view.findViewById(R.id.emergency_call_button); mEmergencyButtonController = emergencyButtonController; + mFeatureFlags = featureFlags; if (messageAreaControllerFactory != null) { try { BouncerKeyguardMessageArea kma = view.requireViewById(R.id.bouncer_message_area); @@ -82,9 +87,21 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> } @Override + @CallSuper protected void onViewAttached() { + updateMessageAreaVisibility(); } + private void updateMessageAreaVisibility() { + if (mMessageAreaController == null) return; + if (mFeatureFlags.isEnabled(Flags.REVAMPED_BOUNCER_MESSAGES)) { + mMessageAreaController.disable(); + } else { + mMessageAreaController.setIsVisible(true); + } + } + + @Override protected void onViewDetached() { } @@ -208,14 +225,14 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mLatencyTracker, mFalsingCollector, emergencyButtonController, mMessageAreaControllerFactory, - mDevicePostureController); + mDevicePostureController, mFeatureFlags); } else if (keyguardInputView instanceof KeyguardPasswordView) { return new KeyguardPasswordViewController((KeyguardPasswordView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mInputMethodManager, emergencyButtonController, mMainExecutor, mResources, - mFalsingCollector, mKeyguardViewController); - + mFalsingCollector, mKeyguardViewController, + mFeatureFlags); } else if (keyguardInputView instanceof KeyguardPINView) { return new KeyguardPinViewController((KeyguardPINView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, @@ -227,13 +244,13 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, - emergencyButtonController); + emergencyButtonController, mFeatureFlags); } else if (keyguardInputView instanceof KeyguardSimPukView) { return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, - emergencyButtonController); + emergencyButtonController, mFeatureFlags); } throw new RuntimeException("Unable to find controller for " + keyguardInputView); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java index 553453d7f79d..fc66527998b9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java @@ -44,11 +44,18 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe private ViewGroup mContainer; private int mTopMargin; protected boolean mAnimate; + private final int mStyleResId; + private boolean mIsDisabled = false; public KeyguardMessageArea(Context context, AttributeSet attrs) { super(context, attrs); setLayerType(LAYER_TYPE_HARDWARE, null); // work around nested unclipped SaveLayer bug - + if (attrs != null) { + mStyleResId = attrs.getStyleAttribute(); + } else { + // Set to default reference style if the component is used without setting "style" attr + mStyleResId = R.style.Keyguard_TextView; + } onThemeChanged(); } @@ -82,13 +89,17 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe } void onDensityOrFontScaleChanged() { - TypedArray array = mContext.obtainStyledAttributes(R.style.Keyguard_TextView, new int[] { + TypedArray array = mContext.obtainStyledAttributes(getStyleResId(), new int[] { android.R.attr.textSize }); setTextSize(TypedValue.COMPLEX_UNIT_PX, array.getDimensionPixelSize(0, 0)); array.recycle(); } + protected int getStyleResId() { + return mStyleResId; + } + @Override public void setMessage(CharSequence msg, boolean animate) { if (!TextUtils.isEmpty(msg)) { @@ -118,6 +129,10 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe } void update() { + if (mIsDisabled) { + setVisibility(GONE); + return; + } CharSequence status = mMessage; setVisibility(TextUtils.isEmpty(status) || (!mIsVisible) ? INVISIBLE : VISIBLE); setText(status); @@ -136,4 +151,17 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe /** Set the text color */ protected abstract void updateTextColor(); + + /** + * Mark this view with {@link android.view.View#GONE} visibility to remove this from the layout + * of the view. Any calls to {@link #setIsVisible(boolean)} after this will be a no-op. + */ + public void disable() { + mIsDisabled = true; + update(); + } + + public boolean isDisabled() { + return mIsDisabled; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index c1896fc641e0..0332c9f57136 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -18,7 +18,9 @@ package com.android.keyguard; import android.content.res.ColorStateList; import android.content.res.Configuration; +import android.text.Editable; import android.text.TextUtils; +import android.text.TextWatcher; import android.view.View; import androidx.annotation.VisibleForTesting; @@ -45,6 +47,31 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final ConfigurationController mConfigurationController; private final AnnounceRunnable mAnnounceRunnable; + private final TextWatcher mTextWatcher = new TextWatcher() { + @Override + public void afterTextChanged(Editable editable) { + CharSequence msg = editable; + if (!TextUtils.isEmpty(msg)) { + mView.removeCallbacks(mAnnounceRunnable); + mAnnounceRunnable.setTextToAnnounce(msg); + mView.postDelayed(() -> { + if (msg == mView.getText()) { + mAnnounceRunnable.run(); + } + }, ANNOUNCEMENT_DELAY); + } + } + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + /* no-op */ + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + /* no-op */ + } + }; private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { public void onFinishedGoingToSleep(int why) { @@ -89,12 +116,14 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mKeyguardUpdateMonitor.registerCallback(mInfoCallback); mView.setSelected(mKeyguardUpdateMonitor.isDeviceInteractive()); mView.onThemeChanged(); + mView.addTextChangedListener(mTextWatcher); } @Override protected void onViewDetached() { mConfigurationController.removeCallback(mConfigurationListener); mKeyguardUpdateMonitor.removeCallback(mInfoCallback); + mView.removeTextChangedListener(mTextWatcher); } /** @@ -104,6 +133,14 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mView.setIsVisible(isVisible); } + /** + * Mark this view with {@link View#GONE} visibility to remove this from the layout of the view. + * Any calls to {@link #setIsVisible(boolean)} after this will be a no-op. + */ + public void disable() { + mView.disable(); + } + public void setMessage(CharSequence s) { setMessage(s, true); } @@ -112,13 +149,10 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> * Sets a message to the underlying text view. */ public void setMessage(CharSequence s, boolean animate) { - mView.setMessage(s, animate); - CharSequence msg = mView.getText(); - if (!TextUtils.isEmpty(msg)) { - mView.removeCallbacks(mAnnounceRunnable); - mAnnounceRunnable.setTextToAnnounce(msg); - mView.postDelayed(mAnnounceRunnable, ANNOUNCEMENT_DELAY); + if (mView.isDisabled()) { + return; } + mView.setMessage(s, animate); } public void setMessage(int resId) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 58807e4bf3bd..2377057f1fc5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -51,7 +51,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { private View[][] mViews; private int mYTrans; private int mYTransOffset; - private View mBouncerMessageView; + private View mBouncerMessageArea; @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN; public static final long ANIMATION_DURATION = 650; @@ -145,7 +145,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { super.onFinishInflate(); mContainer = findViewById(R.id.pin_container); - mBouncerMessageView = findViewById(R.id.bouncer_message_area); + mBouncerMessageArea = findViewById(R.id.bouncer_message_area); mViews = new View[][]{ new View[]{ findViewById(R.id.row0), null, null @@ -221,9 +221,9 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { Interpolator legacyDecelerate = Interpolators.LEGACY_DECELERATE; float standardProgress = standardDecelerate.getInterpolation(progress); - mBouncerMessageView.setTranslationY( + mBouncerMessageArea.setTranslationY( mYTrans - mYTrans * standardProgress); - mBouncerMessageView.setAlpha(standardProgress); + mBouncerMessageArea.setAlpha(standardProgress); for (int i = 0; i < mViews.length; i++) { View[] row = mViews[i]; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java index d221e22a4fcd..1f6b09b91b87 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java @@ -40,6 +40,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.List; @@ -104,10 +105,11 @@ public class KeyguardPasswordViewController @Main DelayableExecutor mainExecutor, @Main Resources resources, FalsingCollector falsingCollector, - KeyguardViewController keyguardViewController) { + KeyguardViewController keyguardViewController, + FeatureFlags featureFlags) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, falsingCollector, - emergencyButtonController); + emergencyButtonController, featureFlags); mKeyguardSecurityCallback = keyguardSecurityCallback; mInputMethodManager = inputMethodManager; mMainExecutor = mainExecutor; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 39225fb03939..64b1c502b2c3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -38,6 +38,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.classifier.FalsingClassifier; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.statusbar.policy.DevicePostureController; import java.util.HashMap; @@ -196,9 +197,9 @@ public class KeyguardPatternViewController FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, KeyguardMessageAreaController.Factory messageAreaControllerFactory, - DevicePostureController postureController) { + DevicePostureController postureController, FeatureFlags featureFlags) { super(view, securityMode, keyguardSecurityCallback, emergencyButtonController, - messageAreaControllerFactory); + messageAreaControllerFactory, featureFlags); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mLatencyTracker = latencyTracker; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index b4ddc9a975c2..5cb2c5c62d6c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -34,6 +34,8 @@ import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; +import androidx.annotation.CallSuper; + import com.android.app.animation.Interpolators; import com.android.internal.widget.LockscreenCredential; import com.android.systemui.R; @@ -82,9 +84,6 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView protected void setPasswordEntryInputEnabled(boolean enabled) { mPasswordEntry.setEnabled(enabled); mOkButton.setEnabled(enabled); - if (enabled && !mPasswordEntry.hasFocus()) { - mPasswordEntry.requestFocus(); - } } @Override @@ -150,7 +149,9 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView } @Override + @CallSuper protected void onFinishInflate() { + super.onFinishInflate(); mPasswordEntry = findViewById(getPasswordTextViewId()); // Set selected property on so the view can send accessibility events. diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index ded1238742fb..31cbdde3452d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -27,6 +27,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.flags.FeatureFlags; public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinBasedInputView> extends KeyguardAbsKeyInputViewController<T> { @@ -58,10 +59,11 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, EmergencyButtonController emergencyButtonController, - FalsingCollector falsingCollector) { + FalsingCollector falsingCollector, + FeatureFlags featureFlags) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, falsingCollector, - emergencyButtonController); + emergencyButtonController, featureFlags); mLiftToActivateListener = liftToActivateListener; mFalsingCollector = falsingCollector; mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId()); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index f23bb0ae11f6..3e16d559742d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -16,8 +16,6 @@ package com.android.keyguard; -import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.DEFAULT_PIN_LENGTH; - import android.view.View; import com.android.internal.util.LatencyTracker; @@ -42,10 +40,9 @@ public class KeyguardPinViewController private NumPadButton mBackspaceKey; private View mOkButton = mView.findViewById(R.id.key_enter); - private int mUserId; private long mPinLength; - private int mPasswordFailedAttempts; + private boolean mDisabledAutoConfirmation; protected KeyguardPinViewController(KeyguardPINView view, KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -59,12 +56,13 @@ public class KeyguardPinViewController FeatureFlags featureFlags) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, - emergencyButtonController, falsingCollector); + emergencyButtonController, falsingCollector, featureFlags); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mPostureController = postureController; mLockPatternUtils = lockPatternUtils; mFeatureFlags = featureFlags; mBackspaceKey = view.findViewById(R.id.delete_button); + mPinLength = mLockPatternUtils.getPinLength(KeyguardUpdateMonitor.getCurrentUser()); } @Override @@ -84,9 +82,8 @@ public class KeyguardPinViewController protected void onUserInput() { super.onUserInput(); - if (isAutoConfirmation()) { - updateOKButtonVisibility(); - updateBackSpaceVisibility(); + if (isAutoPinConfirmEnabledInSettings()) { + updateAutoConfirmationState(); if (mPasswordEntry.getText().length() == mPinLength && mOkButton.getVisibility() == View.INVISIBLE) { verifyPasswordAndUnlock(); @@ -103,13 +100,8 @@ public class KeyguardPinViewController @Override public void startAppearAnimation() { if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) { - mUserId = KeyguardUpdateMonitor.getCurrentUser(); - mPinLength = mLockPatternUtils.getPinLength(mUserId); - mBackspaceKey.setTransparentMode(/* isTransparentMode= */ isAutoConfirmation()); - updateOKButtonVisibility(); - updateBackSpaceVisibility(); mPasswordEntry.setUsePinShapes(true); - mPasswordEntry.setIsPinHinting(isAutoConfirmation() && isPinHinting()); + updateAutoConfirmationState(); } super.startAppearAnimation(); } @@ -120,13 +112,25 @@ public class KeyguardPinViewController mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable); } + @Override + protected void handleAttemptLockout(long elapsedRealtimeDeadline) { + super.handleAttemptLockout(elapsedRealtimeDeadline); + updateAutoConfirmationState(); + } + + private void updateAutoConfirmationState() { + mDisabledAutoConfirmation = mLockPatternUtils.getCurrentFailedPasswordAttempts( + KeyguardUpdateMonitor.getCurrentUser()) >= MIN_FAILED_PIN_ATTEMPTS; + updateOKButtonVisibility(); + updateBackSpaceVisibility(); + updatePinHinting(); + } /** * Updates the visibility of the OK button for auto confirm feature */ private void updateOKButtonVisibility() { - mPasswordFailedAttempts = mLockPatternUtils.getCurrentFailedPasswordAttempts(mUserId); - if (isAutoConfirmation() && mPasswordFailedAttempts < MIN_FAILED_PIN_ATTEMPTS) { + if (isAutoPinConfirmEnabledInSettings() && !mDisabledAutoConfirmation) { mOkButton.setVisibility(View.INVISIBLE); } else { mOkButton.setVisibility(View.VISIBLE); @@ -134,33 +138,41 @@ public class KeyguardPinViewController } /** - * Updates the visibility and the enabled state of the backspace. + * Updates the visibility and the enabled state of the backspace. * Visibility changes are only for auto confirmation configuration. */ private void updateBackSpaceVisibility() { - if (!isAutoConfirmation()) { - return; - } - - if (mPasswordEntry.getText().length() > 0) { - mBackspaceKey.setVisibility(View.VISIBLE); - } else { - mBackspaceKey.setVisibility(View.INVISIBLE); + boolean isAutoConfirmation = isAutoPinConfirmEnabledInSettings(); + mBackspaceKey.setTransparentMode(/* isTransparentMode= */ + isAutoConfirmation && !mDisabledAutoConfirmation); + if (isAutoConfirmation) { + if (mPasswordEntry.getText().length() > 0 + || mDisabledAutoConfirmation) { + mBackspaceKey.setVisibility(View.VISIBLE); + } else { + mBackspaceKey.setVisibility(View.INVISIBLE); + } } } + /** Updates whether to use pin hinting or not. */ + void updatePinHinting() { + mPasswordEntry.setIsPinHinting(isAutoPinConfirmEnabledInSettings() && isPinHinting() + && !mDisabledAutoConfirmation); + } /** - * Responsible for identifying if PIN hinting is to be enabled or not + * Responsible for identifying if PIN hinting is to be enabled or not */ private boolean isPinHinting() { - return mLockPatternUtils.getPinLength(mUserId) == DEFAULT_PIN_LENGTH; + return mLockPatternUtils.getPinLength(KeyguardUpdateMonitor.getCurrentUser()) + == DEFAULT_PIN_LENGTH; } /** - * Responsible for identifying if auto confirm is enabled or not in Settings + * Responsible for identifying if auto confirm is enabled or not in Settings */ - private boolean isAutoConfirmation() { + private boolean isAutoPinConfirmEnabledInSettings() { //Checks if user has enabled the auto confirm in Settings - return mLockPatternUtils.isAutoPinConfirmEnabled(mUserId); + return mLockPatternUtils.isAutoPinConfirmEnabled(KeyguardUpdateMonitor.getCurrentUser()); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 5cc0547b5bde..4b021716f506 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -599,7 +599,6 @@ public class KeyguardSecurityContainer extends ConstraintLayout { */ public void startAppearAnimation(SecurityMode securityMode) { setTranslationY(0f); - setAlpha(1f); updateChildren(0 /* translationY */, 1f /* alpha */); mViewMode.startAppearAnimation(securityMode); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 1cbcb9d56566..b5e54209dab2 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -257,14 +257,14 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard * Authentication has happened and it's time to dismiss keyguard. This function * should clean up and inform KeyguardViewMediator. * - * @param strongAuth whether the user has authenticated with strong authentication like + * @param fromPrimaryAuth whether the user has authenticated with primary auth like * pattern, password or PIN but not by trust agents or fingerprint * @param targetUserId a user that needs to be the foreground user at the dismissal * completion. */ @Override - public void finish(boolean strongAuth, int targetUserId) { - if (!mKeyguardStateController.canDismissLockScreen() && !strongAuth) { + public void finish(boolean fromPrimaryAuth, int targetUserId) { + if (!mKeyguardStateController.canDismissLockScreen() && !fromPrimaryAuth) { Log.e(TAG, "Tried to dismiss keyguard when lockscreen is not dismissible and user " + "was not authenticated with a primary security method " @@ -283,9 +283,9 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } if (mViewMediatorCallback != null) { if (deferKeyguardDone) { - mViewMediatorCallback.keyguardDonePending(strongAuth, targetUserId); + mViewMediatorCallback.keyguardDonePending(fromPrimaryAuth, targetUserId); } else { - mViewMediatorCallback.keyguardDone(strongAuth, targetUserId); + mViewMediatorCallback.keyguardDone(fromPrimaryAuth, targetUserId); } } } @@ -603,8 +603,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard /** * Dismiss keyguard due to a user unlock event. */ - public void finish(boolean strongAuth, int currentUser) { - mKeyguardSecurityCallback.finish(strongAuth, currentUser); + public void finish(boolean primaryAuth, int currentUser) { + mKeyguardSecurityCallback.finish(primaryAuth, currentUser); } /** @@ -674,7 +674,6 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard public void startAppearAnimation() { if (mCurrentSecurityMode != SecurityMode.None) { - setAlpha(1f); mView.startAppearAnimation(mCurrentSecurityMode); getCurrentSecurityController(controller -> controller.startAppearAnimation()); } @@ -736,7 +735,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } boolean finish = false; - boolean strongAuth = false; + boolean primaryAuth = false; int eventSubtype = -1; BouncerUiEvent uiEvent = BouncerUiEvent.UNKNOWN; if (mUpdateMonitor.getUserHasTrust(targetUserId)) { @@ -761,7 +760,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard case Pattern: case Password: case PIN: - strongAuth = true; + primaryAuth = true; finish = true; eventSubtype = BOUNCER_DISMISS_PASSWORD; uiEvent = BouncerUiEvent.BOUNCER_DISMISS_PASSWORD; @@ -805,7 +804,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mUiEventLogger.log(uiEvent, getSessionId()); } if (finish) { - mKeyguardSecurityCallback.finish(strongAuth, targetUserId); + mKeyguardSecurityCallback.finish(primaryAuth, targetUserId); } return finish; } @@ -1112,7 +1111,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard */ public void setExpansion(float fraction) { float scaledFraction = BouncerPanelExpansionCalculator.showBouncerProgress(fraction); - mView.setAlpha(MathUtils.constrain(1 - scaledFraction, 0f, 1f)); + setAlpha(MathUtils.constrain(1 - scaledFraction, 0f, 1f)); mView.setTranslationY(scaledFraction * mTranslationY); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt index 3fc39afe5d0f..1461dbef5049 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimInputView.kt @@ -30,6 +30,7 @@ abstract class KeyguardSimInputView(context: Context, attrs: AttributeSet) : private var disableESimButton: KeyguardEsimArea? = null override fun onFinishInflate() { + super.onFinishInflate() simImageView = findViewById(R.id.keyguard_sim) disableESimButton = findViewById(R.id.keyguard_esim_area) super.onFinishInflate() diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java index a16f30475654..61280affc29e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java @@ -41,6 +41,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.flags.FeatureFlags; public class KeyguardSimPinViewController extends KeyguardPinBasedInputViewController<KeyguardSimPinView> { @@ -81,10 +82,10 @@ public class KeyguardSimPinViewController KeyguardMessageAreaController.Factory messageAreaControllerFactory, LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, TelephonyManager telephonyManager, FalsingCollector falsingCollector, - EmergencyButtonController emergencyButtonController) { + EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, - emergencyButtonController, falsingCollector); + emergencyButtonController, falsingCollector, featureFlags); 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 e9405eb79901..49d786fdddd8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -38,6 +38,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.flags.FeatureFlags; public class KeyguardSimPukViewController extends KeyguardPinBasedInputViewController<KeyguardSimPukView> { @@ -85,10 +86,10 @@ public class KeyguardSimPukViewController KeyguardMessageAreaController.Factory messageAreaControllerFactory, LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, TelephonyManager telephonyManager, FalsingCollector falsingCollector, - EmergencyButtonController emergencyButtonController) { + EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, - emergencyButtonController, falsingCollector); + emergencyButtonController, falsingCollector, featureFlags); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusContainer.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusContainer.kt new file mode 100644 index 000000000000..298eff27060b --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusContainer.kt @@ -0,0 +1,22 @@ +package com.android.keyguard + +import android.content.Context +import android.graphics.Canvas +import android.util.AttributeSet +import android.widget.LinearLayout + +class KeyguardStatusContainer( + context: Context, + attrs: AttributeSet, +) : LinearLayout(context, attrs) { + private var drawAlpha: Int = 255 + + protected override fun onSetAlpha(alpha: Int): Boolean { + drawAlpha = alpha + return true + } + + protected override fun dispatchDraw(canvas: Canvas) { + KeyguardClockFrame.saveCanvasAlpha(this, canvas, drawAlpha) { super.dispatchDraw(canvas) } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java index 23136097f41f..a6252a39ee8a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -19,6 +19,7 @@ package com.android.keyguard; import static java.util.Collections.emptySet; import android.content.Context; +import android.graphics.Canvas; import android.os.Build; import android.os.Trace; import android.util.AttributeSet; @@ -47,6 +48,7 @@ public class KeyguardStatusView extends GridLayout { private KeyguardSliceView mKeyguardSlice; private View mMediaHostContainer; + private int mDrawAlpha = 255; private float mDarkAmount = 0; public KeyguardStatusView(Context context) { @@ -136,30 +138,19 @@ public class KeyguardStatusView extends GridLayout { Trace.endSection(); } - /** - * Clock content will be clipped when goes beyond bounds, - * so we setAlpha for all views except clock - */ - public void setAlpha(float alpha, boolean excludeClock) { - if (!excludeClock) { - setAlpha(alpha); - return; - } - if (alpha == 1 || alpha == 0) { - setAlpha(alpha); - } - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - if (child == mStatusViewContainer) { - for (int j = 0; j < mStatusViewContainer.getChildCount(); j++) { - View innerChild = mStatusViewContainer.getChildAt(j); - if (innerChild != mClockView) { - innerChild.setAlpha(alpha); - } - } - } else { - child.setAlpha(alpha); - } - } + @Override + protected boolean onSetAlpha(int alpha) { + mDrawAlpha = alpha; + return true; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + KeyguardClockFrame.saveCanvasAlpha( + this, canvas, mDrawAlpha, + c -> { + super.dispatchDraw(c); + return kotlin.Unit.INSTANCE; + }); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index af474661a67d..794eeda86b0f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -180,8 +180,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV */ public void setAlpha(float alpha) { if (!mKeyguardVisibilityHelper.isVisibilityAnimating()) { - mView.setAlpha(alpha, true); - mKeyguardClockSwitchController.setAlpha(alpha); + mView.setAlpha(alpha); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 77e13ce28d96..e8046dc07c02 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -29,6 +29,7 @@ import static android.hardware.biometrics.BiometricConstants.LockoutMode; import static android.hardware.biometrics.BiometricSourceType.FACE; import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN; +import static android.os.BatteryManager.CHARGING_POLICY_DEFAULT; import static android.os.PowerManager.WAKE_REASON_UNKNOWN; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT; @@ -893,7 +894,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } @VisibleForTesting - protected void onFingerprintAuthenticated(int userId, boolean isStrongBiometric) { + public void onFingerprintAuthenticated(int userId, boolean isStrongBiometric) { Assert.isMainThread(); Trace.beginSection("KeyGuardUpdateMonitor#onFingerPrintAuthenticated"); mUserFingerprintAuthenticated.put(userId, @@ -1169,7 +1170,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } @VisibleForTesting - protected void onFaceAuthenticated(int userId, boolean isStrongBiometric) { + public void onFaceAuthenticated(int userId, boolean isStrongBiometric) { Trace.beginSection("KeyGuardUpdateMonitor#onFaceAuthenticated"); Assert.isMainThread(); mUserFaceAuthenticated.put(userId, @@ -1235,7 +1236,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab */ private void handleFaceAcquired(int acquireInfo) { Assert.isMainThread(); - mLogger.logFaceAcquired(acquireInfo); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -1288,7 +1288,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return; } Assert.isMainThread(); - mLogger.logFaceAuthHelpMsg(msgId, helpString); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -1462,6 +1461,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab ErrorAuthenticationStatus error = (ErrorAuthenticationStatus) status; handleFaceError(error.getMsgId(), error.getMsg()); } else if (status instanceof FailedAuthenticationStatus) { + if (isFaceLockedOut()) { + // TODO b/270090188: remove this hack when biometrics fixes this issue. + // FailedAuthenticationStatus is emitted after ErrorAuthenticationStatus + // for lockout error is received + mLogger.d("onAuthenticationFailed called after" + + " face has been locked out"); + return; + } handleFaceAuthFailed(); } else if (status instanceof HelpAuthenticationStatus) { HelpAuthenticationStatus helpMsg = (HelpAuthenticationStatus) status; @@ -1970,6 +1977,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Override public void onAuthenticationFailed() { + if (isFaceLockedOut()) { + // TODO b/270090188: remove this hack when biometrics fixes this issue. + // onAuthenticationFailed is called after onAuthenticationError + // for lockout error is received + mLogger.d("onAuthenticationFailed called after face has been locked out"); + return; + } handleFaceAuthFailed(); } @@ -2444,7 +2458,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } // Take a guess at initial SIM state, battery status and PLMN until we get an update - mBatteryStatus = new BatteryStatus(BATTERY_STATUS_UNKNOWN, 100, 0, 0, 0, true); + mBatteryStatus = new BatteryStatus(BATTERY_STATUS_UNKNOWN, /* level= */ 100, /* plugged= */ + 0, CHARGING_POLICY_DEFAULT, /* maxChargingWattage= */0, /* present= */true); // Watch for interesting updates final IntentFilter filter = new IntentFilter(); @@ -2623,6 +2638,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * @return true if the FP sensor is non-UDFPS and the device can be unlocked using fingerprint + * at this moment. + */ + public boolean isFingerprintAllowedInBouncer() { + return !isUdfpsSupported() && isUnlockingWithFingerprintAllowed(); + } + + /** * @return true if there's at least one sfps enrollment for the current user. */ public boolean isSfpsEnrolled() { @@ -3860,8 +3883,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return true; } - // change in battery overheat - return current.health != old.health; + // change in charging status + return current.chargingStatus != old.chargingStatus; } /** @@ -4395,7 +4418,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintListenBuffer.toList() ).printTableData(pw); } - + pw.println("ActiveUnlockRunning=" + + mTrustManager.isActiveUnlockRunning(KeyguardUpdateMonitor.getCurrentUser())); new DumpsysTableLogger( "KeyguardActiveUnlockTriggers", KeyguardActiveUnlockModel.TABLE_HEADERS, diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java index 6ae80a62891b..ebd234fd0846 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java @@ -116,7 +116,12 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni * @param isTransparentMode */ public void setTransparentMode(boolean isTransparentMode) { + if (mIsTransparentMode == isTransparentMode) { + return; + } + mIsTransparentMode = isTransparentMode; + if (isTransparentMode) { setBackgroundColor(getResources().getColor(android.R.color.transparent)); } else { diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java index 540001135543..8e8ee48aba83 100644 --- a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java +++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java @@ -101,6 +101,7 @@ public class PasswordTextView extends FrameLayout { private Interpolator mFastOutSlowInInterpolator; private boolean mShowPassword = true; private UserActivityListener mUserActivityListener; + private boolean mIsPinHinting; private PinShapeInput mPinShapeInput; private boolean mUsePinShapes = false; @@ -419,10 +420,15 @@ public class PasswordTextView extends FrameLayout { /** * Determines whether AutoConfirmation feature is on. * - * @param usePinShapes * @param isPinHinting */ public void setIsPinHinting(boolean isPinHinting) { + // Do not reinflate the view if we are using the same one. + if (mPinShapeInput != null && mIsPinHinting == isPinHinting) { + return; + } + mIsPinHinting = isPinHinting; + if (mPinShapeInput != null) { removeView(mPinShapeInput.getView()); mPinShapeInput = null; diff --git a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java index 9308773858e3..50f8f7e61230 100644 --- a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java @@ -29,11 +29,11 @@ public interface ViewMediatorCallback { /** * Report that the keyguard is done. * - * @param strongAuth whether the user has authenticated with strong authentication like + * @param primaryAuth whether the user has authenticated with primary authentication like * pattern, password or PIN but not by trust agents or fingerprint * @param targetUserId a user that needs to be the foreground user at the completion. */ - void keyguardDone(boolean strongAuth, int targetUserId); + void keyguardDone(boolean primaryAuth, int targetUserId); /** * Report that the keyguard is done drawing. @@ -49,11 +49,11 @@ public interface ViewMediatorCallback { /** * Report that the keyguard is dismissable, pending the next keyguardDone call. * - * @param strongAuth whether the user has authenticated with strong authentication like + * @param primaryAuth whether the user has authenticated with primary authentication like * pattern, password or PIN but not by trust agents or fingerprint * @param targetUserId a user that needs to be the foreground user at the completion. */ - void keyguardDonePending(boolean strongAuth, int targetUserId); + void keyguardDonePending(boolean primaryAuth, int targetUserId); /** * Report when keyguard is actually gone diff --git a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt new file mode 100644 index 000000000000..19787154bec4 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt @@ -0,0 +1,142 @@ +/* + * 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.keyguard.logging + +import androidx.annotation.IntDef +import com.android.keyguard.CarrierTextManager.CarrierTextCallbackInfo +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.dagger.CarrierTextManagerLog +import javax.inject.Inject + +/** Logger adapter for [CarrierTextManager] to add detailed messages in a [LogBuffer] */ +class CarrierTextManagerLogger @Inject constructor(@CarrierTextManagerLog val buffer: LogBuffer) { + /** + * To help disambiguate carrier text manager instances, set a location string here which will + * propagate to [logUpdate] and [logUpdateCarrierTextForReason] + */ + var location: String? = null + + /** + * This method and the methods below trace the execution of CarrierTextManager.updateCarrierText + */ + fun logUpdate(numSubs: Int) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { int1 = numSubs }, + { "updateCarrierText: location=${location ?: "(unknown)"} numSubs=$int1" }, + ) + } + + fun logUpdateLoopStart(sub: Int, simState: Int, carrierName: String) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + int1 = sub + int2 = simState + str1 = carrierName + }, + { "┣ updateCarrierText: updating sub=$int1 simState=$int2 carrierName=$str1" }, + ) + } + + fun logUpdateWfcCheck() { + buffer.log( + TAG, + LogLevel.VERBOSE, + {}, + { "┣ updateCarrierText: found WFC state" }, + ) + } + + fun logUpdateFromStickyBroadcast(plmn: String?, spn: String?) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = plmn + str2 = spn + }, + { "┣ updateCarrierText: getting PLMN/SPN sticky brdcst. plmn=$str1, spn=$str1" }, + ) + } + + /** De-structures the info object so that we don't have to generate new strings */ + fun logCallbackSentFromUpdate(info: CarrierTextCallbackInfo) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = "${info.carrierText}" + bool1 = info.anySimReady + bool2 = info.airplaneMode + }, + { + "┗ updateCarrierText: " + + "result=(carrierText=$str1, anySimReady=$bool1, airplaneMode=$bool2)" + }, + ) + } + + /** + * Used to log the starting point for _why_ the carrier text is updating. In order to keep us + * from holding on to too many objects, we'll just use simple ints for reasons here + */ + fun logUpdateCarrierTextForReason(@CarrierTextRefreshReason reason: Int) { + buffer.log( + TAG, + LogLevel.DEBUG, + { int1 = reason }, + { + "refreshing carrier info for reason: ${reason.reasonMessage()}" + + " location=${location ?: "(unknown)"}" + } + ) + } + + companion object { + const val REASON_REFRESH_CARRIER_INFO = 1 + const val REASON_ON_TELEPHONY_CAPABLE = 2 + const val REASON_ON_SIM_STATE_CHANGED = 3 + const val REASON_ACTIVE_DATA_SUB_CHANGED = 4 + + @Retention(AnnotationRetention.SOURCE) + @IntDef( + value = + [ + REASON_REFRESH_CARRIER_INFO, + REASON_ON_TELEPHONY_CAPABLE, + REASON_ON_SIM_STATE_CHANGED, + REASON_ACTIVE_DATA_SUB_CHANGED, + ] + ) + annotation class CarrierTextRefreshReason + + private fun @receiver:CarrierTextRefreshReason Int.reasonMessage() = + when (this) { + REASON_REFRESH_CARRIER_INFO -> "REFRESH_CARRIER_INFO" + REASON_ON_TELEPHONY_CAPABLE -> "ON_TELEPHONY_CAPABLE" + REASON_ON_SIM_STATE_CHANGED -> "SIM_STATE_CHANGED" + REASON_ACTIVE_DATA_SUB_CHANGED -> "ACTIVE_DATA_SUB_CHANGED" + else -> "unknown" + } + } +} + +private const val TAG = "CarrierTextManagerLog" diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index c2d22c3e1d14..17cc23632d94 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -95,10 +95,6 @@ constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { logBuffer.log(TAG, ERROR, {}, { logMsg }, exception = ex) } - fun logFaceAcquired(acquireInfo: Int) { - logBuffer.log(TAG, DEBUG, { int1 = acquireInfo }, { "Face acquired acquireInfo=$int1" }) - } - fun logFaceAuthDisabledForUser(userId: Int) { logBuffer.log( TAG, @@ -128,18 +124,6 @@ constructor(@KeyguardUpdateMonitorLog private val logBuffer: LogBuffer) { ) } - fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String?) { - logBuffer.log( - TAG, - DEBUG, - { - int1 = msgId - str1 = helpMsg - }, - { "Face help received, msgId: $int1 msg: $str1" } - ) - } - fun logFaceAuthRequested(reason: String?) { logBuffer.log(TAG, DEBUG, { str1 = reason }, { "requestFaceAuth() reason=$str1" }) } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt index f05152ae8418..cb764a8f94aa 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/TrustRepositoryLogger.kt @@ -17,6 +17,7 @@ package com.android.keyguard.logging import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.ActiveUnlockModel import com.android.systemui.keyguard.shared.model.TrustManagedModel import com.android.systemui.keyguard.shared.model.TrustModel import com.android.systemui.log.LogBuffer @@ -76,6 +77,18 @@ constructor( ) } + fun activeUnlockModelEmitted(value: ActiveUnlockModel) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { + int1 = value.userId + bool1 = value.isRunning + }, + { "activeUnlockModel emitted: userId: $int1 isRunning: $bool1" } + ) + } + fun isCurrentUserTrusted(isCurrentUserTrusted: Boolean) { logBuffer.log( TAG, @@ -85,6 +98,15 @@ constructor( ) } + fun isCurrentUserActiveUnlockRunning(isCurrentUserActiveUnlockRunning: Boolean) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { bool1 = isCurrentUserActiveUnlockRunning }, + { "isCurrentUserActiveUnlockRunning emitted: $bool1" } + ) + } + fun isCurrentUserTrustManaged(isTrustManaged: Boolean) { logBuffer.log(TAG, DEBUG, { bool1 = isTrustManaged }, { "isTrustManaged emitted: $bool1" }) } diff --git a/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java b/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java deleted file mode 100644 index bf84f8af02fe..000000000000 --- a/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java +++ /dev/null @@ -1,221 +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. - */ - -package com.android.systemui; - -import android.app.PendingIntent; -import android.content.Intent; -import android.os.UserHandle; -import android.view.View; - -import androidx.annotation.Nullable; - -import com.android.systemui.animation.ActivityLaunchAnimator; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.statusbar.phone.CentralSurfaces; - -import dagger.Lazy; - -import java.util.Optional; - -import javax.inject.Inject; - -/** - * Single common instance of ActivityStarter that can be gotten and referenced from anywhere, but - * delegates to an actual implementation (CentralSurfaces). - * - * @deprecated Migrating to ActivityStarterImpl - */ -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") -@SysUISingleton -public class ActivityStarterDelegate implements ActivityStarter { - - private Lazy<Optional<CentralSurfaces>> mActualStarterOptionalLazy; - - @Inject - public ActivityStarterDelegate(Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy) { - mActualStarterOptionalLazy = centralSurfacesOptionalLazy; - } - - @Override - public void startPendingIntentDismissingKeyguard(PendingIntent intent) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startPendingIntentDismissingKeyguard(intent)); - } - - @Override - public void startPendingIntentDismissingKeyguard(PendingIntent intent, - Runnable intentSentUiThreadCallback) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startPendingIntentDismissingKeyguard( - intent, intentSentUiThreadCallback)); - } - - @Override - public void startPendingIntentDismissingKeyguard(PendingIntent intent, - Runnable intentSentUiThreadCallback, View associatedView) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startPendingIntentDismissingKeyguard( - intent, intentSentUiThreadCallback, associatedView)); - } - - @Override - public void startPendingIntentDismissingKeyguard(PendingIntent intent, - Runnable intentSentUiThreadCallback, - ActivityLaunchAnimator.Controller animationController) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startPendingIntentDismissingKeyguard( - intent, intentSentUiThreadCallback, animationController)); - } - - @Override - public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, - int flags) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startActivity(intent, onlyProvisioned, dismissShade, flags)); - } - - @Override - public void startActivity(Intent intent, boolean dismissShade) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startActivity(intent, dismissShade)); - } - - @Override - public void startActivity(Intent intent, - boolean dismissShade, - @Nullable ActivityLaunchAnimator.Controller animationController) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startActivity(intent, dismissShade, animationController)); - } - - @Override - public void startActivity(Intent intent, boolean dismissShade, - @Nullable ActivityLaunchAnimator.Controller animationController, - boolean showOverLockscreenWhenLocked) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startActivity(intent, dismissShade, animationController, - showOverLockscreenWhenLocked)); - } - - @Override - public void startActivity(Intent intent, boolean dismissShade, - @Nullable ActivityLaunchAnimator.Controller animationController, - boolean showOverLockscreenWhenLocked, UserHandle userHandle) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startActivity(intent, dismissShade, animationController, - showOverLockscreenWhenLocked, userHandle)); - } - - @Override - public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startActivity(intent, onlyProvisioned, dismissShade)); - } - - @Override - public void startActivity(Intent intent, boolean dismissShade, Callback callback) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startActivity(intent, dismissShade, callback)); - } - - @Override - public void postStartActivityDismissingKeyguard(Intent intent, int delay) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.postStartActivityDismissingKeyguard(intent, delay)); - } - - @Override - public void postStartActivityDismissingKeyguard(Intent intent, int delay, - @Nullable ActivityLaunchAnimator.Controller animationController) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.postStartActivityDismissingKeyguard( - intent, delay, animationController)); - } - - @Override - public void postStartActivityDismissingKeyguard(PendingIntent intent) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.postStartActivityDismissingKeyguard(intent)); - } - - @Override - public void postStartActivityDismissingKeyguard(Intent intent, int delay, - @Nullable ActivityLaunchAnimator.Controller animationController, String customMessage) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.postStartActivityDismissingKeyguard(intent, delay, - animationController, customMessage)); - } - - @Override - public void postStartActivityDismissingKeyguard(PendingIntent intent, - ActivityLaunchAnimator.Controller animationController) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.postStartActivityDismissingKeyguard( - intent, animationController)); - } - - @Override - public void postQSRunnableDismissingKeyguard(Runnable runnable) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.postQSRunnableDismissingKeyguard(runnable)); - } - - @Override - public void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancel, - boolean afterKeyguardGone) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.dismissKeyguardThenExecute(action, cancel, afterKeyguardGone)); - } - - @Override - public void dismissKeyguardThenExecute(OnDismissAction action, @Nullable Runnable cancel, - boolean afterKeyguardGone, String customMessage) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.dismissKeyguardThenExecute(action, cancel, afterKeyguardGone, - customMessage)); - } - - @Override - public void startActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned, - boolean dismissShade, boolean disallowEnterPictureInPictureWhileLaunching, - Callback callback, int flags, - @Nullable ActivityLaunchAnimator.Controller animationController, - UserHandle userHandle) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.startActivityDismissingKeyguard(intent, onlyProvisioned, - dismissShade, disallowEnterPictureInPictureWhileLaunching, callback, - flags, animationController, userHandle)); - } - - @Override - public void executeRunnableDismissingKeyguard(Runnable runnable, - Runnable cancelAction, boolean dismissShade, - boolean afterKeyguardGone, boolean deferred) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.executeRunnableDismissingKeyguard(runnable, cancelAction, - dismissShade, afterKeyguardGone, deferred)); - } - - @Override - public void executeRunnableDismissingKeyguard(Runnable runnable, Runnable cancelAction, - boolean dismissShade, boolean afterKeyguardGone, boolean deferred, - boolean willAnimateOnKeyguard, @Nullable String customMessage) { - mActualStarterOptionalLazy.get().ifPresent( - starter -> starter.executeRunnableDismissingKeyguard(runnable, cancelAction, - dismissShade, afterKeyguardGone, deferred, willAnimateOnKeyguard, - customMessage)); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt deleted file mode 100644 index 227f0ace33dd..000000000000 --- a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.android.systemui - -import android.content.ComponentName -import android.content.Context -import android.content.pm.PackageManager -import android.util.Log -import com.android.internal.R -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.FlagListenable -import com.android.systemui.flags.Flags -import com.android.systemui.settings.UserTracker -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext -import javax.inject.Inject - -@SysUISingleton -class ChooserSelector @Inject constructor( - private val context: Context, - private val userTracker: UserTracker, - private val featureFlags: FeatureFlags, - @Application private val coroutineScope: CoroutineScope, - @Background private val bgDispatcher: CoroutineDispatcher, -) : CoreStartable { - - private val chooserComponent = ComponentName.unflattenFromString( - context.resources.getString(R.string.config_chooserActivity)) - - override fun start() { - coroutineScope.launch { - val listener = FlagListenable.Listener { event -> - if (event.flagName == Flags.CHOOSER_UNBUNDLED.name) { - launch { updateUnbundledChooserEnabled() } - event.requestNoRestart() - } - } - featureFlags.addListener(Flags.CHOOSER_UNBUNDLED, listener) - updateUnbundledChooserEnabled() - - awaitCancellationAndThen { featureFlags.removeListener(listener) } - } - } - - private suspend fun updateUnbundledChooserEnabled() { - setUnbundledChooserEnabled(withContext(bgDispatcher) { - featureFlags.isEnabled(Flags.CHOOSER_UNBUNDLED) - }) - } - - private fun setUnbundledChooserEnabled(enabled: Boolean) { - val newState = if (enabled) { - PackageManager.COMPONENT_ENABLED_STATE_ENABLED - } else { - PackageManager.COMPONENT_ENABLED_STATE_DISABLED - } - userTracker.userProfiles.forEach { - try { - context.createContextAsUser(it.userHandle, /* flags = */ 0).packageManager - .setComponentEnabledSetting(chooserComponent, newState, /* flags = */ 0) - } catch (e: IllegalArgumentException) { - Log.w( - "ChooserSelector", - "Unable to set IntentResolver enabled=$enabled for user ${it.id}", - e, - ) - } - } - } - - suspend inline fun awaitCancellation(): Nothing = suspendCancellableCoroutine { } - suspend inline fun awaitCancellationAndThen(block: () -> Unit): Nothing = try { - awaitCancellation() - } finally { - block() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index 76086dffd263..403c80985377 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -31,10 +31,10 @@ import android.graphics.RectF import android.hardware.biometrics.BiometricSourceType import android.view.View import androidx.core.graphics.ColorUtils +import com.android.app.animation.Interpolators import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.settingslib.Utils -import com.android.app.animation.Interpolators import com.android.systemui.biometrics.AuthController import com.android.systemui.log.ScreenDecorationsLogger import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -63,7 +63,7 @@ class FaceScanningOverlay( private var cameraProtectionColor = Color.BLACK var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context, - R.attr.wallpaperTextColorAccent) + com.android.internal.R.attr.materialColorPrimaryFixed) private var cameraProtectionAnimator: ValueAnimator? = null var hideOverlayRunnable: Runnable? = null var faceAuthSucceeded = false diff --git a/packages/SystemUI/src/com/android/systemui/LatencyTester.java b/packages/SystemUI/src/com/android/systemui/LatencyTester.java index 8f419560c78d..39d9714f2209 100644 --- a/packages/SystemUI/src/com/android/systemui/LatencyTester.java +++ b/packages/SystemUI/src/com/android/systemui/LatencyTester.java @@ -20,7 +20,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricSourceType; import android.os.Build; import android.provider.DeviceConfig; @@ -57,6 +56,7 @@ public class LatencyTester implements CoreStartable { private final BiometricUnlockController mBiometricUnlockController; private final BroadcastDispatcher mBroadcastDispatcher; private final DeviceConfigProxy mDeviceConfigProxy; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private boolean mEnabled; @@ -65,11 +65,13 @@ public class LatencyTester implements CoreStartable { BiometricUnlockController biometricUnlockController, BroadcastDispatcher broadcastDispatcher, DeviceConfigProxy deviceConfigProxy, - @Main DelayableExecutor mainExecutor + @Main DelayableExecutor mainExecutor, + KeyguardUpdateMonitor keyguardUpdateMonitor ) { mBiometricUnlockController = biometricUnlockController; mBroadcastDispatcher = broadcastDispatcher; mDeviceConfigProxy = deviceConfigProxy; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; updateEnabled(); mDeviceConfigProxy.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_LATENCY_TRACKER, @@ -85,10 +87,13 @@ public class LatencyTester implements CoreStartable { if (!mEnabled) { return; } - mBiometricUnlockController.onBiometricAcquired(type, - BiometricConstants.BIOMETRIC_ACQUIRED_GOOD); - mBiometricUnlockController.onBiometricAuthenticated( - KeyguardUpdateMonitor.getCurrentUser(), type, true /* isStrongBiometric */); + if (type == BiometricSourceType.FACE) { + mKeyguardUpdateMonitor.onFaceAuthenticated(KeyguardUpdateMonitor.getCurrentUser(), + true); + } else if (type == BiometricSourceType.FINGERPRINT) { + mKeyguardUpdateMonitor.onFingerprintAuthenticated( + KeyguardUpdateMonitor.getCurrentUser(), true); + } } private void registerForBroadcasts(boolean register) { diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java index 632fcdc16259..2b468cf7c16b 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java @@ -27,6 +27,7 @@ import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dagger.WMComponent; import com.android.systemui.util.InitializationChecker; import com.android.wm.shell.dagger.WMShellConcurrencyModule; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.sysui.ShellInterface; import com.android.wm.shell.transition.ShellTransitions; @@ -93,6 +94,7 @@ public abstract class SystemUIInitializer { .setBubbles(mWMComponent.getBubbles()) .setTaskViewFactory(mWMComponent.getTaskViewFactory()) .setTransitions(mWMComponent.getTransitions()) + .setKeyguardTransitions(mWMComponent.getKeyguardTransitions()) .setStartingSurface(mWMComponent.getStartingSurface()) .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper()) .setRecentTasks(mWMComponent.getRecentTasks()) @@ -113,6 +115,7 @@ public abstract class SystemUIInitializer { .setBubbles(Optional.ofNullable(null)) .setTaskViewFactory(Optional.ofNullable(null)) .setTransitions(new ShellTransitions() {}) + .setKeyguardTransitions(new KeyguardTransitions() {}) .setDisplayAreaHelper(Optional.ofNullable(null)) .setStartingSurface(Optional.ofNullable(null)) .setRecentTasks(Optional.ofNullable(null)) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityLogger.kt b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityLogger.kt new file mode 100644 index 000000000000..7b915961c046 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityLogger.kt @@ -0,0 +1,54 @@ +/* + * 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.accessibility + +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger +import javax.inject.Inject + +/** + * Handles logging UiEvent stats for simple UI interactions. + * + * See go/uievent + */ +class AccessibilityLogger @Inject constructor(private val uiEventLogger: UiEventLogger) { + /** Logs the given event */ + fun log(event: UiEventLogger.UiEventEnum) { + uiEventLogger.log(event) + } + + /** Events regarding interaction with the magnifier settings panel */ + enum class MagnificationSettingsEvent constructor(private val id: Int) : + UiEventLogger.UiEventEnum { + @UiEvent(doc = "Magnification settings panel opened.") + MAGNIFICATION_SETTINGS_PANEL_OPENED(1381), + + @UiEvent(doc = "Magnification settings panel closed") + MAGNIFICATION_SETTINGS_PANEL_CLOSED(1382), + + @UiEvent(doc = "Magnification settings panel edit size button clicked") + MAGNIFICATION_SETTINGS_SIZE_EDITING_ACTIVATED(1383), + + @UiEvent(doc = "Magnification settings panel edit size save button clicked") + MAGNIFICATION_SETTINGS_SIZE_EDITING_DEACTIVATED(1384), + + @UiEvent(doc = "Magnification settings panel window size selected") + MAGNIFICATION_SETTINGS_WINDOW_SIZE_SELECTED(1386); + + override fun getId(): Int = this.id + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java index 3349fe5f1147..c3bb423e5e4e 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java @@ -87,19 +87,14 @@ public class MagnificationSettingsController implements ComponentCallbacks { } /** - * Shows magnification settings panel {@link WindowMagnificationSettings}. The panel ui would be - * various for different magnification mode. - * - * @param mode The magnification mode - * @see android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW - * @see android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN + * Shows magnification settings panel {@link WindowMagnificationSettings}. */ - void showMagnificationSettings(int mode) { + void showMagnificationSettings() { if (!mWindowMagnificationSettings.isSettingPanelShowing()) { onConfigurationChanged(mContext.getResources().getConfiguration()); mContext.registerComponentCallbacks(this); } - mWindowMagnificationSettings.showSettingPanel(mode); + mWindowMagnificationSettings.showSettingPanel(); } void closeMagnificationSettings() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java index 1c030da99e15..e2b85fa0ac00 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java @@ -16,10 +16,10 @@ package com.android.systemui.accessibility; -import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY; +import static com.android.systemui.accessibility.AccessibilityLogger.MagnificationSettingsEvent; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; import android.annotation.MainThread; @@ -67,6 +67,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback private final CommandQueue mCommandQueue; private final OverviewProxyService mOverviewProxyService; private final DisplayTracker mDisplayTracker; + private final AccessibilityLogger mA11yLogger; private WindowMagnificationConnectionImpl mWindowMagnificationConnectionImpl; private SysUiState mSysUiState; @@ -152,7 +153,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback CommandQueue commandQueue, ModeSwitchesController modeSwitchesController, SysUiState sysUiState, OverviewProxyService overviewProxyService, SecureSettings secureSettings, DisplayTracker displayTracker, - DisplayManager displayManager) { + DisplayManager displayManager, AccessibilityLogger a11yLogger) { mContext = context; mHandler = mainHandler; mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); @@ -161,6 +162,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback mSysUiState = sysUiState; mOverviewProxyService = overviewProxyService; mDisplayTracker = displayTracker; + mA11yLogger = a11yLogger; mMagnificationControllerSupplier = new ControllerSupplier(context, mHandler, mWindowMagnifierCallback, displayManager, sysUiState, secureSettings); @@ -169,8 +171,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback mModeSwitchesController.setClickListenerDelegate( displayId -> mHandler.post(() -> { - showMagnificationSettingsPanel(displayId, - ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + showMagnificationSettingsPanel(displayId); })); } @@ -253,11 +254,11 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback } @MainThread - void showMagnificationSettingsPanel(int displayId, int mode) { + void showMagnificationSettingsPanel(int displayId) { final MagnificationSettingsController magnificationSettingsController = mMagnificationSettingsSupplier.get(displayId); if (magnificationSettingsController != null) { - magnificationSettingsController.showMagnificationSettings(mode); + magnificationSettingsController.showMagnificationSettings(); } } @@ -334,7 +335,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback @Override public void onClickSettingsButton(int displayId) { mHandler.post(() -> { - showMagnificationSettingsPanel(displayId, ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + showMagnificationSettingsPanel(displayId); }); } }; @@ -345,6 +346,7 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback @Override public void onSetMagnifierSize(int displayId, int index) { mHandler.post(() -> onSetMagnifierSizeInternal(displayId, index)); + mA11yLogger.log(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_WINDOW_SIZE_SELECTED); } @Override @@ -355,6 +357,9 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback @Override public void onEditMagnifierSizeMode(int displayId, boolean enable) { mHandler.post(() -> onEditMagnifierSizeModeInternal(displayId, enable)); + mA11yLogger.log(enable + ? MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_SIZE_EDITING_ACTIVATED + : MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_SIZE_EDITING_DEACTIVATED); } @Override @@ -372,6 +377,9 @@ public class WindowMagnification implements CoreStartable, CommandQueue.Callback @Override public void onSettingsPanelVisibilityChanged(int displayId, boolean shown) { mHandler.post(() -> onSettingsPanelVisibilityChangedInternal(displayId, shown)); + mA11yLogger.log(shown + ? MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_PANEL_OPENED + : MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_PANEL_CLOSED); } }; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index 155c26d9fe9d..3b1d695e3dad 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -16,6 +16,7 @@ package com.android.systemui.accessibility; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; @@ -27,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; +import android.database.ContentObserver; import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Rect; @@ -51,6 +53,7 @@ import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.Switch; +import android.widget.TextView; import com.android.internal.accessibility.common.MagnificationConstants; import com.android.internal.annotations.VisibleForTesting; @@ -85,6 +88,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private SeekBarWithIconButtonsView mZoomSeekbar; private LinearLayout mAllowDiagonalScrollingView; + private TextView mAllowDiagonalScrollingTitle; private Switch mAllowDiagonalScrollingSwitch; private LinearLayout mPanelView; private LinearLayout mSettingView; @@ -101,8 +105,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private static final float A11Y_SCALE_MIN_VALUE = 1.0f; private WindowMagnificationSettingsCallback mCallback; - // the magnification mode that triggers showing the panel - private int mTriggeringMode = ACCESSIBILITY_MAGNIFICATION_MODE_NONE; + private ContentObserver mMagnificationCapabilityObserver; @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -142,6 +145,16 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mGestureDetector = new MagnificationGestureDetector(context, context.getMainThreadHandler(), this); + + mMagnificationCapabilityObserver = new ContentObserver( + mContext.getMainThreadHandler()) { + @Override + public void onChange(boolean selfChange) { + mSettingView.post(() -> { + updateUIControlsIfNeeded(); + }); + } + }; } private class ZoomSeekbarChangeListener implements SeekBar.OnSeekBarChangeListener { @@ -257,7 +270,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @Override public boolean onFinish(float xOffset, float yOffset) { if (!mSingleTapDetected) { - showSettingPanel(mTriggeringMode); + showSettingPanel(); } mSingleTapDetected = false; return true; @@ -285,7 +298,8 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest return; } - // Reset button status. + // Unregister observer before removing view + mSecureSettings.unregisterContentObserver(mMagnificationCapabilityObserver); mWindowManager.removeView(mSettingView); mIsVisible = false; if (resetPosition) { @@ -297,16 +311,8 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mCallback.onSettingsPanelVisibilityChanged(/* shown= */ false); } - /** - * Shows magnification settings panel. The panel ui would be various for - * different magnification mode. - * - * @param mode The magnification mode - * @see android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW - * @see android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN - */ - public void showSettingPanel(int mode) { - showSettingPanel(mode, true); + public void showSettingPanel() { + showSettingPanel(true); } public boolean isSettingPanelShowing() { @@ -322,18 +328,15 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest } /** - * Shows magnification panel for set magnification. + * Shows the panel for magnification settings. * When the panel is going to be visible by calling this method, the layout position can be * reset depending on the flag. * - * @param mode The magnification mode - * @param resetPosition if the button position needs be reset + * @param resetPosition if the panel position needs to be reset */ - private void showSettingPanel(int mode, boolean resetPosition) { + private void showSettingPanel(boolean resetPosition) { if (!mIsVisible) { - updateUIControlsIfNeed(mode); - mTriggeringMode = mode; - + updateUIControlsIfNeeded(); if (resetPosition) { mDraggableWindowBounds.set(getDraggableWindowBounds()); mParams.x = mDraggableWindowBounds.right; @@ -342,6 +345,11 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mWindowManager.addView(mSettingView, mParams); + mSecureSettings.registerContentObserverForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, + mMagnificationCapabilityObserver, + UserHandle.USER_CURRENT); + // Exclude magnification switch button from system gesture area. setSystemGestureExclusion(); mIsVisible = true; @@ -359,35 +367,74 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); } - private void updateUIControlsIfNeed(int mode) { - if (mode == mTriggeringMode) { - return; - } + private int getMagnificationMode() { + return mSecureSettings.getIntForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, + ACCESSIBILITY_MAGNIFICATION_MODE_NONE, + UserHandle.USER_CURRENT); + } - int selectedButtonIndex = mLastSelectedButtonIndex; - switch (mode) { - case ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN: - // set the edit button visibility to View.INVISIBLE to keep the height, to prevent - // the size title from too close to the size buttons - mEditButton.setVisibility(View.INVISIBLE); - mAllowDiagonalScrollingView.setVisibility(View.GONE); - // force the fullscreen button showing - selectedButtonIndex = MagnificationSize.FULLSCREEN; - break; + private int getMagnificationCapability() { + return mSecureSettings.getIntForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, + ACCESSIBILITY_MAGNIFICATION_MODE_NONE, + UserHandle.USER_CURRENT); + } + private void updateUIControlsIfNeeded() { + int capability = getMagnificationCapability(); + + int selectedButtonIndex = mLastSelectedButtonIndex; + switch (capability) { case ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW: mEditButton.setVisibility(View.VISIBLE); mAllowDiagonalScrollingView.setVisibility(View.VISIBLE); + mFullScreenButton.setVisibility(View.GONE); if (selectedButtonIndex == MagnificationSize.FULLSCREEN) { selectedButtonIndex = MagnificationSize.NONE; } break; + case ACCESSIBILITY_MAGNIFICATION_MODE_ALL: + int mode = getMagnificationMode(); + mFullScreenButton.setVisibility(View.VISIBLE); + if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) { + // set the edit button visibility to View.INVISIBLE to keep the height, to + // prevent the size title from too close to the size buttons + mEditButton.setVisibility(View.INVISIBLE); + mAllowDiagonalScrollingView.setVisibility(View.GONE); + // force the fullscreen button showing + selectedButtonIndex = MagnificationSize.FULLSCREEN; + } else { // mode = ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW + mEditButton.setVisibility(View.VISIBLE); + mAllowDiagonalScrollingView.setVisibility(View.VISIBLE); + } + break; + + case ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN: + // We will never fall into this case since we never show settings panel when + // capability equals to ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN. + // Currently, the case follows the UI controls when capability equals to + // ACCESSIBILITY_MAGNIFICATION_MODE_ALL and mode equals to + // ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN, but we could also consider to + // remove the whole icon button selections int the future since they are no use + // for fullscreen only capability. + + mFullScreenButton.setVisibility(View.VISIBLE); + // set the edit button visibility to View.INVISIBLE to keep the height, to + // prevent the size title from too close to the size buttons + mEditButton.setVisibility(View.INVISIBLE); + mAllowDiagonalScrollingView.setVisibility(View.GONE); + // force the fullscreen button showing + selectedButtonIndex = MagnificationSize.FULLSCREEN; + break; + default: break; } updateSelectedButton(selectedButtonIndex); + mSettingView.requestLayout(); } private final BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { @@ -422,6 +469,8 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button); mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button); mFullScreenButton = mSettingView.findViewById(R.id.magnifier_full_button); + mAllowDiagonalScrollingTitle = + mSettingView.findViewById(R.id.magnifier_horizontal_lock_title); mZoomSeekbar = mSettingView.findViewById(R.id.magnifier_zoom_slider); float scale = mSecureSettings.getFloatForUser( @@ -445,6 +494,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mDoneButton.setOnClickListener(mButtonClickListener); mFullScreenButton.setOnClickListener(mButtonClickListener); mEditButton.setOnClickListener(mButtonClickListener); + mAllowDiagonalScrollingTitle.setSelected(true); mSettingView.setOnApplyWindowInsetsListener((v, insets) -> { // Adds a pending post check to avoiding redundant calculation because this callback @@ -474,11 +524,11 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext); - boolean showSettingPanelAfterThemeChange = mIsVisible; + boolean showSettingPanelAfterConfigChange = mIsVisible; hideSettingPanel(/* resetPosition= */ false); inflateView(); - if (showSettingPanelAfterThemeChange) { - showSettingPanel(mTriggeringMode, /* resetPosition= */ false); + if (showSettingPanelAfterConfigChange) { + showSettingPanel(/* resetPosition= */ false); } return; } diff --git a/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt new file mode 100644 index 000000000000..7c394a62a5f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt @@ -0,0 +1,28 @@ +/* + * 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.authentication + +import com.android.systemui.authentication.data.repository.AuthenticationRepositoryModule +import dagger.Module + +@Module( + includes = + [ + AuthenticationRepositoryModule::class, + ], +) +object AuthenticationModule diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt new file mode 100644 index 000000000000..9d9a87d72e46 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -0,0 +1,94 @@ +/* + * 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.authentication.data.repository + +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import dagger.Binds +import dagger.Module +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Defines interface for classes that can access authentication-related application state. */ +interface AuthenticationRepository { + + /** + * Whether the device is unlocked. + * + * A device that is not yet unlocked requires unlocking by completing an authentication + * challenge according to the current authentication method. + * + * Note that this state has no real bearing on whether the lockscreen is showing or dismissed. + */ + val isUnlocked: StateFlow<Boolean> + + /** + * The currently-configured authentication method. This determines how the authentication + * challenge is completed in order to unlock an otherwise locked device. + */ + val authenticationMethod: StateFlow<AuthenticationMethodModel> + + /** + * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically + * dismisses once the authentication challenge is completed. For example, completing a biometric + * authentication challenge via face unlock or fingerprint sensor can automatically bypass the + * lock screen. + */ + val isBypassEnabled: StateFlow<Boolean> + + /** See [isUnlocked]. */ + fun setUnlocked(isUnlocked: Boolean) + + /** See [authenticationMethod]. */ + fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) + + /** See [isBypassEnabled]. */ + fun setBypassEnabled(isBypassEnabled: Boolean) +} + +class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository { + // TODO(b/280883900): get data from real data sources in SysUI. + + private val _isUnlocked = MutableStateFlow(false) + override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow() + + private val _authenticationMethod = + MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.PIN(1234)) + override val authenticationMethod: StateFlow<AuthenticationMethodModel> = + _authenticationMethod.asStateFlow() + + private val _isBypassEnabled = MutableStateFlow(false) + override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow() + + override fun setUnlocked(isUnlocked: Boolean) { + _isUnlocked.value = isUnlocked + } + + override fun setBypassEnabled(isBypassEnabled: Boolean) { + _isBypassEnabled.value = isBypassEnabled + } + + override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) { + _authenticationMethod.value = authenticationMethod + } +} + +@Module +interface AuthenticationRepositoryModule { + @Binds fun repository(impl: AuthenticationRepositoryImpl): AuthenticationRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt new file mode 100644 index 000000000000..5aea930401d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -0,0 +1,204 @@ +/* + * 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.authentication.domain.interactor + +import com.android.systemui.authentication.data.repository.AuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Hosts application business logic related to authentication. */ +@SysUISingleton +class AuthenticationInteractor +@Inject +constructor( + @Application applicationScope: CoroutineScope, + private val repository: AuthenticationRepository, +) { + /** + * The currently-configured authentication method. This determines how the authentication + * challenge is completed in order to unlock an otherwise locked device. + */ + val authenticationMethod: StateFlow<AuthenticationMethodModel> = repository.authenticationMethod + + /** + * Whether the device is unlocked. + * + * A device that is not yet unlocked requires unlocking by completing an authentication + * challenge according to the current authentication method. + * + * Note that this state has no real bearing on whether the lock screen is showing or dismissed. + */ + val isUnlocked: StateFlow<Boolean> = + combine(authenticationMethod, repository.isUnlocked) { authMethod, isUnlocked -> + isUnlockedWithAuthMethod( + isUnlocked = isUnlocked, + authMethod = authMethod, + ) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = + isUnlockedWithAuthMethod( + isUnlocked = repository.isUnlocked.value, + authMethod = repository.authenticationMethod.value, + ) + ) + + /** + * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically + * dismisses once the authentication challenge is completed. For example, completing a biometric + * authentication challenge via face unlock or fingerprint sensor can automatically bypass the + * lock screen. + */ + val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled + + init { + // UNLOCKS WHEN AUTH METHOD REMOVED. + // + // Unlocks the device if the auth method becomes None. + applicationScope.launch { + repository.authenticationMethod.collect { + if (it is AuthenticationMethodModel.None) { + unlockDevice() + } + } + } + } + + /** + * Returns `true` if the device currently requires authentication before content can be viewed; + * `false` if content can be displayed without unlocking first. + */ + fun isAuthenticationRequired(): Boolean { + return !isUnlocked.value && authenticationMethod.value.isSecure + } + + /** + * Unlocks the device, assuming that the authentication challenge has been completed + * successfully. + */ + fun unlockDevice() { + repository.setUnlocked(true) + } + + /** + * Locks the device. From now on, the device will remain locked until [authenticate] is called + * with the correct input. + */ + fun lockDevice() { + repository.setUnlocked(false) + } + + /** + * Attempts to authenticate the user and unlock the device. + * + * @param input The input from the user to try to authenticate with. This can be a list of + * different things, based on the current authentication method. + * @return `true` if the authentication succeeded and the device is now unlocked; `false` + * otherwise. + */ + fun authenticate(input: List<Any>): Boolean { + val isSuccessful = + when (val authMethod = this.authenticationMethod.value) { + is AuthenticationMethodModel.PIN -> input.asCode() == authMethod.code + is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password + is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates + else -> true + } + + if (isSuccessful) { + repository.setUnlocked(true) + } + + return isSuccessful + } + + /** Triggers a biometric-powered unlock of the device. */ + fun biometricUnlock() { + // TODO(b/280883900): only allow this if the biometric is enabled and there's a match. + repository.setUnlocked(true) + } + + /** See [authenticationMethod]. */ + fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) { + repository.setAuthenticationMethod(authenticationMethod) + } + + /** See [isBypassEnabled]. */ + fun toggleBypassEnabled() { + repository.setBypassEnabled(!repository.isBypassEnabled.value) + } + + companion object { + private fun isUnlockedWithAuthMethod( + isUnlocked: Boolean, + authMethod: AuthenticationMethodModel, + ): Boolean { + return if (authMethod is AuthenticationMethodModel.None) { + true + } else { + isUnlocked + } + } + + /** + * Returns a PIN code from the given list. It's assumed the given list elements are all + * [Int]. + */ + private fun List<Any>.asCode(): Int? { + if (isEmpty()) { + return null + } + + var code = 0 + map { it as Int }.forEach { integer -> code = code * 10 + integer } + + return code + } + + /** + * Returns a password from the given list. It's assumed the given list elements are all + * [Char]. + */ + private fun List<Any>.asPassword(): String { + val anyList = this + return buildString { anyList.forEach { append(it as Char) } } + } + + /** + * Returns a list of [AuthenticationMethodModel.Pattern.PatternCoordinate] from the given + * list. It's assumed the given list elements are all + * [AuthenticationMethodModel.Pattern.PatternCoordinate]. + */ + private fun List<Any>.asPattern(): + List<AuthenticationMethodModel.Pattern.PatternCoordinate> { + val anyList = this + return buildList { + anyList.forEach { add(it as AuthenticationMethodModel.Pattern.PatternCoordinate) } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt new file mode 100644 index 000000000000..83250b638424 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt @@ -0,0 +1,47 @@ +/* + * 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.authentication.shared.model + +/** Enumerates all known authentication methods. */ +sealed class AuthenticationMethodModel( + /** + * Whether the authentication method is considered to be "secure". + * + * "Secure" authentication methods require authentication to unlock the device. Non-secure auth + * methods simply require user dismissal. + */ + open val isSecure: Boolean, +) { + /** There is no authentication method on the device. We shouldn't even show the lock screen. */ + object None : AuthenticationMethodModel(isSecure = false) + + /** The most basic authentication method. The lock screen can be swiped away when displayed. */ + object Swipe : AuthenticationMethodModel(isSecure = false) + + data class PIN(val code: Int) : AuthenticationMethodModel(isSecure = true) + + data class Password(val password: String) : AuthenticationMethodModel(isSecure = true) + + data class Pattern(val coordinates: List<PatternCoordinate>) : + AuthenticationMethodModel(isSecure = true) { + + data class PatternCoordinate( + val x: Int, + val y: Int, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java index 2aac056b0bde..263df3357d67 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java @@ -78,7 +78,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { private boolean mShowPercentAvailable; private String mEstimateText = null; private boolean mCharging; - private boolean mIsOverheated; + private boolean mIsBatteryDefender; private boolean mDisplayShieldEnabled; // Error state where we know nothing about the current battery state private boolean mBatteryStateUnknown; @@ -213,9 +213,9 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { mDrawable.setPowerSaveEnabled(isPowerSave); } - void onIsOverheatedChanged(boolean isOverheated) { - boolean valueChanged = mIsOverheated != isOverheated; - mIsOverheated = isOverheated; + void onIsBatteryDefenderChanged(boolean isBatteryDefender) { + boolean valueChanged = mIsBatteryDefender != isBatteryDefender; + mIsBatteryDefender = isBatteryDefender; if (valueChanged) { updateContentDescription(); // The battery drawable is a different size depending on whether it's currently @@ -308,12 +308,12 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { contentDescription = context.getString(R.string.accessibility_battery_unknown); } else if (mShowPercentMode == MODE_ESTIMATE && !TextUtils.isEmpty(mEstimateText)) { contentDescription = context.getString( - mIsOverheated + mIsBatteryDefender ? R.string.accessibility_battery_level_charging_paused_with_estimate : R.string.accessibility_battery_level_with_estimate, mLevel, mEstimateText); - } else if (mIsOverheated) { + } else if (mIsBatteryDefender) { contentDescription = context.getString(R.string.accessibility_battery_level_charging_paused, mLevel); } else if (mCharging) { @@ -399,9 +399,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { float mainBatteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor; - // If the battery is marked as overheated, we should display a shield indicating that the - // battery is being "defended". - boolean displayShield = mDisplayShieldEnabled && mIsOverheated; + boolean displayShield = mDisplayShieldEnabled && mIsBatteryDefender; float fullBatteryIconHeight = BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield); float fullBatteryIconWidth = diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java index f4ec33ad24b5..f6a10bd4af4a 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java @@ -91,8 +91,8 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> } @Override - public void onIsOverheatedChanged(boolean isOverheated) { - mView.onIsOverheatedChanged(isOverheated); + public void onIsBatteryDefenderChanged(boolean isBatteryDefender) { + mView.onIsBatteryDefenderChanged(isBatteryDefender); } }; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index e089fd32829d..4db371bed517 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -28,6 +28,7 @@ import android.annotation.Nullable; import android.annotation.StringRes; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Insets; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.PromptInfo; @@ -298,9 +299,25 @@ public abstract class AuthBiometricView extends LinearLayout { mJankListener = jankListener; } + private void updatePaddings(int size) { + final Insets navBarInsets = Utils.getNavbarInsets(mContext); + if (size != AuthDialog.SIZE_LARGE) { + if (mPanelController.getPosition() == AuthPanelController.POSITION_LEFT) { + setPadding(navBarInsets.left, 0, 0, 0); + } else if (mPanelController.getPosition() == AuthPanelController.POSITION_RIGHT) { + setPadding(0, 0, navBarInsets.right, 0); + } else { + setPadding(0, 0, 0, navBarInsets.bottom); + } + } else { + setPadding(0, 0, 0, 0); + } + } + @VisibleForTesting final void updateSize(@AuthDialog.DialogSize int newSize) { Log.v(TAG, "Current size: " + mSize + " New size: " + newSize); + updatePaddings(newSize); if (newSize == AuthDialog.SIZE_SMALL) { mTitleView.setVisibility(View.GONE); mSubtitleView.setVisibility(View.GONE); @@ -527,6 +544,11 @@ public abstract class AuthBiometricView extends LinearLayout { mState = newState; } + void onOrientationChanged() { + // Update padding and AuthPanel outline by calling updateSize when the orientation changed. + updateSize(mSize); + } + public void onDialogAnimatedIn() { updateState(STATE_AUTHENTICATING); } @@ -868,6 +890,25 @@ public abstract class AuthBiometricView extends LinearLayout { } mLayoutParams = onMeasureInternal(width, height); + + final Insets navBarInsets = Utils.getNavbarInsets(mContext); + final int navBarHeight = navBarInsets.bottom; + final int navBarWidth; + if (mPanelController.getPosition() == AuthPanelController.POSITION_LEFT) { + navBarWidth = navBarInsets.left; + } else if (mPanelController.getPosition() == AuthPanelController.POSITION_RIGHT) { + navBarWidth = navBarInsets.right; + } else { + navBarWidth = 0; + } + + // The actual auth dialog w/h should include navigation bar size. + if (navBarWidth != 0 || navBarHeight != 0) { + mLayoutParams = new AuthDialog.LayoutParams( + mLayoutParams.mMediumWidth + navBarWidth, + mLayoutParams.mMediumHeight + navBarInsets.bottom); + } + setMeasuredDimension(mLayoutParams.mMediumWidth, mLayoutParams.mMediumHeight); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index ce85124fa7a7..e775c2e2a3fa 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -19,6 +19,7 @@ package com.android.systemui.biometrics; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION; @@ -512,6 +513,9 @@ public class AuthContainerView extends LinearLayout @Override public void onOrientationChanged() { maybeUpdatePositionForUdfps(true /* invalidate */); + if (mBiometricView != null) { + mBiometricView.onOrientationChanged(); + } } @Override @@ -899,7 +903,9 @@ public class AuthContainerView extends LinearLayout windowFlags, PixelFormat.TRANSLUCENT); lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~WindowInsets.Type.ime()); + lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~WindowInsets.Type.ime() + & ~WindowInsets.Type.systemBars()); + lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; lp.setTitle("BiometricPrompt"); lp.accessibilityTitle = title; lp.dimAmount = BACKGROUND_DIM_AMOUNT; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt index 8edccf166efe..b72801d3b5fe 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt @@ -21,7 +21,7 @@ constructor( fun enable(onPanelInteraction: Runnable) { if (action == null) { action = Action(onPanelInteraction) - shadeExpansionStateManager.addShadeExpansionListener(this::onPanelExpansionChanged) + shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) } else { Log.e(TAG, "Already enabled") } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java index 5c616f005d4d..ad100716eceb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java @@ -20,6 +20,7 @@ import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.content.Context; +import android.graphics.Insets; import android.graphics.Outline; import android.util.Log; import android.view.View; @@ -64,13 +65,12 @@ public class AuthPanelController extends ViewOutlineProvider { @Override public void getOutline(View view, Outline outline) { final int left = getLeftBound(mPosition); - final int right = left + mContentWidth; + final int right = getRightBound(mPosition, left); // If the content fits in the container, shrink the height to wrap it. Otherwise, expand to // fill the display (minus the margin), since the content is scrollable. final int top = getTopBound(mPosition); - final int bottom = Math.min(top + mContentHeight, mContainerHeight - mMargin); - + final int bottom = getBottomBound(top); outline.setRoundRect(left, top, right, bottom, mCornerRadius); } @@ -79,6 +79,10 @@ public class AuthPanelController extends ViewOutlineProvider { case POSITION_BOTTOM: return (mContainerWidth - mContentWidth) / 2; case POSITION_LEFT: + if (!mUseFullScreen) { + final Insets navBarInsets = Utils.getNavbarInsets(mContext); + return mMargin + navBarInsets.left; + } return mMargin; case POSITION_RIGHT: return mContainerWidth - mContentWidth - mMargin; @@ -88,6 +92,27 @@ public class AuthPanelController extends ViewOutlineProvider { } } + private int getRightBound(@Position int position, int left) { + if (!mUseFullScreen) { + final Insets navBarInsets = Utils.getNavbarInsets(mContext); + if (position == POSITION_RIGHT) { + return left + mContentWidth - navBarInsets.right; + } else if (position == POSITION_LEFT) { + return left + mContentWidth - navBarInsets.left; + } + } + return left + mContentWidth; + } + + private int getBottomBound(int top) { + if (!mUseFullScreen) { + final Insets navBarInsets = Utils.getNavbarInsets(mContext); + return Math.min(top + mContentHeight - navBarInsets.bottom, + mContainerHeight - mMargin - navBarInsets.bottom); + } + return Math.min(top + mContentHeight, mContainerHeight - mMargin); + } + private int getTopBound(@Position int position) { switch (position) { case POSITION_BOTTOM: @@ -113,6 +138,10 @@ public class AuthPanelController extends ViewOutlineProvider { mPosition = position; } + public @Position int getPosition() { + return mPosition; + } + public void setUseFullScreen(boolean fullScreen) { mUseFullScreen = fullScreen; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java index 4b17be3c45d4..6db266f4f1cb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java @@ -29,7 +29,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.biometrics.BiometricFaceConstants; -import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricSourceType; import android.os.Handler; import android.os.UserHandle; @@ -43,6 +42,9 @@ import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.statusbar.policy.KeyguardStateController; + +import java.util.Optional; + import javax.inject.Inject; /** @@ -66,6 +68,7 @@ public class BiometricNotificationService implements CoreStartable { private final Handler mHandler; private final NotificationManager mNotificationManager; private final BiometricNotificationBroadcastReceiver mBroadcastReceiver; + private final FingerprintReEnrollNotification mFingerprintReEnrollNotification; private NotificationChannel mNotificationChannel; private boolean mFaceNotificationQueued; private boolean mFingerprintNotificationQueued; @@ -102,8 +105,15 @@ public class BiometricNotificationService implements CoreStartable { Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.FACE_UNLOCK_RE_ENROLL, REENROLL_REQUIRED, UserHandle.USER_CURRENT); - } else if (msgId == BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL - && biometricSourceType == BiometricSourceType.FINGERPRINT) { + } + } + + @Override + public void onBiometricHelp(int msgId, String helpString, + BiometricSourceType biometricSourceType) { + if (biometricSourceType == BiometricSourceType.FINGERPRINT + && mFingerprintReEnrollNotification.isFingerprintReEnrollRequired( + msgId)) { mFingerprintReenrollRequired = true; } } @@ -115,13 +125,16 @@ public class BiometricNotificationService implements CoreStartable { KeyguardUpdateMonitor keyguardUpdateMonitor, KeyguardStateController keyguardStateController, Handler handler, NotificationManager notificationManager, - BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver) { + BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver, + Optional<FingerprintReEnrollNotification> fingerprintReEnrollNotification) { mContext = context; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mKeyguardStateController = keyguardStateController; mHandler = handler; mNotificationManager = notificationManager; mBroadcastReceiver = biometricNotificationBroadcastReceiver; + mFingerprintReEnrollNotification = fingerprintReEnrollNotification.orElse( + new FingerprintReEnrollNotificationImpl()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java new file mode 100644 index 000000000000..ca94e9993f76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java @@ -0,0 +1,25 @@ +/* + * 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.biometrics; + +/** + * Checks if the fingerprint HAL has sent a re-enrollment request. + */ +public interface FingerprintReEnrollNotification { + /** Returns true if msgId corresponds to FINGERPRINT_ACQUIRED_RE_ENROLL. */ + boolean isFingerprintReEnrollRequired(int msgId); +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java new file mode 100644 index 000000000000..1f86bc6ae298 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +import android.hardware.biometrics.BiometricFingerprintConstants; + +/** + * Checks if the fingerprint HAL has sent a re-enrollment request. + */ +public class FingerprintReEnrollNotificationImpl implements FingerprintReEnrollNotification{ + @Override + public boolean isFingerprintReEnrollRequired(int msgId) { + return msgId == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_RE_ENROLL; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt index 962140fa2cff..07825377440b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt @@ -436,25 +436,36 @@ private fun LottieAnimationView.addOverlayDynamicColor( fun update() { val isKeyguard = reason == REASON_AUTH_KEYGUARD if (isKeyguard) { - val color = context.getColor(R.color.numpad_key_color_secondary) // match bouncer color + val color = + com.android.settingslib.Utils.getColorAttrDefaultColor( + context, + com.android.internal.R.attr.materialColorPrimaryFixed + ) + val outerRimColor = + com.android.settingslib.Utils.getColorAttrDefaultColor( + context, + com.android.internal.R.attr.materialColorPrimaryFixedDim + ) val chevronFill = com.android.settingslib.Utils.getColorAttrDefaultColor( context, - android.R.attr.textColorPrimaryInverse + com.android.internal.R.attr.materialColorOnPrimaryFixed ) - for (key in listOf(".blue600", ".blue400")) { - addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) - } + addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + } + addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP) } addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP) } - } else if (!isDarkMode(context)) { - addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP) + } else { + if (!isDarkMode(context)) { + addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP) + } } - } else if (isDarkMode(context)) { for (key in listOf(".blue600", ".blue400")) { addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) { PorterDuffColorFilter( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt index 1dbafc6519f0..94b5fb2861b1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt @@ -108,7 +108,9 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( } override fun onViewAttached() { - shadeExpansionStateManager.addExpansionListener(shadeExpansionListener) + val currentState = + shadeExpansionStateManager.addExpansionListener(shadeExpansionListener) + shadeExpansionListener.onPanelExpansionChanged(currentState) dialogManager.registerListener(dialogListener) dumpManager.registerDumpable(dumpTag, this) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index bc44df488d90..2eb533029cf5 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -466,7 +466,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { if (!mOnFingerDown) { playStartHaptic(); } - mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */); + mKeyguardViewManager.notifyKeyguardAuthenticated(false /* primaryAuth */); mAttemptedToDismissKeyguard = true; } @@ -636,7 +636,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mOverlay.getOverlayView().getViewRootImpl().getInputToken()); } - return processedTouch.getTouchData().isWithinSensor(mOverlayParams.getNativeSensorBounds()); + return processedTouch.getTouchData().isWithinBounds(mOverlayParams.getNativeSensorBounds()); } private boolean oldOnTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt index 5c88c9e49f35..3fc3e82b6b5a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt @@ -290,7 +290,8 @@ constructor( qsExpansion = keyguardViewManager.qsExpansion keyguardViewManager.addCallback(statusBarKeyguardViewManagerCallback) configurationController.addCallback(configurationListener) - shadeExpansionStateManager.addExpansionListener(shadeExpansionListener) + val currentState = shadeExpansionStateManager.addExpansionListener(shadeExpansionListener) + shadeExpansionListener.onPanelExpansionChanged(currentState) updateScaleFactor() view.updatePadding() updateAlpha() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java index 63f0e9dda633..a2840fc1dc8c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java @@ -176,9 +176,9 @@ public class UdfpsKeyguardViewLegacy extends UdfpsAnimationView { } mTextColorPrimary = Utils.getColorAttrDefaultColor(mContext, - android.R.attr.textColorPrimary); + com.android.internal.R.attr.materialColorOnSurface); final int backgroundColor = Utils.getColorAttrDefaultColor(getContext(), - com.android.internal.R.attr.colorSurface); + com.android.internal.R.attr.materialColorSurfaceContainerHigh); mBgProtection.setImageTintList(ColorStateList.valueOf(backgroundColor)); mLockScreenFp.invalidate(); // updated with a valueCallback } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt index d0d6f4cbf166..b538085fa40d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt @@ -26,13 +26,16 @@ import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING import android.content.Context import android.content.pm.PackageManager +import android.graphics.Insets import android.hardware.biometrics.BiometricManager.Authenticators import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.SensorPropertiesInternal import android.os.UserManager import android.util.DisplayMetrics import android.view.ViewGroup +import android.view.WindowInsets import android.view.WindowManager +import android.view.WindowMetrics import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityManager import com.android.internal.widget.LockPatternUtils @@ -114,6 +117,14 @@ object Utils { return hasPermission && "android" == clientPackage } + @JvmStatic + fun getNavbarInsets(context: Context): Insets { + val windowManager: WindowManager? = context.getSystemService(WindowManager::class.java) + val windowMetrics: WindowMetrics? = windowManager?.maximumWindowMetrics + return windowMetrics?.windowInsets?.getInsets(WindowInsets.Type.navigationBars()) + ?: Insets.NONE + } + @Retention(RetentionPolicy.SOURCE) @IntDef(CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD) internal annotation class CredentialType diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt index 79a0acb8bbc1..cf6044f146b0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetector.kt @@ -22,6 +22,11 @@ import com.android.systemui.dagger.SysUISingleton /** Returns whether the touch coordinates are within the sensor's bounding box. */ @SysUISingleton class BoundingBoxOverlapDetector : OverlapDetector { - override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean = - touchData.isWithinSensor(nativeSensorBounds) + override fun isGoodOverlap( + touchData: NormalizedTouchData, + nativeSensorBounds: Rect, + nativeOverlayBounds: Rect, + ): Boolean = + touchData.isWithinBounds(nativeOverlayBounds) && + touchData.isWithinBounds(nativeSensorBounds) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt index baf8d743938d..f70e01dc9050 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt @@ -30,8 +30,8 @@ private enum class SensorPixelPosition { TARGET // Pixel within sensor center target } -private val isDebug = true -private val TAG = "EllipseOverlapDetector" +private const val isDebug = false +private const val TAG = "EllipseOverlapDetector" /** * Approximates the touch as an ellipse and determines whether the ellipse has a sufficient overlap @@ -39,12 +39,21 @@ private val TAG = "EllipseOverlapDetector" */ @SysUISingleton class EllipseOverlapDetector(private val params: EllipseOverlapDetectorParams) : OverlapDetector { - override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean { - // First, check if touch is within bounding box, - if (nativeSensorBounds.contains(touchData.x.toInt(), touchData.y.toInt())) { + override fun isGoodOverlap( + touchData: NormalizedTouchData, + nativeSensorBounds: Rect, + nativeOverlayBounds: Rect, + ): Boolean { + // First, check if touch is within bounding box to exit early + if (touchData.isWithinBounds(nativeSensorBounds)) { return true } + // Check touch is within overlay bounds, not worth checking if outside + if (!touchData.isWithinBounds(nativeOverlayBounds)) { + return false + } + var isTargetTouched = false var sensorPixels = 0 var coveredPixels = 0 @@ -77,7 +86,7 @@ class EllipseOverlapDetector(private val params: EllipseOverlapDetectorParams) : val percentage: Float = coveredPixels.toFloat() / sensorPixels if (isDebug) { - Log.v( + Log.d( TAG, "covered: $coveredPixels, sensor: $sensorPixels, " + "percentage: $percentage, isCenterTouched: $isTargetTouched" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt index 6854b50370ba..1b4062a4e9bc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/NormalizedTouchData.kt @@ -51,16 +51,16 @@ data class NormalizedTouchData( ) { /** - * [nativeSensorBounds] contains the location and dimensions of the sensor area in native - * resolution and natural orientation. + * [nativeBounds] contains the location and dimensions of the area in native resolution and + * natural orientation. * - * Returns whether the coordinates of the given pointer are within the sensor's bounding box. + * Returns whether the coordinates of the given pointer are within the bounding box. */ - fun isWithinSensor(nativeSensorBounds: Rect): Boolean { - return nativeSensorBounds.left <= x && - nativeSensorBounds.right >= x && - nativeSensorBounds.top <= y && - nativeSensorBounds.bottom >= y + fun isWithinBounds(nativeBounds: Rect): Boolean { + return nativeBounds.left <= x && + nativeBounds.right >= x && + nativeBounds.top <= y && + nativeBounds.bottom >= y } @JvmOverloads diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt index 0fec8ffbaa0a..f16347159010 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/OverlapDetector.kt @@ -20,5 +20,9 @@ import android.graphics.Rect /** Determines whether the touch has a sufficient overlap with the sensor. */ interface OverlapDetector { - fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean + fun isGoodOverlap( + touchData: NormalizedTouchData, + nativeSensorBounds: Rect, + nativeOverlayBounds: Rect + ): Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt index 6c9390ddad7d..eeb0f4c7bb13 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt @@ -46,7 +46,13 @@ class SinglePointerTouchProcessor @Inject constructor(val overlapDetector: Overl val touchData = List(event.pointerCount) { event.normalize(it, overlayParams) } val pointersOnSensor = touchData - .filter { overlapDetector.isGoodOverlap(it, overlayParams.nativeSensorBounds) } + .filter { + overlapDetector.isGoodOverlap( + it, + overlayParams.nativeSensorBounds, + overlayParams.nativeOverlayBounds + ) + } .map { it.pointerId } return PreprocessedTouch(touchData, previousPointerOnSensorId, pointersOnSensor) } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt new file mode 100644 index 000000000000..4c817b2e46a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt @@ -0,0 +1,35 @@ +/* + * 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.bouncer.data.repo + +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Provides access to bouncer-related application state. */ +@SysUISingleton +class BouncerRepository @Inject constructor() { + private val _message = MutableStateFlow<String?>(null) + /** The user-facing message to show in the bouncer. */ + val message: StateFlow<String?> = _message.asStateFlow() + + fun setMessage(message: String?) { + _message.value = message + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt new file mode 100644 index 000000000000..8264fed4846a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -0,0 +1,172 @@ +/* + * 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.bouncer.domain.interactor + +import android.content.Context +import com.android.systemui.R +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.dagger.qualifiers.Application +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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** Encapsulates business logic and application state accessing use-cases. */ +class BouncerInteractor +@AssistedInject +constructor( + @Application private val applicationScope: CoroutineScope, + @Application private val applicationContext: Context, + private val repository: BouncerRepository, + private val authenticationInteractor: AuthenticationInteractor, + private val sceneInteractor: SceneInteractor, + @Assisted private val containerName: String, +) { + + /** The user-facing message to show in the bouncer. */ + val message: StateFlow<String?> = repository.message + + /** + * The currently-configured authentication method. This determines how the authentication + * challenge is completed in order to unlock an otherwise locked device. + */ + val authenticationMethod: StateFlow<AuthenticationMethodModel> = + authenticationInteractor.authenticationMethod + + init { + applicationScope.launch { + combine( + sceneInteractor.currentScene(containerName), + authenticationInteractor.authenticationMethod, + ::Pair, + ) + .collect { (currentScene, authMethod) -> + if (currentScene.key == SceneKey.Bouncer) { + when (authMethod) { + is AuthenticationMethodModel.None -> + sceneInteractor.setCurrentScene( + containerName, + SceneModel(SceneKey.Gone), + ) + is AuthenticationMethodModel.Swipe -> + sceneInteractor.setCurrentScene( + containerName, + SceneModel(SceneKey.Lockscreen), + ) + else -> Unit + } + } + } + } + } + + /** + * Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown. + * + * @param containerName The name of the scene container to show the bouncer in. + * @param message An optional message to show to the user in the bouncer. + */ + fun showOrUnlockDevice( + containerName: String, + message: String? = null, + ) { + if (authenticationInteractor.isAuthenticationRequired()) { + repository.setMessage(message ?: promptMessage(authenticationMethod.value)) + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Bouncer), + ) + } else { + authenticationInteractor.unlockDevice() + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Gone), + ) + } + } + + /** + * Resets the user-facing message back to the default according to the current authentication + * method. + */ + fun resetMessage() { + repository.setMessage(promptMessage(authenticationMethod.value)) + } + + /** Removes the user-facing message. */ + fun clearMessage() { + repository.setMessage(null) + } + + /** + * Attempts to authenticate based on the given user input. + * + * If the input is correct, the device will be unlocked and the lock screen and bouncer will be + * dismissed and hidden. + */ + fun authenticate( + input: List<Any>, + ) { + val isAuthenticated = authenticationInteractor.authenticate(input) + if (isAuthenticated) { + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Gone), + ) + } else { + repository.setMessage(errorMessage(authenticationMethod.value)) + } + } + + private fun promptMessage(authMethod: AuthenticationMethodModel): String { + return when (authMethod) { + is AuthenticationMethodModel.PIN -> + applicationContext.getString(R.string.keyguard_enter_your_pin) + is AuthenticationMethodModel.Password -> + applicationContext.getString(R.string.keyguard_enter_your_password) + is AuthenticationMethodModel.Pattern -> + applicationContext.getString(R.string.keyguard_enter_your_pattern) + else -> "" + } + } + + private fun errorMessage(authMethod: AuthenticationMethodModel): String { + return when (authMethod) { + is AuthenticationMethodModel.PIN -> applicationContext.getString(R.string.kg_wrong_pin) + is AuthenticationMethodModel.Password -> + applicationContext.getString(R.string.kg_wrong_password) + is AuthenticationMethodModel.Pattern -> + applicationContext.getString(R.string.kg_wrong_pattern) + else -> "" + } + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): BouncerInteractor + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt new file mode 100644 index 000000000000..ebefb78f0477 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -0,0 +1,19 @@ +/* + * 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.bouncer.ui.viewmodel + +sealed interface AuthMethodBouncerViewModel diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt new file mode 100644 index 000000000000..eaa8ed5b358e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -0,0 +1,97 @@ +/* + * 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.bouncer.ui.viewmodel + +import android.content.Context +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.dagger.qualifiers.Application +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Holds UI state and handles user input on bouncer UIs. */ +class BouncerViewModel +@AssistedInject +constructor( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + interactorFactory: BouncerInteractor.Factory, + containerName: String, +) { + private val interactor: BouncerInteractor = interactorFactory.create(containerName) + + private val pin: PinBouncerViewModel by lazy { + PinBouncerViewModel( + applicationScope = applicationScope, + interactor = interactor, + ) + } + + private val password: PasswordBouncerViewModel by lazy { + PasswordBouncerViewModel( + interactor = interactor, + ) + } + + private val pattern: PatternBouncerViewModel by lazy { + PatternBouncerViewModel( + applicationContext = applicationContext, + applicationScope = applicationScope, + interactor = interactor, + ) + } + + /** View-model for the current UI, based on the current authentication method. */ + val authMethod: StateFlow<AuthMethodBouncerViewModel?> = + interactor.authenticationMethod + .map { authMethod -> toViewModel(authMethod) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = toViewModel(interactor.authenticationMethod.value), + ) + + /** The user-facing message to show in the bouncer. */ + val message: StateFlow<String> = + interactor.message + .map { it ?: "" } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = interactor.message.value ?: "", + ) + + /** Notifies that the emergency services button was clicked. */ + fun onEmergencyServicesButtonClicked() { + // TODO(b/280877228): implement this + } + + private fun toViewModel( + authMethod: AuthenticationMethodModel, + ): AuthMethodBouncerViewModel? { + return when (authMethod) { + is AuthenticationMethodModel.PIN -> pin + is AuthenticationMethodModel.Password -> password + is AuthenticationMethodModel.Pattern -> pattern + else -> null + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt new file mode 100644 index 000000000000..730d4e8ba050 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.ui.viewmodel + +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Holds UI state and handles user input for the password bouncer UI. */ +class PasswordBouncerViewModel( + private val interactor: BouncerInteractor, +) : AuthMethodBouncerViewModel { + + private val _password = MutableStateFlow("") + /** The password entered so far. */ + val password: StateFlow<String> = _password.asStateFlow() + + /** Notifies that the UI has been shown to the user. */ + fun onShown() { + interactor.resetMessage() + } + + /** Notifies that the user has changed the password input. */ + fun onPasswordInputChanged(password: String) { + if (this.password.value.isEmpty() && password.isNotEmpty()) { + interactor.clearMessage() + } + + _password.value = password + } + + /** Notifies that the user has pressed the key for attempting to authenticate the password. */ + fun onAuthenticateKeyPressed() { + interactor.authenticate(password.value.toCharArray().toList()) + _password.value = "" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt new file mode 100644 index 000000000000..eb1b45771ad4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt @@ -0,0 +1,189 @@ +/* + * 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.bouncer.ui.viewmodel + +import android.content.Context +import android.util.TypedValue +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.sqrt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Holds UI state and handles user input for the pattern bouncer UI. */ +class PatternBouncerViewModel( + private val applicationContext: Context, + applicationScope: CoroutineScope, + private val interactor: BouncerInteractor, +) : AuthMethodBouncerViewModel { + + /** The number of columns in the dot grid. */ + val columnCount = 3 + /** The number of rows in the dot grid. */ + val rowCount = 3 + + private val _selectedDots = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf()) + /** The dots that were selected by the user, in the order of selection. */ + val selectedDots: StateFlow<List<PatternDotViewModel>> = + _selectedDots + .map { it.toList() } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList(), + ) + + private val _currentDot = MutableStateFlow<PatternDotViewModel?>(null) + /** The most-recently selected dot that the user selected. */ + val currentDot: StateFlow<PatternDotViewModel?> = _currentDot.asStateFlow() + + private val _dots = MutableStateFlow(defaultDots()) + /** All dots on the grid. */ + val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow() + + /** Notifies that the UI has been shown to the user. */ + fun onShown() { + interactor.resetMessage() + } + + /** Notifies that the user has started a drag gesture across the dot grid. */ + fun onDragStart() { + interactor.clearMessage() + } + + /** + * Notifies that the user is dragging across the dot grid. + * + * @param xPx The horizontal coordinate of the position of the user's pointer, in pixels. + * @param yPx The vertical coordinate of the position of the user's pointer, in pixels. + * @param containerSizePx The size of the container of the dot grid, in pixels. It's assumed + * that the dot grid is perfectly square such that width and height are equal. + * @param verticalOffsetPx How far down from `0` does the dot grid start on the display. + */ + fun onDrag(xPx: Float, yPx: Float, containerSizePx: Int, verticalOffsetPx: Float) { + val cellWidthPx = containerSizePx / columnCount + val cellHeightPx = containerSizePx / rowCount + + if (xPx < 0 || yPx < verticalOffsetPx) { + return + } + + val dotColumn = (xPx / cellWidthPx).toInt() + val dotRow = ((yPx - verticalOffsetPx) / cellHeightPx).toInt() + if (dotColumn > columnCount - 1 || dotRow > rowCount - 1) { + return + } + + val dotPixelX = dotColumn * cellWidthPx + cellWidthPx / 2 + val dotPixelY = dotRow * cellHeightPx + cellHeightPx / 2 + verticalOffsetPx + + val distance = sqrt((xPx - dotPixelX).pow(2) + (yPx - dotPixelY).pow(2)) + val hitRadius = hitFactor * min(cellWidthPx, cellHeightPx) / 2 + if (distance > hitRadius) { + return + } + + val hitDot = dots.value.firstOrNull { dot -> dot.x == dotColumn && dot.y == dotRow } + if (hitDot != null && !_selectedDots.value.contains(hitDot)) { + val skippedOverDots = + currentDot.value?.let { previousDot -> + buildList { + var dot = previousDot + while (dot != hitDot) { + add(dot) + dot = + PatternDotViewModel( + x = + if (hitDot.x > dot.x) dot.x + 1 + else if (hitDot.x < dot.x) dot.x - 1 else dot.x, + y = + if (hitDot.y > dot.y) dot.y + 1 + else if (hitDot.y < dot.y) dot.y - 1 else dot.y, + ) + } + } + } + ?: emptyList() + + _selectedDots.value = + linkedSetOf<PatternDotViewModel>().apply { + addAll(_selectedDots.value) + addAll(skippedOverDots) + add(hitDot) + } + _currentDot.value = hitDot + } + } + + /** Notifies that the user has ended the drag gesture across the dot grid. */ + fun onDragEnd() { + interactor.authenticate(_selectedDots.value.map { it.toCoordinate() }) + + _dots.value = defaultDots() + _currentDot.value = null + _selectedDots.value = linkedSetOf() + } + + private fun defaultDots(): List<PatternDotViewModel> { + return buildList { + (0 until columnCount).forEach { x -> + (0 until rowCount).forEach { y -> + add( + PatternDotViewModel( + x = x, + y = y, + ) + ) + } + } + } + } + + private val hitFactor: Float by lazy { + val outValue = TypedValue() + applicationContext.resources.getValue( + com.android.internal.R.dimen.lock_pattern_dot_hit_factor, + outValue, + true + ) + max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR) + } + + companion object { + private const val MIN_DOT_HIT_FACTOR = 0.2f + } +} + +data class PatternDotViewModel( + val x: Int, + val y: Int, +) { + fun toCoordinate(): AuthenticationMethodModel.Pattern.PatternCoordinate { + return AuthenticationMethodModel.Pattern.PatternCoordinate( + x = x, + y = y, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt new file mode 100644 index 000000000000..f9223cb0872e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -0,0 +1,101 @@ +/* + * 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.bouncer.ui.viewmodel + +import androidx.annotation.VisibleForTesting +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.util.kotlin.pairwise +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Holds UI state and handles user input for the PIN code bouncer UI. */ +class PinBouncerViewModel( + private val applicationScope: CoroutineScope, + private val interactor: BouncerInteractor, +) : AuthMethodBouncerViewModel { + + private val entered = MutableStateFlow<List<Int>>(emptyList()) + /** + * The length of the PIN digits that were input so far, two values are supplied the previous and + * the current. + */ + val pinLengths: StateFlow<Pair<Int, Int>> = + entered + .pairwise() + .map { it.previousValue.size to it.newValue.size } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = 0 to 0, + ) + private var resetPinJob: Job? = null + + /** Notifies that the UI has been shown to the user. */ + fun onShown() { + interactor.resetMessage() + } + + /** Notifies that the user clicked on a PIN button with the given digit value. */ + fun onPinButtonClicked(input: Int) { + resetPinJob?.cancel() + resetPinJob = null + + if (entered.value.isEmpty()) { + interactor.clearMessage() + } + + entered.value += input + } + + /** Notifies that the user clicked the backspace button. */ + fun onBackspaceButtonClicked() { + if (entered.value.isEmpty()) { + return + } + + entered.value = entered.value.toMutableList().apply { removeLast() } + } + + /** Notifies that the user long-pressed the backspace button. */ + fun onBackspaceButtonLongPressed() { + resetPinJob?.cancel() + resetPinJob = + applicationScope.launch { + while (entered.value.isNotEmpty()) { + onBackspaceButtonClicked() + delay(BACKSPACE_LONG_PRESS_DELAY_MS) + } + } + } + + /** Notifies that the user clicked the "enter" button. */ + fun onAuthenticateButtonClicked() { + interactor.authenticate(entered.value) + entered.value = emptyList() + } + + companion object { + @VisibleForTesting const val BACKSPACE_LONG_PRESS_DELAY_MS = 80L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java index f973aeef0e6b..4d99282e6f8c 100644 --- a/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java +++ b/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java @@ -24,6 +24,7 @@ import static com.android.systemui.controls.dagger.ControlsComponent.Visibility. import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import android.util.Log; import android.view.View; import android.widget.ImageView; @@ -39,6 +40,7 @@ import com.android.systemui.controls.dagger.ControlsComponent; import com.android.systemui.controls.management.ControlsListingController; import com.android.systemui.controls.ui.ControlsActivity; import com.android.systemui.controls.ui.ControlsUiController; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.SystemUser; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.plugins.ActivityStarter; @@ -56,17 +58,20 @@ import javax.inject.Named; * devices at home like lights and thermostats). */ public class DreamHomeControlsComplication implements Complication { + private final Resources mResources; private final DreamHomeControlsComplicationComponent.Factory mComponentFactory; @Inject public DreamHomeControlsComplication( + @Main Resources resources, DreamHomeControlsComplicationComponent.Factory componentFactory) { + mResources = resources; mComponentFactory = componentFactory; } @Override public ViewHolder createView(ComplicationViewModel model) { - return mComponentFactory.create().getViewHolder(); + return mComponentFactory.create(mResources).getViewHolder(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamHomeControlsComplicationComponent.java b/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamHomeControlsComplicationComponent.java index ef18d660ef36..2b5aa7cd9c51 100644 --- a/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamHomeControlsComplicationComponent.java +++ b/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamHomeControlsComplicationComponent.java @@ -18,12 +18,19 @@ package com.android.systemui.complication.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.widget.ImageView; +import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.complication.DreamHomeControlsComplication; +import com.android.systemui.shared.shadow.DoubleShadowIconDrawable; +import com.android.systemui.shared.shadow.DoubleShadowTextHelper; +import dagger.BindsInstance; import dagger.Module; import dagger.Provides; import dagger.Subcomponent; @@ -59,7 +66,7 @@ public interface DreamHomeControlsComplicationComponent { */ @Subcomponent.Factory interface Factory { - DreamHomeControlsComplicationComponent create(); + DreamHomeControlsComplicationComponent create(@BindsInstance Resources resources); } /** @@ -68,6 +75,7 @@ public interface DreamHomeControlsComplicationComponent { @Module interface DreamHomeControlsModule { String DREAM_HOME_CONTROLS_CHIP_VIEW = "dream_home_controls_chip_view"; + String DREAM_HOME_CONTROLS_BACKGROUND_DRAWABLE = "dream_home_controls_background_drawable"; /** * Provides the dream home controls chip view. @@ -75,9 +83,56 @@ public interface DreamHomeControlsComplicationComponent { @Provides @DreamHomeControlsComplicationScope @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) - static ImageView provideHomeControlsChipView(LayoutInflater layoutInflater) { - return (ImageView) layoutInflater.inflate(R.layout.dream_overlay_home_controls_chip, - null, false); + static ImageView provideHomeControlsChipView( + LayoutInflater layoutInflater, + @Named(DREAM_HOME_CONTROLS_BACKGROUND_DRAWABLE) Drawable backgroundDrawable) { + final ImageView chip = + (ImageView) layoutInflater.inflate(R.layout.dream_overlay_home_controls_chip, + null, false); + chip.setBackground(backgroundDrawable); + + return chip; + } + + @Provides + @DreamHomeControlsComplicationScope + @Named(DREAM_HOME_CONTROLS_BACKGROUND_DRAWABLE) + static Drawable providesHomeControlsBackground(Context context, Resources resources) { + final Drawable background = new DoubleShadowIconDrawable(createShadowInfo( + resources, + R.dimen.dream_overlay_bottom_affordance_key_text_shadow_radius, + R.dimen.dream_overlay_bottom_affordance_key_text_shadow_dx, + R.dimen.dream_overlay_bottom_affordance_key_text_shadow_dy, + R.dimen.dream_overlay_bottom_affordance_key_shadow_alpha + ), + createShadowInfo( + resources, + R.dimen.dream_overlay_bottom_affordance_ambient_text_shadow_radius, + R.dimen.dream_overlay_bottom_affordance_ambient_text_shadow_dx, + R.dimen.dream_overlay_bottom_affordance_ambient_text_shadow_dy, + R.dimen.dream_overlay_bottom_affordance_ambient_shadow_alpha + ), + resources.getDrawable(R.drawable.dream_overlay_bottom_affordance_bg), + resources.getDimensionPixelOffset( + R.dimen.dream_overlay_bottom_affordance_width), + resources.getDimensionPixelSize(R.dimen.dream_overlay_bottom_affordance_inset) + ); + + background.setTintList( + Utils.getColorAttr(context, com.android.internal.R.attr.colorSurface)); + + return background; + } + + private static DoubleShadowTextHelper.ShadowInfo createShadowInfo(Resources resources, + int blurId, int offsetXId, int offsetYId, int alphaId) { + + return new DoubleShadowTextHelper.ShadowInfo( + resources.getDimension(blurId), + resources.getDimension(offsetXId), + resources.getDimension(offsetYId), + resources.getFloat(alphaId) + ); } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 92607c6101af..d73c85b0803b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -328,6 +328,13 @@ class ControlsUiControllerImpl @Inject constructor ( @VisibleForTesting internal fun startRemovingApp(componentName: ComponentName, appName: CharSequence) { + activityStarter.dismissKeyguardThenExecute({ + showAppRemovalDialog(componentName, appName) + true + }, null, true) + } + + private fun showAppRemovalDialog(componentName: ComponentName, appName: CharSequence) { removeAppDialog?.cancel() removeAppDialog = dialogsFactory.createRemoveAppDialog(context, appName) { shouldRemove -> if (!shouldRemove || !controlsController.get().removeFavorites(componentName)) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index dba353b0d70e..8d5a2dd8df25 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -32,6 +32,7 @@ import com.android.systemui.sensorprivacy.television.TvSensorPrivacyChangedActiv import com.android.systemui.sensorprivacy.television.TvUnblockSensorActivity; import com.android.systemui.settings.brightness.BrightnessDialog; import com.android.systemui.statusbar.tv.notifications.TvNotificationPanelActivity; +import com.android.systemui.telephony.ui.activity.SwitchToManagedProfileForCallActivity; import com.android.systemui.tuner.TunerActivity; import com.android.systemui.usb.UsbAccessoryUriActivity; import com.android.systemui.usb.UsbConfirmActivity; @@ -178,4 +179,11 @@ public abstract class DefaultActivityBinder { @ClassKey(TvSensorPrivacyChangedActivity.class) public abstract Activity bindTvSensorPrivacyChangedActivity( TvSensorPrivacyChangedActivity activity); + + /** Inject into SwitchToManagedProfileForCallActivity. */ + @Binds + @IntoMap + @ClassKey(SwitchToManagedProfileForCallActivity.class) + public abstract Activity bindSwitchToManagedProfileForCallActivity( + SwitchToManagedProfileForCallActivity activity); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java b/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java index 8764297c1b5d..a3e26b881a3b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/PluginModule.java @@ -16,17 +16,13 @@ package com.android.systemui.dagger; -import com.android.systemui.ActivityStarterDelegate; import com.android.systemui.classifier.FalsingManagerProxy; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.globalactions.GlobalActionsComponent; import com.android.systemui.globalactions.GlobalActionsImpl; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.GlobalActions; -import com.android.systemui.plugins.PluginDependencyProvider; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarStateControllerImpl; @@ -36,7 +32,6 @@ import com.android.systemui.volume.VolumeDialogControllerImpl; import dagger.Binds; import dagger.Module; -import dagger.Provides; /** * Module for binding Plugin implementations. @@ -47,16 +42,8 @@ import dagger.Provides; public abstract class PluginModule { /** */ - @Provides - static ActivityStarter provideActivityStarter(ActivityStarterDelegate delegate, - PluginDependencyProvider dependencyProvider, ActivityStarterImpl activityStarterImpl, - FeatureFlags featureFlags) { - if (featureFlags.isEnabled(Flags.USE_NEW_ACTIVITY_STARTER)) { - return activityStarterImpl; - } - dependencyProvider.allowPluginDependency(ActivityStarter.class, delegate); - return delegate; - } + @Binds + abstract ActivityStarter provideActivityStarter(ActivityStarterImpl activityStarterImpl); /** */ @Binds diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index f68bd49230d9..2262d8ab2000 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -40,6 +40,7 @@ import com.android.systemui.qs.tileimpl.QSFactoryImpl; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsImplementation; import com.android.systemui.rotationlock.RotationLockModule; +import com.android.systemui.scene.SceneContainerFrameworkModule; import com.android.systemui.screenshot.ReferenceScreenshotModule; import com.android.systemui.settings.dagger.MultiUserUtilsModule; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; @@ -103,6 +104,7 @@ import javax.inject.Named; QSModule.class, ReferenceScreenshotModule.class, RotationLockModule.class, + SceneContainerFrameworkModule.class, StatusBarEventsModule.class, StartCentralSurfacesModule.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 1a0fcea6ca87..52355f34a71d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -42,6 +42,7 @@ import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.recents.RecentTasks; @@ -104,6 +105,9 @@ public interface SysUIComponent { Builder setTransitions(ShellTransitions t); @BindsInstance + Builder setKeyguardTransitions(KeyguardTransitions k); + + @BindsInstance Builder setStartingSurface(Optional<StartingSurface> s); @BindsInstance diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 5d6479ef4532..25634f009fc7 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -17,7 +17,6 @@ package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger -import com.android.systemui.ChooserSelector import com.android.systemui.CoreStartable import com.android.systemui.LatencyTester import com.android.systemui.ScreenDecorations @@ -55,6 +54,7 @@ import com.android.systemui.theme.ThemeOverlayController import com.android.systemui.toast.ToastUI import com.android.systemui.usb.StorageNotification import com.android.systemui.util.NotificationChannels +import com.android.systemui.util.StartBinderLoggerModule import com.android.systemui.volume.VolumeUI import com.android.systemui.wmshell.WMShell import dagger.Binds @@ -68,6 +68,7 @@ import dagger.multibindings.IntoMap @Module(includes = [ MultiUserUtilsModule::class, StartControlsStartableModule::class, + StartBinderLoggerModule::class, ]) abstract class SystemUICoreStartableModule { /** Inject into AuthController. */ @@ -84,12 +85,6 @@ abstract class SystemUICoreStartableModule { service: BiometricNotificationService ): CoreStartable - /** Inject into ChooserCoreStartable. */ - @Binds - @IntoMap - @ClassKey(ChooserSelector::class) - abstract fun bindChooserSelector(sysui: ChooserSelector): CoreStartable - /** Inject into ClipboardListener. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 17cf8084df1f..5bcf32a9ebdb 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -32,8 +32,10 @@ import com.android.systemui.accessibility.AccessibilityModule; import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule; import com.android.systemui.appops.dagger.AppOpsModule; import com.android.systemui.assist.AssistModule; +import com.android.systemui.authentication.AuthenticationModule; import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; +import com.android.systemui.biometrics.FingerprintReEnrollNotification; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.biometrics.dagger.UdfpsModule; @@ -53,6 +55,8 @@ import com.android.systemui.flags.FlagsModule; import com.android.systemui.keyboard.KeyboardModule; import com.android.systemui.keyguard.data.BouncerViewModule; import com.android.systemui.log.dagger.LogModule; +import com.android.systemui.log.dagger.MonitorLog; +import com.android.systemui.log.table.TableLogBuffer; import com.android.systemui.mediaprojection.appselector.MediaProjectionModule; import com.android.systemui.model.SysUiState; import com.android.systemui.motiontool.MotionToolModule; @@ -152,6 +156,7 @@ import javax.inject.Named; AccessibilityRepositoryModule.class, AppOpsModule.class, AssistModule.class, + AuthenticationModule.class, BiometricsModule.class, BouncerViewModule.class, ClipboardOverlayModule.class, @@ -247,8 +252,8 @@ public abstract class SystemUIModule { @Provides @SystemUser static Monitor provideSystemUserMonitor(@Main Executor executor, - SystemProcessCondition systemProcessCondition) { - return new Monitor(executor, Collections.singleton(systemProcessCondition)); + SystemProcessCondition systemProcessCondition, @MonitorLog TableLogBuffer logBuffer) { + return new Monitor(executor, Collections.singleton(systemProcessCondition), logBuffer); } @BindsOptionalOf @@ -289,6 +294,9 @@ public abstract class SystemUIModule { @BindsOptionalOf abstract SystemStatusAnimationScheduler optionalSystemStatusAnimationScheduler(); + @BindsOptionalOf + abstract FingerprintReEnrollNotification optionalFingerprintReEnrollNotification(); + @SysUISingleton @Binds abstract SystemClock bindSystemClock(SystemClockImpl systemClock); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java index 17d2332a4dac..b71871ebdb55 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java @@ -31,6 +31,7 @@ import com.android.wm.shell.dagger.WMShellModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; +import com.android.wm.shell.keyguard.KeyguardTransitions; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.recents.RecentTasks; @@ -99,6 +100,9 @@ public interface WMComponent { ShellTransitions getTransitions(); @WMSingleton + KeyguardTransitions getKeyguardTransitions(); + + @WMSingleton Optional<StartingSurface> getStartingSurface(); @WMSingleton diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index e068962130c9..524cfaa0dc29 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -63,7 +63,7 @@ object Flags { // TODO(b/279735475): Tracking Bug @JvmField - val NEW_LIGHT_BAR_LOGIC = unreleasedFlag(279735475, "new_light_bar_logic", teamfood = true) + val NEW_LIGHT_BAR_LOGIC = releasedFlag(279735475, "new_light_bar_logic") /** * This flag is server-controlled and should stay as [unreleasedFlag] since we never want to @@ -120,7 +120,12 @@ object Flags { resourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher, "bouncer_user_switcher") // TODO(b/254512676): Tracking Bug - @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = releasedFlag(207, "lockscreen_custom_clocks") + @JvmField + val LOCKSCREEN_CUSTOM_CLOCKS = resourceBooleanFlag( + 207, + R.bool.config_enableLockScreenCustomClocks, + "lockscreen_custom_clocks" + ) // TODO(b/275694445): Tracking Bug @JvmField @@ -235,7 +240,12 @@ object Flags { /** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */ // TODO(b/279794160): Tracking bug. @JvmField - val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer") + val DELAY_BOUNCER = releasedFlag(235, "delay_bouncer") + + /** Migrate the indication area to the new keyguard root view. */ + // TODO(b/280067944): Tracking bug. + @JvmField + val MIGRATE_INDICATION_AREA = unreleasedFlag(236, "migrate_indication_area") // 300 - power menu // TODO(b/254512600): Tracking Bug @@ -333,8 +343,7 @@ object Flags { // TODO(b/280426085): Tracking Bug @JvmField - val NEW_BLUETOOTH_REPOSITORY = - unreleasedFlag(612, "new_bluetooth_repository", teamfood = true) + val NEW_BLUETOOTH_REPOSITORY = unreleasedFlag(612, "new_bluetooth_repository") // 700 - dialer/calls // TODO(b/254512734): Tracking Bug @@ -524,6 +533,13 @@ object Flags { @JvmField val LOCKSCREEN_LIVE_WALLPAPER = sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = false) + // TODO(b/281648899): Tracking bug + @Keep + @JvmField + val WALLPAPER_MULTI_CROP = + sysPropBooleanFlag(1118, "persist.wm.debug.wallpaper_multi_crop", default = false) + + // 1200 - predictive back @Keep @JvmField @@ -550,7 +566,7 @@ object Flags { // TODO(b/270987164): Tracking Bug @JvmField - val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features") + val TRACKPAD_GESTURE_FEATURES = unreleasedFlag(1205, "trackpad_gesture_features", teamfood = true) // TODO(b/263826204): Tracking Bug @JvmField @@ -601,7 +617,8 @@ object Flags { // 1700 - clipboard @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") // TODO(b/278714186) Tracking Bug - @JvmField val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout") + @JvmField val CLIPBOARD_IMAGE_TIMEOUT = + unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true) // TODO(b/279405451): Tracking Bug @JvmField val CLIPBOARD_SHARED_TRANSITIONS = unreleasedFlag(1703, "clipboard_shared_transitions") @@ -653,15 +670,6 @@ object Flags { val WARN_ON_BLOCKING_BINDER_TRANSACTIONS = unreleasedFlag(2400, "warn_on_blocking_binder_transactions") - // 2500 - output switcher - // TODO(b/261538825): Tracking Bug - @JvmField - val OUTPUT_SWITCHER_ADVANCED_LAYOUT = releasedFlag(2500, "output_switcher_advanced_layout") - @JvmField - val OUTPUT_SWITCHER_ROUTES_PROCESSING = releasedFlag(2501, "output_switcher_routes_processing") - @JvmField - val OUTPUT_SWITCHER_DEVICE_STATUS = releasedFlag(2502, "output_switcher_device_status") - // 2700 - unfold transitions // TODO(b/265764985): Tracking Bug @Keep diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 8b6bd2486117..54da680d8a68 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -21,7 +21,6 @@ import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; @@ -37,15 +36,12 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.TransitionFlags; import static android.view.WindowManager.TransitionOldType; import static android.view.WindowManager.TransitionType; -import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.Service; import android.app.WindowConfiguration; import android.content.Intent; -import android.graphics.Point; -import android.graphics.Rect; import android.os.Binder; import android.os.Bundle; import android.os.Debug; @@ -67,8 +63,6 @@ import android.view.WindowManager; import android.view.WindowManagerPolicyConstants; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; -import android.window.RemoteTransition; -import android.window.TransitionFilter; import android.window.TransitionInfo; import com.android.internal.policy.IKeyguardDismissCallback; @@ -81,6 +75,7 @@ import com.android.systemui.SystemUIApplication; import com.android.systemui.settings.DisplayTracker; import com.android.wm.shell.transition.ShellTransitions; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.util.TransitionUtil; import java.util.ArrayList; @@ -109,7 +104,8 @@ public class KeyguardService extends Service { } } - private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers) { + private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers, + SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) { final ArrayList<RemoteAnimationTarget> out = new ArrayList<>(); for (int i = 0; i < info.getChanges().size(); i++) { boolean changeIsWallpaper = @@ -119,32 +115,14 @@ public class KeyguardService extends Service { final TransitionInfo.Change change = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); final int taskId = taskInfo != null ? change.getTaskInfo().taskId : -1; - boolean isNotInRecents; - WindowConfiguration windowConfiguration = null; - if (taskInfo != null) { - if (taskInfo.getConfiguration() != null) { - windowConfiguration = - change.getTaskInfo().getConfiguration().windowConfiguration; - } - isNotInRecents = !change.getTaskInfo().isRunning; - } else { - isNotInRecents = true; - } - Rect localBounds = new Rect(change.getEndAbsBounds()); - localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y); - - final RemoteAnimationTarget target = new RemoteAnimationTarget( - taskId, - newModeToLegacyMode(change.getMode()), - change.getLeash(), - (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0 - || (change.getFlags() & TransitionInfo.FLAG_SHOW_WALLPAPER) != 0, - null /* clipRect */, - new Rect(0, 0, 0, 0) /* contentInsets */, + + final RemoteAnimationTarget target = TransitionUtil.newTarget(change, + // wallpapers go into the "below" layer space info.getChanges().size() - i, - new Point(), localBounds, new Rect(change.getEndAbsBounds()), - windowConfiguration, isNotInRecents, null /* startLeash */, - change.getStartAbsBounds(), taskInfo, false /* allowEnterPip */); + // keyguard treats wallpaper as translucent + (change.getFlags() & TransitionInfo.FLAG_SHOW_WALLPAPER) != 0, + info, t, leashMap); + // Use hasAnimatingParent to mark the anything below root task if (taskId != -1 && change.getParent() != null) { final TransitionInfo.Change parentChange = info.getChange(change.getParent()); @@ -178,35 +156,31 @@ public class KeyguardService extends Service { // Wrap Keyguard going away animation. // Note: Also used for wrapping occlude by Dream animation. It works (with some redundancy). - private static IRemoteTransition wrap(IRemoteAnimationRunner runner) { + public static IRemoteTransition wrap(IRemoteAnimationRunner runner) { return new IRemoteTransition.Stub() { final ArrayMap<IBinder, IRemoteTransitionFinishedCallback> mFinishCallbacks = new ArrayMap<>(); + private final ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = new ArrayMap<>(); @Override public void startAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { Slog.d(TAG, "Starts IRemoteAnimationRunner: info=" + info); - final RemoteAnimationTarget[] apps = wrap(info, false /* wallpapers */); - final RemoteAnimationTarget[] wallpapers = wrap(info, true /* wallpapers */); + final RemoteAnimationTarget[] apps = + wrap(info, false /* wallpapers */, t, mLeashMap); + final RemoteAnimationTarget[] wallpapers = + wrap(info, true /* wallpapers */, t, mLeashMap); final RemoteAnimationTarget[] nonApps = new RemoteAnimationTarget[0]; // Sets the alpha to 0 for the opening root task for fade in animation. And since // the fade in animation can only apply on the first opening app, so set alpha to 1 // for anything else. - boolean foundOpening = false; for (RemoteAnimationTarget target : apps) { if (target.taskId != -1 && target.mode == RemoteAnimationTarget.MODE_OPENING && !target.hasAnimatingParent) { - if (foundOpening) { - Log.w(TAG, "More than one opening target"); - t.setAlpha(target.leash, 1.0f); - continue; - } t.setAlpha(target.leash, 0.0f); - foundOpening = true; } else { t.setAlpha(target.leash, 1.0f); } @@ -273,7 +247,8 @@ public class KeyguardService extends Service { if (mShellTransitions == null || !Transitions.ENABLE_SHELL_TRANSITIONS) { RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); final RemoteAnimationAdapter exitAnimationAdapter = - new RemoteAnimationAdapter(mExitAnimationRunner, 0, 0); + new RemoteAnimationAdapter( + mKeyguardViewMediator.getExitAnimationRunner(), 0, 0); definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, exitAnimationAdapter); definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER, @@ -297,92 +272,7 @@ public class KeyguardService extends Service { unoccludeAnimationAdapter); ActivityTaskManager.getInstance().registerRemoteAnimationsForDisplay( mDisplayTracker.getDefaultDisplayId(), definition); - return; - } - Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_GOING_AWAY"); - TransitionFilter f = new TransitionFilter(); - f.mFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY; - mShellTransitions.registerRemote(f, new RemoteTransition( - wrap(mExitAnimationRunner), getIApplicationThread(), "ExitKeyguard")); - - Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_(UN)OCCLUDE"); - // Register for occluding - final RemoteTransition occludeTransition = new RemoteTransition( - mOccludeAnimation, getIApplicationThread(), "KeyguardOcclude"); - f = new TransitionFilter(); - f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED; - f.mRequirements = new TransitionFilter.Requirement[]{ - new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; - // First require at-least one app showing that occludes. - f.mRequirements[0].mMustBeIndependent = false; - f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - // Then require that we aren't closing any occludes (because this would mean a - // regular task->task or activity->activity animation not involving keyguard). - f.mRequirements[1].mNot = true; - f.mRequirements[1].mMustBeIndependent = false; - f.mRequirements[1].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; - mShellTransitions.registerRemote(f, occludeTransition); - - // Now register for un-occlude. - final RemoteTransition unoccludeTransition = new RemoteTransition( - mUnoccludeAnimation, getIApplicationThread(), "KeyguardUnocclude"); - f = new TransitionFilter(); - f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED; - f.mRequirements = new TransitionFilter.Requirement[]{ - new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; - // First require at-least one app going-away (doesn't need occlude flag - // as that is implicit by it having been visible and we don't want to exclude - // cases where we are un-occluding because the app removed its showWhenLocked - // capability at runtime). - f.mRequirements[1].mMustBeIndependent = false; - f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; - f.mRequirements[1].mMustBeTask = true; - // Then require that we aren't opening any occludes (otherwise we'd remain - // occluded). - f.mRequirements[0].mNot = true; - f.mRequirements[0].mMustBeIndependent = false; - f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - mShellTransitions.registerRemote(f, unoccludeTransition); - - // Register for specific transition type. - // Above filter cannot fulfill all conditions. - // E.g. close top activity while screen off but next activity is occluded, this should - // an occluded transition, but since the activity is invisible, the condition would - // match unoccluded transition. - // But on the contrary, if we add above condition in occluded transition, then when user - // trying to dismiss occluded activity when unlock keyguard, the condition would match - // occluded transition. - f = new TransitionFilter(); - f.mTypeSet = new int[]{TRANSIT_KEYGUARD_OCCLUDE}; - mShellTransitions.registerRemote(f, occludeTransition); - - f = new TransitionFilter(); - f.mTypeSet = new int[]{TRANSIT_KEYGUARD_UNOCCLUDE}; - mShellTransitions.registerRemote(f, unoccludeTransition); - - Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_OCCLUDE for DREAM"); - // Register for occluding by Dream - f = new TransitionFilter(); - f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED; - f.mRequirements = new TransitionFilter.Requirement[]{ - new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; - // First require at-least one app of type DREAM showing that occludes. - f.mRequirements[0].mActivityType = WindowConfiguration.ACTIVITY_TYPE_DREAM; - f.mRequirements[0].mMustBeIndependent = false; - f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - // Then require that we aren't closing any occludes (because this would mean a - // regular task->task or activity->activity animation not involving keyguard). - f.mRequirements[1].mNot = true; - f.mRequirements[1].mMustBeIndependent = false; - f.mRequirements[1].mFlags = FLAG_OCCLUDES_KEYGUARD; - f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; - mShellTransitions.registerRemote(f, new RemoteTransition( - wrap(mKeyguardViewMediator.getOccludeByDreamAnimationRunner()), - getIApplicationThread(), "KeyguardOccludeByDream")); + } } @Override @@ -402,27 +292,6 @@ public class KeyguardService extends Service { } } - private final IRemoteAnimationRunner.Stub mExitAnimationRunner = - new IRemoteAnimationRunner.Stub() { - @Override // Binder interface - public void onAnimationStart(@WindowManager.TransitionOldType int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) { - Trace.beginSection("mExitAnimationRunner.onAnimationStart#startKeyguardExitAnimation"); - checkPermission(); - mKeyguardViewMediator.startKeyguardExitAnimation(transit, apps, wallpapers, - nonApps, finishedCallback); - Trace.endSection(); - } - - @Override // Binder interface - public void onAnimationCancelled() { - mKeyguardViewMediator.cancelKeyguardExitAnimation(); - } - }; - final IRemoteTransition mOccludeAnimation = new IRemoteTransition.Stub() { @Override public void startAnimation(IBinder transition, TransitionInfo info, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index bd6dfe3dfc9a..f96f337d5cb2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -258,12 +258,10 @@ class KeyguardUnlockAnimationController @Inject constructor( */ private var surfaceBehindAlpha = 1f - private var wallpaperAlpha = 1f - @VisibleForTesting var surfaceBehindAlphaAnimator = ValueAnimator.ofFloat(0f, 1f) - var wallpaperAlphaAnimator = ValueAnimator.ofFloat(0f, 1f) + var wallpaperCannedUnlockAnimator = ValueAnimator.ofFloat(0f, 1f) /** * Matrix applied to [surfaceBehindRemoteAnimationTarget], which is the surface of the @@ -330,6 +328,7 @@ class KeyguardUnlockAnimationController @Inject constructor( if (surfaceBehindAlpha == 0f) { Log.d(TAG, "surfaceBehindAlphaAnimator#onAnimationEnd") surfaceBehindRemoteAnimationTargets = null + wallpaperTargets = null keyguardViewMediator.get().finishSurfaceBehindRemoteAnimation( false /* cancelled */) } else { @@ -340,23 +339,17 @@ class KeyguardUnlockAnimationController @Inject constructor( }) } - with(wallpaperAlphaAnimator) { + with(wallpaperCannedUnlockAnimator) { duration = LAUNCHER_ICONS_ANIMATION_DURATION_MS interpolator = Interpolators.ALPHA_OUT addUpdateListener { valueAnimator: ValueAnimator -> - wallpaperAlpha = valueAnimator.animatedValue as Float - setWallpaperAppearAmount(wallpaperAlpha) + setWallpaperAppearAmount(valueAnimator.animatedValue as Float) } addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - Log.d(TAG, "wallpaperAlphaAnimator#onAnimationEnd, animation ended ") - if (wallpaperAlpha == 1f) { - wallpaperTargets = null - keyguardViewMediator.get().finishExitRemoteAnimation() - } else { - Log.d(TAG, "wallpaperAlphaAnimator#onAnimationEnd, " + - "animation was cancelled: skipping finishAnimation()") - } + Log.d(TAG, "wallpaperCannedUnlockAnimator#onAnimationEnd") + keyguardViewMediator.get().exitKeyguardAndFinishSurfaceBehindRemoteAnimation( + false /* cancelled */) } }) } @@ -387,11 +380,6 @@ class KeyguardUnlockAnimationController @Inject constructor( context.resources.getDimensionPixelSize(R.dimen.rounded_corner_radius).toFloat() } - fun isAnyKeyguyardAnimatorPlaying(): Boolean { - return surfaceBehindAlphaAnimator.isStarted || - wallpaperAlphaAnimator.isStarted || surfaceBehindEntryAnimator.isStarted - } - /** * Add a listener to be notified of various stages of the unlock animation. */ @@ -536,8 +524,6 @@ class KeyguardUnlockAnimationController @Inject constructor( wallpaperTargets = wallpapers surfaceBehindRemoteAnimationStartTime = startTime - fadeInWallpaper() - // If we specifically requested that the surface behind be made visible (vs. it being made // visible because we're unlocking), then we're in the middle of a swipe-to-unlock touch // gesture and the surface behind the keyguard should be made visible so that we can animate @@ -599,7 +585,9 @@ class KeyguardUnlockAnimationController @Inject constructor( // Finish the keyguard remote animation if the dismiss amount has crossed the threshold. // Check it here in case there is no more change to the dismiss amount after the last change // that starts the keyguard animation. @see #updateKeyguardViewMediatorIfThresholdsReached() - finishKeyguardExitRemoteAnimationIfReachThreshold() + if (!playingCannedUnlockAnimation) { + finishKeyguardExitRemoteAnimationIfReachThreshold() + } } /** @@ -650,7 +638,7 @@ class KeyguardUnlockAnimationController @Inject constructor( * transition if possible. */ private fun unlockToLauncherWithInWindowAnimations() { - setSurfaceBehindAppearAmount(1f) + setSurfaceBehindAppearAmount(1f, wallpapers = false) try { // Begin the animation, waiting for the shade to animate out. @@ -676,19 +664,8 @@ class KeyguardUnlockAnimationController @Inject constructor( // visible, hide our smartspace. lockscreenSmartspace?.visibility = View.INVISIBLE - // As soon as the shade has animated out of the way, finish the keyguard exit animation. The - // in-window animations in the Launcher window will end on their own. - handler.postDelayed({ - if (keyguardViewMediator.get().isShowingAndNotOccluded && - !keyguardStateController.isKeyguardGoingAway) { - Log.e(TAG, "Finish keyguard exit animation delayed Runnable ran, but we are " + - "showing and not going away.") - return@postDelayed - } - - keyguardViewMediator.get().exitKeyguardAndFinishSurfaceBehindRemoteAnimation( - false /* cancelled */) - }, CANNED_UNLOCK_START_DELAY) + // Start an animation for the wallpaper, which will finish keyguard exit when it completes. + fadeInWallpaper() } /** @@ -809,7 +786,16 @@ class KeyguardUnlockAnimationController @Inject constructor( * animations and swipe gestures to animate the surface's entry (and exit, if the swipe is * cancelled). */ - fun setSurfaceBehindAppearAmount(amount: Float) { + fun setSurfaceBehindAppearAmount(amount: Float, wallpapers: Boolean = true) { + val animationAlpha = when { + // If we're snapping the keyguard back, immediately begin fading it out. + keyguardStateController.isSnappingKeyguardBackAfterSwipe -> amount + // If the screen has turned back off, the unlock animation is going to be cancelled, + // so set the surface alpha to 0f so it's no longer visible. + !powerManager.isInteractive -> 0f + else -> surfaceBehindAlpha + } + surfaceBehindRemoteAnimationTargets?.forEach { surfaceBehindRemoteAnimationTarget -> val surfaceHeight: Int = surfaceBehindRemoteAnimationTarget.screenSpaceBounds.height() @@ -839,16 +825,6 @@ class KeyguardUnlockAnimationController @Inject constructor( surfaceHeight * SURFACE_BEHIND_SCALE_PIVOT_Y ) - - val animationAlpha = when { - // If we're snapping the keyguard back, immediately begin fading it out. - keyguardStateController.isSnappingKeyguardBackAfterSwipe -> amount - // If the screen has turned back off, the unlock animation is going to be cancelled, - // so set the surface alpha to 0f so it's no longer visible. - !powerManager.isInteractive -> 0f - else -> surfaceBehindAlpha - } - // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is // unable to draw val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget.leash @@ -863,28 +839,27 @@ class KeyguardUnlockAnimationController @Inject constructor( } else { applyParamsToSurface( SyncRtSurfaceTransactionApplier.SurfaceParams.Builder( - surfaceBehindRemoteAnimationTarget.leash) - .withMatrix(surfaceBehindMatrix) - .withCornerRadius(roundedCornerRadius) - .withAlpha(animationAlpha) - .build() + surfaceBehindRemoteAnimationTarget.leash) + .withMatrix(surfaceBehindMatrix) + .withCornerRadius(roundedCornerRadius) + .withAlpha(animationAlpha) + .build() ) } } + + if (wallpapers) { + setWallpaperAppearAmount(amount) + } } - /** - * Modify the opacity of a wallpaper window. - */ fun setWallpaperAppearAmount(amount: Float) { - wallpaperTargets?.forEach { wallpaper -> - val animationAlpha = when { - // If the screen has turned back off, the unlock animation is going to be cancelled, - // so set the surface alpha to 0f so it's no longer visible. - !powerManager.isInteractive -> 0f - else -> amount - } + val animationAlpha = when { + !powerManager.isInteractive -> 0f + else -> amount + } + wallpaperTargets?.forEach { wallpaper -> // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is // unable to draw val sc: SurfaceControl? = wallpaper.leash @@ -923,6 +898,7 @@ class KeyguardUnlockAnimationController @Inject constructor( setSurfaceBehindAppearAmount(1f) surfaceBehindAlphaAnimator.cancel() surfaceBehindEntryAnimator.cancel() + wallpaperCannedUnlockAnimator.cancel() try { launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */) } catch (e: RemoteException) { @@ -931,6 +907,7 @@ class KeyguardUnlockAnimationController @Inject constructor( // That target is no longer valid since the animation finished, null it out. surfaceBehindRemoteAnimationTargets = null + wallpaperTargets = null playingCannedUnlockAnimation = false willUnlockWithInWindowLauncherAnimations = false @@ -972,8 +949,8 @@ class KeyguardUnlockAnimationController @Inject constructor( private fun fadeInWallpaper() { Log.d(TAG, "fadeInWallpaper") - wallpaperAlphaAnimator.cancel() - wallpaperAlphaAnimator.start() + wallpaperCannedUnlockAnimator.cancel() + wallpaperCannedUnlockAnimator.start() } private fun fadeOutSurfaceBehind() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 46ff0d9ded55..45e4623c7e27 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -79,7 +79,6 @@ import android.provider.DeviceConfig; import android.provider.Settings; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; -import android.util.EventLog; import android.util.Log; import android.util.Slog; import android.util.SparseBooleanArray; @@ -94,6 +93,7 @@ import android.view.WindowManager; import android.view.WindowManagerPolicyConstants; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.window.IRemoteTransition; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -154,12 +154,14 @@ import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.util.DeviceConfigProxy; +import com.android.wm.shell.keyguard.KeyguardTransitions; import dagger.Lazy; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Optional; import java.util.concurrent.Executor; /** @@ -744,7 +746,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } @Override - public void keyguardDone(boolean strongAuth, int targetUserId) { + public void keyguardDone(boolean primaryAuth, int targetUserId) { if (targetUserId != KeyguardUpdateMonitor.getCurrentUser()) { return; } @@ -765,7 +767,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } @Override - public void keyguardDonePending(boolean strongAuth, int targetUserId) { + public void keyguardDonePending(boolean primaryAuth, int targetUserId) { Trace.beginSection("KeyguardViewMediator.mViewMediatorCallback#keyguardDonePending"); if (DEBUG) Log.d(TAG, "keyguardDonePending"); if (targetUserId != KeyguardUpdateMonitor.getCurrentUser()) { @@ -962,7 +964,26 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; - private IRemoteAnimationRunner mOccludeAnimationRunner = + private final IRemoteAnimationRunner.Stub mExitAnimationRunner = + new IRemoteAnimationRunner.Stub() { + @Override // Binder interface + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) { + Trace.beginSection("mExitAnimationRunner.onAnimationStart#startKeyguardExitAnimation"); + startKeyguardExitAnimation(transit, apps, wallpapers, nonApps, finishedCallback); + Trace.endSection(); + } + + @Override // Binder interface + public void onAnimationCancelled() { + cancelKeyguardExitAnimation(); + } + }; + + private final IRemoteAnimationRunner mOccludeAnimationRunner = new OccludeActivityLaunchRemoteAnimationRunner(mOccludeAnimationController); private final IRemoteAnimationRunner mOccludeByDreamAnimationRunner = @@ -1004,7 +1025,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } final RemoteAnimationTarget primary = apps[0]; - final boolean isDream = (apps[0].taskInfo.topActivityType + final boolean isDream = (apps[0].taskInfo != null + && apps[0].taskInfo.topActivityType == WindowConfiguration.ACTIVITY_TYPE_DREAM); if (!isDream) { Log.w(TAG, "The occluding app isn't Dream; " @@ -1104,7 +1126,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } final RemoteAnimationTarget primary = apps[0]; - final boolean isDream = (apps[0].taskInfo.topActivityType + final boolean isDream = (apps[0].taskInfo != null + && apps[0].taskInfo.topActivityType == WindowConfiguration.ACTIVITY_TYPE_DREAM); final SyncRtSurfaceTransactionApplier applier = @@ -1187,6 +1210,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final InteractionJankMonitor mInteractionJankMonitor; private boolean mWallpaperSupportsAmbientMode; private ScreenOnCoordinator mScreenOnCoordinator; + private final KeyguardTransitions mKeyguardTransitions; private Lazy<ActivityLaunchAnimator> mActivityLaunchAnimator; private Lazy<ScrimController> mScrimControllerLazy; @@ -1222,6 +1246,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, ScreenOffAnimationController screenOffAnimationController, Lazy<NotificationShadeDepthController> notificationShadeDepthController, ScreenOnCoordinator screenOnCoordinator, + KeyguardTransitions keyguardTransitions, InteractionJankMonitor interactionJankMonitor, DreamOverlayStateController dreamOverlayStateController, Lazy<ShadeController> shadeControllerLazy, @@ -1249,6 +1274,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, dumpManager.registerDumpable(getClass().getName(), this); mDeviceConfig = deviceConfig; mScreenOnCoordinator = screenOnCoordinator; + mKeyguardTransitions = keyguardTransitions; mNotificationShadeWindowControllerLazy = notificationShadeWindowControllerLazy; mShowHomeOverLockscreen = mDeviceConfig.getBoolean( DeviceConfig.NAMESPACE_SYSTEMUI, @@ -1324,6 +1350,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, setShowingLocked(false /* showing */, true /* forceCallbacks */); } + mKeyguardTransitions.register( + KeyguardService.wrap(getExitAnimationRunner()), + KeyguardService.wrap(getOccludeAnimationRunner()), + KeyguardService.wrap(getOccludeByDreamAnimationRunner()), + KeyguardService.wrap(getUnoccludeAnimationRunner())); + final ContentResolver cr = mContext.getContentResolver(); mDeviceInteractive = mPM.isInteractive(); @@ -1469,13 +1501,17 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, notifyFinishedGoingToSleep(); if (cameraGestureTriggered) { - // Just to make sure, make sure the device is awake. mContext.getSystemService(PowerManager.class).wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_CAMERA_LAUNCH, "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK"); setPendingLock(false); mPendingReset = false; + mPowerGestureIntercepted = true; + if (DEBUG) { + Log.d(TAG, "cameraGestureTriggered=" + cameraGestureTriggered + + ",mPowerGestureIntercepted=" + mPowerGestureIntercepted); + } } if (mPendingReset) { @@ -1674,7 +1710,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mAnimatingScreenOff = false; cancelDoKeyguardLaterLocked(); cancelDoKeyguardForChildProfilesLocked(); - if (DEBUG) Log.d(TAG, "onStartedWakingUp, seq = " + mDelayedShowingSequence); + if (cameraGestureTriggered) { + mPowerGestureIntercepted = true; + } + if (DEBUG) { + Log.d(TAG, "onStartedWakingUp, seq = " + mDelayedShowingSequence + + ", mPowerGestureIntercepted = " + mPowerGestureIntercepted); + } notifyStartedWakingUp(); } mUiEventLogger.logWithInstanceIdAndPosition( @@ -1859,6 +1901,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Trace.endSection(); } + public IRemoteAnimationRunner getExitAnimationRunner() { + return mExitAnimationRunner; + } + public IRemoteAnimationRunner getOccludeAnimationRunner() { return mOccludeAnimationRunner; } @@ -1897,12 +1943,19 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, startKeyguardExitAnimation(0, 0); } + mPowerGestureIntercepted = mUpdateMonitor.isSecureCameraLaunchedOverKeyguard(); + if (mOccluded != isOccluded) { mOccluded = isOccluded; mKeyguardViewControllerLazy.get().setOccluded(isOccluded, animate && mDeviceInteractive); adjustStatusBarLocked(); } + + if (DEBUG) { + Log.d(TAG, "isOccluded=" + isOccluded + ",mPowerGestureIntercepted=" + + mPowerGestureIntercepted); + } } Trace.endSection(); } @@ -2219,16 +2272,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; - private void keyguardDone() { - Trace.beginSection("KeyguardViewMediator#keyguardDone"); - if (DEBUG) Log.d(TAG, "keyguardDone()"); - userActivity(); - EventLog.writeEvent(70000, 2); - Message msg = mHandler.obtainMessage(KEYGUARD_DONE); - mHandler.sendMessage(msg); - Trace.endSection(); - } - /** * This handler will be associated with the policy thread, which will also * be the UI thread of the keyguard. Since the apis of the policy, and therefore @@ -2966,19 +3009,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mSurfaceBehindRemoteAnimationRequested = false; mSurfaceBehindRemoteAnimationRunning = false; mKeyguardStateController.notifyKeyguardGoingAway(false); - finishExitRemoteAnimation(); - } - - void finishExitRemoteAnimation() { - if (mKeyguardUnlockAnimationControllerLazy.get().isAnyKeyguyardAnimatorPlaying() - || mKeyguardStateController.isDismissingFromSwipe()) { - // If the animation is ongoing, or we are not done with the swipe gesture, - // it's too early to terminate the animation - Log.d(TAG, "finishAnimation not executing now because " - + "not all animations have finished"); - return; - } - Log.d(TAG, "finishAnimation executing"); if (mSurfaceBehindRemoteAnimationFinishedCallback != null) { try { @@ -3030,6 +3060,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, flags |= StatusBarManager.DISABLE_RECENT; } + if (mPowerGestureIntercepted) { + flags |= StatusBarManager.DISABLE_RECENT; + } + if (DEBUG) { Log.d(TAG, "adjustStatusBarLocked: mShowing=" + mShowing + " mOccluded=" + mOccluded + " isSecure=" + isSecure() + " force=" + forceHideHomeRecentsButtons @@ -3124,7 +3158,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Trace.beginSection("KeyguardViewMediator#onWakeAndUnlocking"); mWakeAndUnlocking = true; - keyguardDone(); + mKeyguardViewControllerLazy.get().notifyKeyguardAuthenticated(/* primaryAuth */ false); + userActivity(); Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt new file mode 100644 index 000000000000..4eb7d44ebfa6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard + +import android.annotation.WorkerThread +import android.content.ComponentCallbacks2 +import android.os.Trace +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.utils.GlobalWindowManager +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Releases cached resources on allocated by keyguard. + * + * We release most resources when device goes idle since that's the least likely time it'll cause + * jank during use. Idle in this case means after lockscreen -> AoD transition completes or when the + * device screen is turned off, depending on settings. + */ +@SysUISingleton +class ResourceTrimmer +@Inject +constructor( + private val keyguardInteractor: KeyguardInteractor, + private val globalWindowManager: GlobalWindowManager, + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, +) : CoreStartable, WakefulnessLifecycle.Observer { + + override fun start() { + Log.d(LOG_TAG, "Resource trimmer registered.") + applicationScope.launch(bgDispatcher) { + // We need to wait for the AoD transition (and animation) to complete. + // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f + // signal. This is to make sure we don't clear font caches during animation which + // would jank and leave stale data in memory. + val isDozingFully = + keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged() + combine( + keyguardInteractor.wakefulnessModel.map { it.state }, + keyguardInteractor.isDreaming, + isDozingFully, + ::Triple + ) + .distinctUntilChanged() + .collect { onWakefulnessUpdated(it.first, it.second, it.third) } + } + } + + @WorkerThread + private fun onWakefulnessUpdated( + wakefulness: WakefulnessState, + isDreaming: Boolean, + isDozingFully: Boolean + ) { + if (DEBUG) { + Log.d( + LOG_TAG, + "Wakefulness: $wakefulness Dreaming: $isDreaming DozeAmount: $isDozingFully" + ) + } + // There are three scenarios: + // * No dozing and no AoD at all - where we go directly to ASLEEP with isDreaming = false + // and dozeAmount == 0f + // * Dozing without Aod - where we go to ASLEEP with isDreaming = true and dozeAmount jumps + // to 1f + // * AoD - where we go to ASLEEP with iDreaming = true and dozeAmount slowly increases + // to 1f + val dozeDisabledAndScreenOff = wakefulness == WakefulnessState.ASLEEP && !isDreaming + val dozeEnabledAndDozeAnimationCompleted = + wakefulness == WakefulnessState.ASLEEP && isDreaming && isDozingFully + if (dozeDisabledAndScreenOff || dozeEnabledAndDozeAnimationCompleted) { + Trace.beginSection("ResourceTrimmer#trimMemory") + Log.d(LOG_TAG, "SysUI asleep, trimming memory.") + globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + Trace.endSection() + } + } + + companion object { + private const val LOG_TAG = "ResourceTrimmer" + private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardBouncerMessages.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactory.kt index f4145dbef80f..4085dab39300 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardBouncerMessages.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactory.kt @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.keyguard +package com.android.systemui.keyguard.bouncer.data.factory import android.annotation.IntDef +import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardSecurityModel.SecurityMode import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT @@ -34,6 +35,7 @@ import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R.string.bouncer_face_not_recognized import com.android.systemui.R.string.keyguard_enter_password import com.android.systemui.R.string.keyguard_enter_pattern @@ -71,10 +73,86 @@ import com.android.systemui.R.string.kg_wrong_input_try_fp_suggestion import com.android.systemui.R.string.kg_wrong_password_try_again import com.android.systemui.R.string.kg_wrong_pattern_try_again import com.android.systemui.R.string.kg_wrong_pin_try_again +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel +import com.android.systemui.keyguard.bouncer.shared.model.Message +import javax.inject.Inject -typealias BouncerMessage = Pair<Int, Int> +@SysUISingleton +class BouncerMessageFactory +@Inject +constructor( + private val updateMonitor: KeyguardUpdateMonitor, + private val securityModel: KeyguardSecurityModel, +) { -fun emptyBouncerMessage(): BouncerMessage = Pair(0, 0) + fun createFromPromptReason( + @BouncerPromptReason reason: Int, + userId: Int, + ): BouncerMessageModel? { + val pair = + getBouncerMessage( + reason, + securityModel.getSecurityMode(userId), + updateMonitor.isFingerprintAllowedInBouncer + ) + return pair?.let { + BouncerMessageModel( + message = Message(messageResId = pair.first), + secondaryMessage = Message(messageResId = pair.second) + ) + } + } + + fun createFromString( + primaryMsg: String? = null, + secondaryMsg: String? = null + ): BouncerMessageModel = + BouncerMessageModel( + message = primaryMsg?.let { Message(message = it) }, + secondaryMessage = secondaryMsg?.let { Message(message = it) }, + ) + + /** + * Helper method that provides the relevant bouncer message that should be shown for different + * scenarios indicated by [reason]. [securityMode] & [fpAllowedInBouncer] parameters are used to + * provide a more specific message. + */ + private fun getBouncerMessage( + @BouncerPromptReason reason: Int, + securityMode: SecurityMode, + fpAllowedInBouncer: Boolean = false + ): Pair<Int, Int>? { + return when (reason) { + PROMPT_REASON_RESTART -> authRequiredAfterReboot(securityMode) + PROMPT_REASON_TIMEOUT -> authRequiredAfterPrimaryAuthTimeout(securityMode) + PROMPT_REASON_DEVICE_ADMIN -> authRequiredAfterAdminLockdown(securityMode) + PROMPT_REASON_USER_REQUEST -> authRequiredAfterUserLockdown(securityMode) + PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode) + PROMPT_REASON_PREPARE_FOR_UPDATE -> authRequiredForUnattendedUpdate(securityMode) + PROMPT_REASON_FINGERPRINT_LOCKED_OUT -> fingerprintUnlockUnavailable(securityMode) + PROMPT_REASON_FACE_LOCKED_OUT -> faceUnlockUnavailable(securityMode) + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT -> + if (fpAllowedInBouncer) incorrectSecurityInputWithFingerprint(securityMode) + else incorrectSecurityInput(securityMode) + PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT -> + if (fpAllowedInBouncer) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode) + else nonStrongAuthTimeout(securityMode) + PROMPT_REASON_TRUSTAGENT_EXPIRED -> + if (fpAllowedInBouncer) trustAgentDisabledWithFingerprintAllowed(securityMode) + else trustAgentDisabled(securityMode) + PROMPT_REASON_INCORRECT_FACE_INPUT -> + if (fpAllowedInBouncer) incorrectFaceInputWithFingerprintAllowed(securityMode) + else incorrectFaceInput(securityMode) + PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT -> incorrectFingerprintInput(securityMode) + PROMPT_REASON_DEFAULT -> + if (fpAllowedInBouncer) defaultMessageWithFingerprint(securityMode) + else defaultMessage(securityMode) + PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode) + else -> null + } + } +} @Retention(AnnotationRetention.SOURCE) @IntDef( @@ -97,48 +175,7 @@ fun emptyBouncerMessage(): BouncerMessage = Pair(0, 0) ) annotation class BouncerPromptReason -/** - * Helper method that provides the relevant bouncer message that should be shown for different - * scenarios indicated by [reason]. [securityMode] & [fpAllowedInBouncer] parameters are used to - * provide a more specific message. - */ -@JvmOverloads -fun getBouncerMessage( - @BouncerPromptReason reason: Int, - securityMode: SecurityMode, - fpAllowedInBouncer: Boolean = false -): BouncerMessage { - return when (reason) { - PROMPT_REASON_RESTART -> authRequiredAfterReboot(securityMode) - PROMPT_REASON_TIMEOUT -> authRequiredAfterPrimaryAuthTimeout(securityMode) - PROMPT_REASON_DEVICE_ADMIN -> authRequiredAfterAdminLockdown(securityMode) - PROMPT_REASON_USER_REQUEST -> authRequiredAfterUserLockdown(securityMode) - PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode) - PROMPT_REASON_PREPARE_FOR_UPDATE -> authRequiredForUnattendedUpdate(securityMode) - PROMPT_REASON_FINGERPRINT_LOCKED_OUT -> fingerprintUnlockUnavailable(securityMode) - PROMPT_REASON_FACE_LOCKED_OUT -> faceUnlockUnavailable(securityMode) - PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT -> - if (fpAllowedInBouncer) incorrectSecurityInputWithFingerprint(securityMode) - else incorrectSecurityInput(securityMode) - PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT -> - if (fpAllowedInBouncer) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode) - else nonStrongAuthTimeout(securityMode) - PROMPT_REASON_TRUSTAGENT_EXPIRED -> - if (fpAllowedInBouncer) trustAgentDisabledWithFingerprintAllowed(securityMode) - else trustAgentDisabled(securityMode) - PROMPT_REASON_INCORRECT_FACE_INPUT -> - if (fpAllowedInBouncer) incorrectFaceInputWithFingerprintAllowed(securityMode) - else incorrectFaceInput(securityMode) - PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT -> incorrectFingerprintInput(securityMode) - PROMPT_REASON_DEFAULT -> - if (fpAllowedInBouncer) defaultMessageWithFingerprint(securityMode) - else defaultMessage(securityMode) - PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode) - else -> emptyBouncerMessage() - } -} - -fun defaultMessage(securityMode: SecurityMode): BouncerMessage { +private fun defaultMessage(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, 0) SecurityMode.Password -> Pair(keyguard_enter_password, 0) @@ -147,7 +184,7 @@ fun defaultMessage(securityMode: SecurityMode): BouncerMessage { } } -fun defaultMessageWithFingerprint(securityMode: SecurityMode): BouncerMessage { +private fun defaultMessageWithFingerprint(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, 0) SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, 0) @@ -156,7 +193,7 @@ fun defaultMessageWithFingerprint(securityMode: SecurityMode): BouncerMessage { } } -fun incorrectSecurityInput(securityMode: SecurityMode): BouncerMessage { +private fun incorrectSecurityInput(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, 0) SecurityMode.Password -> Pair(kg_wrong_password_try_again, 0) @@ -165,7 +202,7 @@ fun incorrectSecurityInput(securityMode: SecurityMode): BouncerMessage { } } -fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): BouncerMessage { +private fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(kg_wrong_pattern_try_again, kg_wrong_input_try_fp_suggestion) SecurityMode.Password -> Pair(kg_wrong_password_try_again, kg_wrong_input_try_fp_suggestion) @@ -174,7 +211,7 @@ fun incorrectSecurityInputWithFingerprint(securityMode: SecurityMode): BouncerMe } } -fun incorrectFingerprintInput(securityMode: SecurityMode): BouncerMessage { +private fun incorrectFingerprintInput(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_pattern) SecurityMode.Password -> Pair(kg_fp_not_recognized, kg_bio_try_again_or_password) @@ -183,7 +220,7 @@ fun incorrectFingerprintInput(securityMode: SecurityMode): BouncerMessage { } } -fun incorrectFaceInput(securityMode: SecurityMode): BouncerMessage { +private fun incorrectFaceInput(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_pattern) SecurityMode.Password -> Pair(bouncer_face_not_recognized, kg_bio_try_again_or_password) @@ -192,7 +229,7 @@ fun incorrectFaceInput(securityMode: SecurityMode): BouncerMessage { } } -fun incorrectFaceInputWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessage { +private fun incorrectFaceInputWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, bouncer_face_not_recognized) SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, bouncer_face_not_recognized) @@ -201,7 +238,7 @@ fun incorrectFaceInputWithFingerprintAllowed(securityMode: SecurityMode): Bounce } } -fun biometricLockout(securityMode: SecurityMode): BouncerMessage { +private fun biometricLockout(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_bio_too_many_attempts_pattern) SecurityMode.Password -> Pair(keyguard_enter_password, kg_bio_too_many_attempts_password) @@ -210,7 +247,7 @@ fun biometricLockout(securityMode: SecurityMode): BouncerMessage { } } -fun authRequiredAfterReboot(securityMode: SecurityMode): BouncerMessage { +private fun authRequiredAfterReboot(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_reason_restart_pattern) SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_reason_restart_password) @@ -219,7 +256,7 @@ fun authRequiredAfterReboot(securityMode: SecurityMode): BouncerMessage { } } -fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): BouncerMessage { +private fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_dpm_lock) SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_dpm_lock) @@ -228,7 +265,7 @@ fun authRequiredAfterAdminLockdown(securityMode: SecurityMode): BouncerMessage { } } -fun authRequiredAfterUserLockdown(securityMode: SecurityMode): BouncerMessage { +private fun authRequiredAfterUserLockdown(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_user_lockdown_pattern) SecurityMode.Password -> @@ -238,7 +275,7 @@ fun authRequiredAfterUserLockdown(securityMode: SecurityMode): BouncerMessage { } } -fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): BouncerMessage { +private fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_unattended_update) SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_unattended_update) @@ -247,7 +284,7 @@ fun authRequiredForUnattendedUpdate(securityMode: SecurityMode): BouncerMessage } } -fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): BouncerMessage { +private fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_pattern_auth_timeout) SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_password_auth_timeout) @@ -256,7 +293,7 @@ fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): BouncerMess } } -fun nonStrongAuthTimeout(securityMode: SecurityMode): BouncerMessage { +private fun nonStrongAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_auth_timeout) SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_auth_timeout) @@ -265,7 +302,7 @@ fun nonStrongAuthTimeout(securityMode: SecurityMode): BouncerMessage { } } -fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessage { +private fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_prompt_auth_timeout) SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_prompt_auth_timeout) @@ -274,7 +311,7 @@ fun nonStrongAuthTimeoutWithFingerprintAllowed(securityMode: SecurityMode): Boun } } -fun faceUnlockUnavailable(securityMode: SecurityMode): BouncerMessage { +private fun faceUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_face_locked_out) SecurityMode.Password -> Pair(keyguard_enter_password, kg_face_locked_out) @@ -283,7 +320,7 @@ fun faceUnlockUnavailable(securityMode: SecurityMode): BouncerMessage { } } -fun fingerprintUnlockUnavailable(securityMode: SecurityMode): BouncerMessage { +private fun fingerprintUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_fp_locked_out) SecurityMode.Password -> Pair(keyguard_enter_password, kg_fp_locked_out) @@ -292,7 +329,7 @@ fun fingerprintUnlockUnavailable(securityMode: SecurityMode): BouncerMessage { } } -fun trustAgentDisabled(securityMode: SecurityMode): BouncerMessage { +private fun trustAgentDisabled(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_trust_agent_disabled) SecurityMode.Password -> Pair(keyguard_enter_password, kg_trust_agent_disabled) @@ -301,7 +338,7 @@ fun trustAgentDisabled(securityMode: SecurityMode): BouncerMessage { } } -fun trustAgentDisabledWithFingerprintAllowed(securityMode: SecurityMode): BouncerMessage { +private fun trustAgentDisabledWithFingerprintAllowed(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_trust_agent_disabled) SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_trust_agent_disabled) @@ -310,7 +347,7 @@ fun trustAgentDisabledWithFingerprintAllowed(securityMode: SecurityMode): Bounce } } -fun primaryAuthLockedOut(securityMode: SecurityMode): BouncerMessage { +private fun primaryAuthLockedOut(securityMode: SecurityMode): Pair<Int, Int> { return when (securityMode) { SecurityMode.Pattern -> Pair(kg_too_many_failed_attempts_countdown, kg_primary_auth_locked_out_pattern) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt new file mode 100644 index 000000000000..c4400bcb6b21 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.bouncer.data.repository + +import android.hardware.biometrics.BiometricSourceType +import android.hardware.biometrics.BiometricSourceType.FACE +import android.hardware.biometrics.BiometricSourceType.FINGERPRINT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FACE_LOCKED_OUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FINGERPRINT_LOCKED_OUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FACE_INPUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory +import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel +import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.TrustRepository +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +/** Provide different sources of messages that needs to be shown on the bouncer. */ +interface BouncerMessageRepository { + /** + * Messages that are shown in response to the incorrect security attempts on the bouncer and + * primary authentication method being locked out, along with countdown messages before primary + * auth is active again. + */ + val primaryAuthMessage: Flow<BouncerMessageModel?> + + /** + * Help messages that are shown to the user on how to successfully perform authentication using + * face. + */ + val faceAcquisitionMessage: Flow<BouncerMessageModel?> + + /** + * Help messages that are shown to the user on how to successfully perform authentication using + * fingerprint. + */ + val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?> + + /** Custom message that is displayed when the bouncer is being shown to launch an app. */ + val customMessage: Flow<BouncerMessageModel?> + + /** + * Messages that are shown in response to biometric authentication attempts through face or + * fingerprint. + */ + val biometricAuthMessage: Flow<BouncerMessageModel?> + + /** Messages that are shown when certain auth flags are set. */ + val authFlagsMessage: Flow<BouncerMessageModel?> + + /** Messages that are show after biometrics are locked out temporarily or permanently */ + val biometricLockedOutMessage: Flow<BouncerMessageModel?> + + /** Set the value for [primaryAuthMessage] */ + fun setPrimaryAuthMessage(value: BouncerMessageModel?) + + /** Set the value for [faceAcquisitionMessage] */ + fun setFaceAcquisitionMessage(value: BouncerMessageModel?) + /** Set the value for [fingerprintAcquisitionMessage] */ + fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) + + /** Set the value for [customMessage] */ + fun setCustomMessage(value: BouncerMessageModel?) + + /** + * Clear any previously set messages for [primaryAuthMessage], [faceAcquisitionMessage], + * [fingerprintAcquisitionMessage] & [customMessage] + */ + fun clearMessage() +} + +@SysUISingleton +class BouncerMessageRepositoryImpl +@Inject +constructor( + trustRepository: TrustRepository, + biometricSettingsRepository: BiometricSettingsRepository, + updateMonitor: KeyguardUpdateMonitor, + private val bouncerMessageFactory: BouncerMessageFactory, + private val userRepository: UserRepository, + fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, +) : BouncerMessageRepository { + + private val isFaceEnrolledAndEnabled = + and( + biometricSettingsRepository.isFaceAuthenticationEnabled, + biometricSettingsRepository.isFaceEnrolled + ) + + private val isFingerprintEnrolledAndEnabled = + and( + biometricSettingsRepository.isFingerprintEnabledByDevicePolicy, + biometricSettingsRepository.isFingerprintEnrolled + ) + + private val isAnyBiometricsEnabledAndEnrolled = + or(isFaceEnrolledAndEnabled, isFingerprintEnrolledAndEnabled) + + private val authFlagsBasedPromptReason: Flow<Int> = + combine( + biometricSettingsRepository.authenticationFlags, + trustRepository.isCurrentUserTrustManaged, + isAnyBiometricsEnabledAndEnrolled, + ::Triple + ) + .map { (flags, isTrustManaged, biometricsEnrolledAndEnabled) -> + val trustOrBiometricsAvailable = (isTrustManaged || biometricsEnrolledAndEnabled) + return@map if ( + trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterReboot + ) { + PROMPT_REASON_RESTART + } else if (trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterTimeout) { + PROMPT_REASON_TIMEOUT + } else if (flags.isPrimaryAuthRequiredAfterDpmLockdown) { + PROMPT_REASON_DEVICE_ADMIN + } else if (isTrustManaged && flags.someAuthRequiredAfterUserRequest) { + PROMPT_REASON_TRUSTAGENT_EXPIRED + } else if (isTrustManaged && flags.someAuthRequiredAfterTrustAgentExpired) { + PROMPT_REASON_TRUSTAGENT_EXPIRED + } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) { + PROMPT_REASON_USER_REQUEST + } else if ( + trustOrBiometricsAvailable && flags.primaryAuthRequiredForUnattendedUpdate + ) { + PROMPT_REASON_PREPARE_FOR_UPDATE + } else if ( + trustOrBiometricsAvailable && + flags.strongerAuthRequiredAfterNonStrongBiometricsTimeout + ) { + PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT + } else { + PROMPT_REASON_NONE + } + } + + private val biometricAuthReason: Flow<Int> = + conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onBiometricAuthFailed( + biometricSourceType: BiometricSourceType? + ) { + val promptReason = + if (biometricSourceType == FINGERPRINT) + PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT + else if ( + biometricSourceType == FACE && !updateMonitor.isFaceLockedOut + ) { + PROMPT_REASON_INCORRECT_FACE_INPUT + } else PROMPT_REASON_NONE + trySendWithFailureLogging(promptReason, TAG, "onBiometricAuthFailed") + } + + override fun onBiometricsCleared() { + trySendWithFailureLogging( + PROMPT_REASON_NONE, + TAG, + "onBiometricsCleared" + ) + } + + override fun onBiometricAcquired( + biometricSourceType: BiometricSourceType?, + acquireInfo: Int + ) { + trySendWithFailureLogging( + PROMPT_REASON_NONE, + TAG, + "clearBiometricPrompt for new auth session." + ) + } + + override fun onBiometricAuthenticated( + userId: Int, + biometricSourceType: BiometricSourceType?, + isStrongBiometric: Boolean + ) { + trySendWithFailureLogging( + PROMPT_REASON_NONE, + TAG, + "onBiometricAuthenticated" + ) + } + } + updateMonitor.registerCallback(callback) + awaitClose { updateMonitor.removeCallback(callback) } + } + .distinctUntilChanged() + + private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val primaryAuthMessage: Flow<BouncerMessageModel?> = _primaryAuthMessage + + private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val faceAcquisitionMessage: Flow<BouncerMessageModel?> = _faceAcquisitionMessage + + private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?> = + _fingerprintAcquisitionMessage + + private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val customMessage: Flow<BouncerMessageModel?> = _customMessage + + override val biometricAuthMessage: Flow<BouncerMessageModel?> = + biometricAuthReason + .map { + if (it == PROMPT_REASON_NONE) null + else + bouncerMessageFactory.createFromPromptReason( + it, + userRepository.getSelectedUserInfo().id + ) + } + .onStart { emit(null) } + .distinctUntilChanged() + + override val authFlagsMessage: Flow<BouncerMessageModel?> = + authFlagsBasedPromptReason + .map { + if (it == PROMPT_REASON_NONE) null + else + bouncerMessageFactory.createFromPromptReason( + it, + userRepository.getSelectedUserInfo().id + ) + } + .onStart { emit(null) } + .distinctUntilChanged() + + // TODO (b/262838215): Replace with DeviceEntryFaceAuthRepository when the new face auth system + // has been launched. + private val faceLockedOut: Flow<Boolean> = conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onLockedOutStateChanged(biometricSourceType: BiometricSourceType?) { + if (biometricSourceType == FACE) { + trySendWithFailureLogging( + updateMonitor.isFaceLockedOut, + TAG, + "face lock out state changed." + ) + } + } + } + updateMonitor.registerCallback(callback) + trySendWithFailureLogging(updateMonitor.isFaceLockedOut, TAG, "face lockout initial value") + awaitClose { updateMonitor.removeCallback(callback) } + } + + override val biometricLockedOutMessage: Flow<BouncerMessageModel?> = + combine(fingerprintAuthRepository.isLockedOut, faceLockedOut) { fp, face -> + return@combine if (fp) { + bouncerMessageFactory.createFromPromptReason( + PROMPT_REASON_FINGERPRINT_LOCKED_OUT, + userRepository.getSelectedUserInfo().id + ) + } else if (face) { + bouncerMessageFactory.createFromPromptReason( + PROMPT_REASON_FACE_LOCKED_OUT, + userRepository.getSelectedUserInfo().id + ) + } else null + } + + override fun setPrimaryAuthMessage(value: BouncerMessageModel?) { + _primaryAuthMessage.value = value + } + + override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) { + _faceAcquisitionMessage.value = value + } + + override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) { + _fingerprintAcquisitionMessage.value = value + } + + override fun setCustomMessage(value: BouncerMessageModel?) { + _customMessage.value = value + } + + override fun clearMessage() { + _fingerprintAcquisitionMessage.value = null + _faceAcquisitionMessage.value = null + _primaryAuthMessage.value = null + _customMessage.value = null + } + + companion object { + const val TAG = "BouncerDetailedMessageRepository" + } +} + +private fun and(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) = + flow.combine(anotherFlow) { a, b -> a && b } + +private fun or(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) = + flow.combine(anotherFlow) { a, b -> a || b } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageAuditLogger.kt new file mode 100644 index 000000000000..56f81fca0221 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageAuditLogger.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.bouncer.domain.interactor + +import android.os.Build +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository +import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +private val TAG = BouncerMessageAuditLogger::class.simpleName!! + +/** Logger that echoes bouncer messages state to logcat in debuggable builds. */ +@SysUISingleton +class BouncerMessageAuditLogger +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val repository: BouncerMessageRepository, + private val interactor: BouncerMessageInteractor, +) : CoreStartable { + override fun start() { + if (Build.isDebuggable()) { + collectAndLog(repository.biometricAuthMessage, "biometricMessage: ") + collectAndLog(repository.primaryAuthMessage, "primaryAuthMessage: ") + collectAndLog(repository.customMessage, "customMessage: ") + collectAndLog(repository.faceAcquisitionMessage, "faceAcquisitionMessage: ") + collectAndLog( + repository.fingerprintAcquisitionMessage, + "fingerprintAcquisitionMessage: " + ) + collectAndLog(repository.authFlagsMessage, "authFlagsMessage: ") + collectAndLog(interactor.bouncerMessage, "interactor.bouncerMessage: ") + } + } + + private fun collectAndLog(flow: Flow<BouncerMessageModel?>, context: String) { + scope.launch { flow.collect { Log.d(TAG, context + it) } } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt new file mode 100644 index 000000000000..1754d934ee3a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.bouncer.domain.interactor + +import android.os.CountDownTimer +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES +import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory +import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository +import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlin.math.roundToInt +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 + +@SysUISingleton +class BouncerMessageInteractor +@Inject +constructor( + private val repository: BouncerMessageRepository, + private val factory: BouncerMessageFactory, + private val userRepository: UserRepository, + private val countDownTimerUtil: CountDownTimerUtil, + private val featureFlags: FeatureFlags, +) { + fun onPrimaryAuthLockedOut(secondsBeforeLockoutReset: Long) { + if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return + + val callback = + object : CountDownTimerCallback { + override fun onFinish() { + repository.clearMessage() + } + + override fun onTick(millisUntilFinished: Long) { + val secondsRemaining = (millisUntilFinished / 1000.0).roundToInt() + val message = + factory.createFromPromptReason( + reason = PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT, + userId = userRepository.getSelectedUserInfo().id + ) + message?.message?.animate = false + message?.message?.formatterArgs = + mutableMapOf<String, Any>(Pair("count", secondsRemaining)) + repository.setPrimaryAuthMessage(message) + } + } + countDownTimerUtil.startNewTimer(secondsBeforeLockoutReset * 1000, 1000, callback) + } + + fun onPrimaryAuthIncorrectAttempt() { + if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return + + repository.setPrimaryAuthMessage( + factory.createFromPromptReason( + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT, + userRepository.getSelectedUserInfo().id + ) + ) + } + + fun setFingerprintAcquisitionMessage(value: String?) { + if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return + + repository.setFingerprintAcquisitionMessage( + if (value != null) { + factory.createFromString(secondaryMsg = value) + } else { + null + } + ) + } + + fun setFaceAcquisitionMessage(value: String?) { + if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return + + repository.setFaceAcquisitionMessage( + if (value != null) { + factory.createFromString(secondaryMsg = value) + } else { + null + } + ) + } + + fun setCustomMessage(value: String?) { + if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return + + repository.setCustomMessage( + if (value != null) { + factory.createFromString(secondaryMsg = value) + } else { + null + } + ) + } + + fun onPrimaryBouncerUserInput() { + if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return + + repository.clearMessage() + } + + fun onBouncerBeingHidden() { + if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return + + repository.clearMessage() + } + + private fun firstNonNullMessage( + oneMessageModel: Flow<BouncerMessageModel?>, + anotherMessageModel: Flow<BouncerMessageModel?> + ): Flow<BouncerMessageModel?> { + return oneMessageModel.combine(anotherMessageModel) { a, b -> a ?: b } + } + + // Null if feature flag is enabled which gets ignored always or empty bouncer message model that + // always maps to an empty string. + private fun nullOrEmptyMessage() = + flowOf( + if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null + else factory.createFromString("", "") + ) + + val bouncerMessage = + listOf( + nullOrEmptyMessage(), + repository.primaryAuthMessage, + repository.biometricAuthMessage, + repository.fingerprintAcquisitionMessage, + repository.faceAcquisitionMessage, + repository.customMessage, + repository.authFlagsMessage, + repository.biometricLockedOutMessage, + userRepository.selectedUserInfo.map { + factory.createFromPromptReason(PROMPT_REASON_DEFAULT, it.id) + }, + ) + .reduce(::firstNonNullMessage) + .distinctUntilChanged() +} + +interface CountDownTimerCallback { + fun onFinish() + fun onTick(millisUntilFinished: Long) +} + +@SysUISingleton +open class CountDownTimerUtil @Inject constructor() { + + /** + * Start a new count down timer that runs for [millisInFuture] with a tick every + * [millisInterval] + */ + fun startNewTimer( + millisInFuture: Long, + millisInterval: Long, + callback: CountDownTimerCallback, + ): CountDownTimer { + return object : CountDownTimer(millisInFuture, millisInterval) { + override fun onFinish() = callback.onFinish() + + override fun onTick(millisUntilFinished: Long) = + callback.onTick(millisUntilFinished) + } + .start() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/shared/model/BouncerMessageModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/shared/model/BouncerMessageModel.kt new file mode 100644 index 000000000000..46e88733ae6c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/shared/model/BouncerMessageModel.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.bouncer.shared.model + +import android.content.res.ColorStateList + +/** + * Represents the message displayed on the bouncer. It has two parts, primary and a secondary + * message + */ +data class BouncerMessageModel(val message: Message? = null, val secondaryMessage: Message? = null) + +/** + * Representation of a single message on the bouncer. It can be either a string or a string resource + * ID + */ +data class Message( + val message: String? = null, + val messageResId: Int? = null, + val colorState: ColorStateList? = null, + /** Any plural formatter arguments that can used to format the [messageResId] */ + var formatterArgs: Map<String, Any>? = null, + /** Specifies whether this text should be animated when it is shown. */ + var animate: Boolean = true, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt new file mode 100644 index 000000000000..c0a5a51a910d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/ui/BouncerMessageView.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.bouncer.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.android.keyguard.BouncerKeyguardMessageArea +import com.android.keyguard.KeyguardMessageArea +import com.android.keyguard.KeyguardMessageAreaController +import com.android.systemui.R + +class BouncerMessageView : LinearLayout { + constructor(context: Context?) : super(context) + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + + init { + inflate(context, R.layout.bouncer_message_view, this) + } + + var primaryMessageView: BouncerKeyguardMessageArea? = null + var secondaryMessageView: BouncerKeyguardMessageArea? = null + var primaryMessage: KeyguardMessageAreaController<KeyguardMessageArea>? = null + var secondaryMessage: KeyguardMessageAreaController<KeyguardMessageArea>? = null + override fun onFinishInflate() { + super.onFinishInflate() + primaryMessageView = findViewById(R.id.bouncer_primary_message_area) + secondaryMessageView = findViewById(R.id.bouncer_secondary_message_area) + } + + fun init(factory: KeyguardMessageAreaController.Factory) { + primaryMessage = factory.create(primaryMessageView) + primaryMessage?.init() + secondaryMessage = factory.create(secondaryMessageView) + secondaryMessage?.init() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index deb8f5d96cbd..d7c039d9b519 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -64,6 +64,7 @@ import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.util.DeviceConfigProxy; +import com.android.wm.shell.keyguard.KeyguardTransitions; import dagger.Lazy; import dagger.Module; @@ -86,6 +87,7 @@ import java.util.concurrent.Executor; KeyguardRepositoryModule.class, KeyguardFaceAuthModule.class, StartKeyguardTransitionModule.class, + ResourceTrimmerModule.class, }) public class KeyguardModule { /** @@ -119,6 +121,7 @@ public class KeyguardModule { ScreenOffAnimationController screenOffAnimationController, Lazy<NotificationShadeDepthController> notificationShadeDepthController, ScreenOnCoordinator screenOnCoordinator, + KeyguardTransitions keyguardTransitions, InteractionJankMonitor interactionJankMonitor, DreamOverlayStateController dreamOverlayStateController, Lazy<ShadeController> shadeController, @@ -152,6 +155,7 @@ public class KeyguardModule { screenOffAnimationController, notificationShadeDepthController, screenOnCoordinator, + keyguardTransitions, interactionJankMonitor, dreamOverlayStateController, shadeController, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java new file mode 100644 index 000000000000..d693326f0dba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.dagger; + +import com.android.systemui.CoreStartable; +import com.android.systemui.keyguard.ResourceTrimmer; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; + +/** + * Binds {@link ResourceTrimmer} into {@link CoreStartable} set. + */ +@Module +public abstract class ResourceTrimmerModule { + + /** Bind ResourceTrimmer into CoreStarteables. */ + @Binds + @IntoMap + @ClassKey(ResourceTrimmer.class) + public abstract CoreStartable bindResourceTrimmer(ResourceTrimmer resourceTrimmer); +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt index 0055f9a36529..0b6c7c415599 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt @@ -25,7 +25,6 @@ import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback import android.os.UserHandle import android.util.Log import com.android.internal.widget.LockPatternUtils -import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.biometrics.AuthController @@ -36,13 +35,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.TAG +import com.android.systemui.keyguard.shared.model.AuthenticationFlags import com.android.systemui.keyguard.shared.model.DevicePosture import com.android.systemui.user.data.repository.UserRepository import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -108,10 +108,14 @@ interface BiometricSettingsRepository { * lockdown. */ val isCurrentUserInLockdown: Flow<Boolean> + + /** Authentication flags set for the current user. */ + val authenticationFlags: Flow<AuthenticationFlags> } -const val TAG = "BiometricsRepositoryImpl" +private const val TAG = "BiometricsRepositoryImpl" +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class BiometricSettingsRepositoryImpl @Inject @@ -129,6 +133,8 @@ constructor( dumpManager: DumpManager, ) : BiometricSettingsRepository, Dumpable { + private val biometricsEnabledForUser = mutableMapOf<Int, Boolean>() + override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> private val strongAuthTracker = StrongAuthTracker(userRepository, context) @@ -136,6 +142,9 @@ constructor( override val isCurrentUserInLockdown: Flow<Boolean> = strongAuthTracker.currentUserAuthFlags.map { it.isInUserLockdown } + override val authenticationFlags: Flow<AuthenticationFlags> = + strongAuthTracker.currentUserAuthFlags + init { Log.d(TAG, "Registering StrongAuthTracker") lockPatternUtils.registerStrongAuthTracker(strongAuthTracker) @@ -231,9 +240,14 @@ constructor( } } + private val isFaceEnabledByBiometricsManagerForCurrentUser: Flow<Boolean> = + userRepository.selectedUserInfo.flatMapLatest { userInfo -> + isFaceEnabledByBiometricsManager.map { biometricsEnabledForUser[userInfo.id] ?: false } + } + override val isFaceAuthenticationEnabled: Flow<Boolean> get() = - combine(isFaceEnabledByBiometricsManager, isFaceEnabledByDevicePolicy) { + combine(isFaceEnabledByBiometricsManagerForCurrentUser, isFaceEnabledByDevicePolicy) { biometricsManagerSetting, devicePolicySetting -> biometricsManagerSetting && devicePolicySetting @@ -249,13 +263,13 @@ constructor( .flowOn(backgroundDispatcher) .distinctUntilChanged() - private val isFaceEnabledByBiometricsManager = + private val isFaceEnabledByBiometricsManager: Flow<Pair<Int, Boolean>> = conflatedCallbackFlow { val callback = object : IBiometricEnabledOnKeyguardCallback.Stub() { override fun onChanged(enabled: Boolean, userId: Int) { trySendWithFailureLogging( - enabled, + Pair(userId, enabled), TAG, "biometricsEnabled state changed" ) @@ -264,9 +278,10 @@ constructor( biometricManager?.registerEnabledOnKeyguardCallback(callback) awaitClose {} } + .onEach { biometricsEnabledForUser[it.first] = it.second } // This is because the callback is binder-based and we want to avoid multiple callbacks // being registered. - .stateIn(scope, SharingStarted.Eagerly, false) + .stateIn(scope, SharingStarted.Eagerly, Pair(0, false)) override val isStrongBiometricAllowed: StateFlow<Boolean> = strongAuthTracker.isStrongBiometricAllowed.stateIn( @@ -306,14 +321,13 @@ constructor( ) } +@OptIn(ExperimentalCoroutinesApi::class) private class StrongAuthTracker(private val userRepository: UserRepository, context: Context?) : LockPatternUtils.StrongAuthTracker(context) { // Backing field for onStrongAuthRequiredChanged - private val _strongAuthFlags = - MutableStateFlow( - StrongAuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId)) - ) + private val _authFlags = + MutableStateFlow(AuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId))) // Backing field for onIsNonStrongBiometricAllowedChanged private val _nonStrongBiometricAllowed = @@ -321,17 +335,15 @@ private class StrongAuthTracker(private val userRepository: UserRepository, cont Pair(currentUserId, isNonStrongBiometricAllowedAfterIdleTimeout(currentUserId)) ) - val currentUserAuthFlags: Flow<StrongAuthenticationFlags> = + val currentUserAuthFlags: Flow<AuthenticationFlags> = userRepository.selectedUserInfo .map { it.id } .distinctUntilChanged() .flatMapLatest { userId -> - _strongAuthFlags - .filter { it.userId == userId } + _authFlags + .map { AuthenticationFlags(userId, getStrongAuthForUser(userId)) } .onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") } - .onStart { - emit(StrongAuthenticationFlags(userId, getStrongAuthForUser(userId))) - } + .onStart { emit(AuthenticationFlags(userId, getStrongAuthForUser(userId))) } } /** isStrongBiometricAllowed for the current user. */ @@ -356,7 +368,7 @@ private class StrongAuthTracker(private val userRepository: UserRepository, cont override fun onStrongAuthRequiredChanged(userId: Int) { val newFlags = getStrongAuthForUser(userId) - _strongAuthFlags.value = StrongAuthenticationFlags(userId, newFlags) + _authFlags.value = AuthenticationFlags(userId, newFlags) Log.d(TAG, "onStrongAuthRequiredChanged for userId: $userId, flag value: $newFlags") } @@ -375,11 +387,3 @@ private fun DevicePolicyManager.isFingerprintDisabled(userId: Int): Boolean = private fun DevicePolicyManager.isNotActive(userId: Int, policy: Int): Boolean = (getKeyguardDisabledFeatures(null, userId) and policy) == 0 - -private data class StrongAuthenticationFlags(val userId: Int, val flag: Int) { - val isInUserLockdown = containsFlag(flag, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN) -} - -private fun containsFlag(haystack: Int, needle: Int): Boolean { - return haystack and needle != 0 -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt index 7c14280a7858..5d15e69f0162 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt @@ -22,7 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel -import com.android.systemui.log.dagger.BouncerLog +import com.android.systemui.log.dagger.BouncerTableLog import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.util.time.SystemClock @@ -105,7 +105,7 @@ class KeyguardBouncerRepositoryImpl constructor( private val clock: SystemClock, @Application private val applicationScope: CoroutineScope, - @BouncerLog private val buffer: TableLogBuffer, + @BouncerTableLog private val buffer: TableLogBuffer, ) : KeyguardBouncerRepository { /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */ private val _primaryBouncerShow = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt index e7b9af62dacf..4055fd0bbc49 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt @@ -16,8 +16,14 @@ package com.android.systemui.keyguard.data.repository +import com.android.systemui.CoreStartable +import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository +import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepositoryImpl +import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageAuditLogger import dagger.Binds import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap @Module interface KeyguardRepositoryModule { @@ -46,5 +52,13 @@ interface KeyguardRepositoryModule { @Binds fun keyguardBouncerRepository(impl: KeyguardBouncerRepositoryImpl): KeyguardBouncerRepository + @Binds + fun bouncerMessageRepository(impl: BouncerMessageRepositoryImpl): BouncerMessageRepository + + @Binds + @IntoMap + @ClassKey(BouncerMessageAuditLogger::class) + fun bind(impl: BouncerMessageAuditLogger): CoreStartable + @Binds fun trustRepository(impl: TrustRepositoryImpl): TrustRepository } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt index f2f1c48476bc..867675bab790 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt @@ -22,6 +22,7 @@ import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLoggin import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.shared.model.ActiveUnlockModel import com.android.systemui.keyguard.shared.model.TrustManagedModel import com.android.systemui.keyguard.shared.model.TrustModel import com.android.systemui.user.data.repository.UserRepository @@ -29,7 +30,6 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -45,8 +45,8 @@ interface TrustRepository { /** Flow representing whether the current user is trusted. */ val isCurrentUserTrusted: Flow<Boolean> - /** Flow representing whether active unlock is available for the current user. */ - val isCurrentUserActiveUnlockAvailable: StateFlow<Boolean> + /** Flow representing whether active unlock is running for the current user. */ + val isCurrentUserActiveUnlockRunning: Flow<Boolean> /** Reports that whether trust is managed has changed for the current user. */ val isCurrentUserTrustManaged: StateFlow<Boolean> @@ -62,6 +62,7 @@ constructor( private val logger: TrustRepositoryLogger, ) : TrustRepository { private val latestTrustModelForUser = mutableMapOf<Int, TrustModel>() + private val activeUnlockRunningForUser = mutableMapOf<Int, ActiveUnlockModel>() private val trustManagedForUser = mutableMapOf<Int, TrustManagedModel>() private val trust = @@ -87,6 +88,17 @@ constructor( override fun onEnabledTrustAgentsChanged(userId: Int) = Unit + override fun onIsActiveUnlockRunningChanged( + isRunning: Boolean, + userId: Int + ) { + trySendWithFailureLogging( + ActiveUnlockModel(isRunning, userId), + TrustRepositoryLogger.TAG, + "onActiveUnlockRunningChanged" + ) + } + override fun onTrustManagedChanged(isTrustManaged: Boolean, userId: Int) { logger.onTrustManagedChanged(isTrustManaged, userId) trySendWithFailureLogging( @@ -95,11 +107,6 @@ constructor( "onTrustManagedChanged" ) } - - override fun onIsActiveUnlockRunningChanged( - isRunning: Boolean, - userId: Int - ) = Unit } trustManager.registerTrustListener(callback) logger.trustListenerRegistered() @@ -114,6 +121,10 @@ constructor( latestTrustModelForUser[it.userId] = it logger.trustModelEmitted(it) } + is ActiveUnlockModel -> { + activeUnlockRunningForUser[it.userId] = it + logger.activeUnlockModelEmitted(it) + } is TrustManagedModel -> { trustManagedForUser[it.userId] = it logger.trustManagedModelEmitted(it) @@ -122,8 +133,17 @@ constructor( } .shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1) - // TODO: Implement based on TrustManager callback b/267322286 - override val isCurrentUserActiveUnlockAvailable: StateFlow<Boolean> = MutableStateFlow(true) + override val isCurrentUserActiveUnlockRunning: Flow<Boolean> = + combine(trust, userRepository.selectedUserInfo, ::Pair) + .map { activeUnlockRunningForUser[it.second.id]?.isRunning ?: false } + .distinctUntilChanged() + .onEach { logger.isCurrentUserActiveUnlockRunning(it) } + .onStart { + emit( + activeUnlockRunningForUser[userRepository.getSelectedUserInfo().id]?.isRunning + ?: false + ) + } override val isCurrentUserTrustManaged: StateFlow<Boolean> get() = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt index ea6700e92731..ca430da0ffce 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractor.kt @@ -17,12 +17,14 @@ package com.android.systemui.keyguard.domain.interactor +import android.content.Context import android.content.Intent import android.content.IntentFilter import android.view.accessibility.AccessibilityManager import androidx.annotation.VisibleForTesting import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -55,6 +57,7 @@ import kotlinx.coroutines.launch class KeyguardLongPressInteractor @Inject constructor( + @Application private val appContext: Context, @Application private val scope: CoroutineScope, transitionInteractor: KeyguardTransitionInteractor, repository: KeyguardRepository, @@ -169,7 +172,8 @@ constructor( private fun isFeatureEnabled(): Boolean { return featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED) && - featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI) + featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI) && + appContext.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled) } /** Updates application state to ask to show the menu. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 8e65c4d0a836..22753376a5d3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -19,12 +19,15 @@ package com.android.systemui.keyguard.domain.interactor import android.app.AlertDialog import android.app.admin.DevicePolicyManager +import android.content.Context import android.content.Intent import android.util.Log import com.android.internal.widget.LockPatternUtils +import com.android.systemui.R import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled import com.android.systemui.dock.DockManager @@ -75,6 +78,7 @@ constructor( private val devicePolicyManager: DevicePolicyManager, private val dockManager: DockManager, @Background private val backgroundDispatcher: CoroutineDispatcher, + @Application private val appContext: Context, ) { private val isUsingRepository: Boolean get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) @@ -408,7 +412,8 @@ constructor( name = Contract.FlagsTable.FLAG_NAME_CUSTOM_LOCK_SCREEN_QUICK_AFFORDANCES_ENABLED, value = !isFeatureDisabledByDevicePolicy() && - featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES), + featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) && + appContext.resources.getBoolean(R.bool.custom_lockscreen_shortcuts_enabled), ), KeyguardPickerFlag( name = Contract.FlagsTable.FLAG_NAME_CUSTOM_CLOCKS_ENABLED, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt new file mode 100644 index 000000000000..d0bc25fd26f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.dagger.qualifiers.Application +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.util.kotlin.pairwise +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Hosts business and application state accessing logic for the lockscreen scene. */ +class LockscreenSceneInteractor +@AssistedInject +constructor( + @Application applicationScope: CoroutineScope, + private val authenticationInteractor: AuthenticationInteractor, + bouncerInteractorFactory: BouncerInteractor.Factory, + private val sceneInteractor: SceneInteractor, + @Assisted private val containerName: String, +) { + private val bouncerInteractor: BouncerInteractor = + bouncerInteractorFactory.create(containerName) + + /** Whether the device is currently locked. */ + val isDeviceLocked: StateFlow<Boolean> = + authenticationInteractor.isUnlocked + .map { !it } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = !authenticationInteractor.isUnlocked.value, + ) + + /** Whether it's currently possible to swipe up to dismiss the lockscreen. */ + val isSwipeToDismissEnabled: StateFlow<Boolean> = + combine( + authenticationInteractor.isUnlocked, + authenticationInteractor.authenticationMethod, + ) { isUnlocked, authMethod -> + isSwipeToUnlockEnabled( + isUnlocked = isUnlocked, + authMethod = authMethod, + ) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = + isSwipeToUnlockEnabled( + isUnlocked = authenticationInteractor.isUnlocked.value, + authMethod = authenticationInteractor.authenticationMethod.value, + ), + ) + + init { + // LOCKING SHOWS Lockscreen. + // + // Move to the lockscreen scene if the device becomes locked while in any scene. + applicationScope.launch { + authenticationInteractor.isUnlocked + .map { !it } + .distinctUntilChanged() + .collect { isLocked -> + if (isLocked) { + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Lockscreen), + ) + } + } + } + + // BYPASS UNLOCK. + // + // Moves to the gone scene if bypass is enabled and the device becomes unlocked while in the + // lockscreen scene. + applicationScope.launch { + combine( + authenticationInteractor.isBypassEnabled, + authenticationInteractor.isUnlocked, + sceneInteractor.currentScene(containerName), + ::Triple, + ) + .collect { (isBypassEnabled, isUnlocked, currentScene) -> + if (isBypassEnabled && isUnlocked && currentScene.key == SceneKey.Lockscreen) { + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Gone), + ) + } + } + } + + // SWIPE TO DISMISS Lockscreen. + // + // If switched from the lockscreen to the gone scene and the auth method was a swipe, + // unlocks the device. + applicationScope.launch { + combine( + authenticationInteractor.authenticationMethod, + sceneInteractor.currentScene(containerName).pairwise(), + ::Pair, + ) + .collect { (authMethod, scenes) -> + val (previousScene, currentScene) = scenes + if ( + authMethod is AuthenticationMethodModel.Swipe && + previousScene.key == SceneKey.Lockscreen && + currentScene.key == SceneKey.Gone + ) { + authenticationInteractor.unlockDevice() + } + } + } + + // DISMISS Lockscreen IF AUTH METHOD IS REMOVED. + // + // If the auth method becomes None while on the lockscreen scene, dismisses the lock + // screen. + applicationScope.launch { + combine( + authenticationInteractor.authenticationMethod, + sceneInteractor.currentScene(containerName), + ::Pair, + ) + .collect { (authMethod, scene) -> + if ( + scene.key == SceneKey.Lockscreen && + authMethod == AuthenticationMethodModel.None + ) { + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Gone), + ) + } + } + } + } + + /** Attempts to dismiss the lockscreen. This will cause the bouncer to show, if needed. */ + fun dismissLockscreen() { + bouncerInteractor.showOrUnlockDevice(containerName = containerName) + } + + private fun isSwipeToUnlockEnabled( + isUnlocked: Boolean, + authMethod: AuthenticationMethodModel, + ): Boolean { + return !isUnlocked && authMethod is AuthenticationMethodModel.Swipe + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): LockscreenSceneInteractor + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt index 110bcd715be2..54bc349cc4ac 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt @@ -31,6 +31,7 @@ import com.android.systemui.DejankUtils import com.android.systemui.R import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags @@ -39,15 +40,19 @@ import com.android.systemui.keyguard.data.BouncerView import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants +import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel import com.android.systemui.plugins.ActivityStarter import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.statusbar.policy.KeyguardStateController +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import javax.inject.Inject /** @@ -58,18 +63,19 @@ import javax.inject.Inject class PrimaryBouncerInteractor @Inject constructor( - private val repository: KeyguardBouncerRepository, - private val primaryBouncerView: BouncerView, - @Main private val mainHandler: Handler, - private val keyguardStateController: KeyguardStateController, - private val keyguardSecurityModel: KeyguardSecurityModel, - private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor, - private val falsingCollector: FalsingCollector, - private val dismissCallbackRegistry: DismissCallbackRegistry, - private val context: Context, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val trustRepository: TrustRepository, - private val featureFlags: FeatureFlags, + private val repository: KeyguardBouncerRepository, + private val primaryBouncerView: BouncerView, + @Main private val mainHandler: Handler, + private val keyguardStateController: KeyguardStateController, + private val keyguardSecurityModel: KeyguardSecurityModel, + private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor, + private val falsingCollector: FalsingCollector, + private val dismissCallbackRegistry: DismissCallbackRegistry, + private val context: Context, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val trustRepository: TrustRepository, + private val featureFlags: FeatureFlags, + @Application private val applicationScope: CoroutineScope, ) { private val passiveAuthBouncerDelay = context.resources.getInteger( R.integer.primary_bouncer_passive_auth_delay).toLong() @@ -104,6 +110,7 @@ constructor( /** Allow for interaction when just about fully visible */ val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 } val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing + private var currentUserActiveUnlockRunning = false /** This callback needs to be a class field so it does not get garbage collected. */ val keyguardUpdateMonitorCallback = @@ -122,6 +129,13 @@ constructor( init { keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) + if (featureFlags.isEnabled(Flags.DELAY_BOUNCER)) { + applicationScope.launch { + trustRepository.isCurrentUserActiveUnlockRunning.collect { + currentUserActiveUnlockRunning = it + } + } + } } // TODO(b/243685699): Move isScrimmed logic to data layer. @@ -183,6 +197,7 @@ constructor( cancelShowRunnable() repository.setPrimaryShowingSoon(false) repository.setPrimaryShow(false) + repository.setPanelExpansion(EXPANSION_HIDDEN) primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE) Trace.endSection() } @@ -377,8 +392,9 @@ constructor( private fun usePrimaryBouncerPassiveAuthDelay(): Boolean { val canRunFaceAuth = keyguardStateController.isFaceAuthEnabled && keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE) - val canRunActiveUnlock = trustRepository.isCurrentUserActiveUnlockAvailable.value && + val canRunActiveUnlock = currentUserActiveUnlockRunning && keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState() + return featureFlags.isEnabled(Flags.DELAY_BOUNCER) && !needsFullscreenBouncer() && (canRunFaceAuth || canRunActiveUnlock) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ActiveUnlockModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ActiveUnlockModel.kt new file mode 100644 index 000000000000..730907240b0e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ActiveUnlockModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.shared.model + +/** Represents the active unlock state */ +data class ActiveUnlockModel( + /** If true, the system believes active unlock is available and can be usd to unlock. */ + val isRunning: Boolean, + /** The user, for which active unlock may be running. */ + val userId: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt new file mode 100644 index 000000000000..cf5b88fde3dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.shared.model + +import com.android.internal.widget.LockPatternUtils + +/** Authentication flags corresponding to a user. */ +data class AuthenticationFlags(val userId: Int, val flag: Int) { + val isInUserLockdown = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN + ) + + val isPrimaryAuthRequiredAfterReboot = + containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT) + + val isPrimaryAuthRequiredAfterTimeout = + containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT) + + val isPrimaryAuthRequiredAfterDpmLockdown = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW + ) + + val someAuthRequiredAfterUserRequest = + containsFlag(flag, LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST) + + val someAuthRequiredAfterTrustAgentExpired = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED + ) + + val primaryAuthRequiredForUnattendedUpdate = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE + ) + + /** Either Class 3 biometrics or primary auth can be used to unlock the device. */ + val strongerAuthRequiredAfterNonStrongBiometricsTimeout = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT + ) +} + +private fun containsFlag(haystack: Int, needle: Int): Boolean { + return haystack and needle != 0 +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt index c8bd958615cf..9e7dec4dc1d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt @@ -38,7 +38,8 @@ data class AcquiredAuthenticationStatus(val acquiredInfo: Int) : AuthenticationS object FailedAuthenticationStatus : AuthenticationStatus() /** Face authentication error message */ -data class ErrorAuthenticationStatus(val msgId: Int, val msg: String?) : AuthenticationStatus() { +data class ErrorAuthenticationStatus(val msgId: Int, val msg: String? = null) : + AuthenticationStatus() { /** * Method that checks if [msgId] is a lockout error. A lockout error means that face * authentication is locked out. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 3aa57dde3178..555a09baa5b7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -25,6 +25,7 @@ import android.content.IntentFilter import android.graphics.Rect import android.hardware.display.DisplayManager import android.os.Bundle +import android.os.Handler import android.os.IBinder import android.view.LayoutInflater import android.view.SurfaceControlViewHost @@ -41,6 +42,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.shared.clocks.ClockRegistry +import com.android.systemui.shared.clocks.DefaultClockController import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController @@ -57,6 +59,7 @@ class KeyguardPreviewRenderer constructor( @Application private val context: Context, @Main private val mainDispatcher: CoroutineDispatcher, + @Main private val mainHandler: Handler, private val bottomAreaViewModel: KeyguardBottomAreaViewModel, displayManager: DisplayManager, private val windowManager: WindowManager, @@ -112,7 +115,7 @@ constructor( } fun render() { - runBlocking(mainDispatcher) { + mainHandler.post { val rootView = FrameLayout(context) setUpBottomArea(rootView) @@ -121,7 +124,9 @@ constructor( setUpUdfps(rootView) - setUpClock(rootView) + if (!shouldHideClock) { + setUpClock(rootView) + } rootView.measure( View.MeasureSpec.makeMeasureSpec( @@ -168,14 +173,12 @@ constructor( * @param hide TRUE hides smartspace, FALSE shows smartspace */ fun hideSmartspace(hide: Boolean) { - runBlocking(mainDispatcher) { - smartSpaceView?.visibility = if (hide) View.INVISIBLE else View.VISIBLE - } + mainHandler.post { smartSpaceView?.visibility = if (hide) View.INVISIBLE else View.VISIBLE } } /** Sets the clock's color to the overridden seed color. */ fun onColorOverridden(@ColorInt color: Int?) { - runBlocking(mainDispatcher) { + mainHandler.post { colorOverride = color clockController.clock?.run { events.onSeedColorChanged(color) } } @@ -313,6 +316,36 @@ constructor( ) disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }) + val layoutChangeListener = + object : View.OnLayoutChangeListener { + override fun onLayoutChange( + v: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + if (clockController.clock !is DefaultClockController) { + clockController.clock + ?.largeClock + ?.events + ?.onTargetRegionChanged( + KeyguardClockSwitch.getLargeClockRegion(parentView) + ) + } + } + } + + parentView.addOnLayoutChangeListener(layoutChangeListener) + + disposables.add( + DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) } + ) + onClockChanged(parentView) } @@ -321,28 +354,23 @@ constructor( clockController.clock = clock colorOverride?.let { clock.events.onSeedColorChanged(it) } - if (!shouldHideClock) { - clock.largeClock.events.onTargetRegionChanged( - KeyguardClockSwitch.getLargeClockRegion(parentView) - ) - clockView?.let { parentView.removeView(it) } - clockView = - clock.largeClock.view.apply { - if (shouldHighlightSelectedAffordance) { - alpha = DIM_ALPHA - } - parentView.addView(this) - visibility = View.VISIBLE + clock.largeClock.events.onTargetRegionChanged( + KeyguardClockSwitch.getLargeClockRegion(parentView) + ) + + clockView?.let { parentView.removeView(it) } + clockView = + clock.largeClock.view.apply { + if (shouldHighlightSelectedAffordance) { + alpha = DIM_ALPHA } - } else { - clockView?.visibility = View.GONE - } + parentView.addView(this) + visibility = View.VISIBLE + } // Hide smart space if the clock has weather display; otherwise show it - val hasCustomWeatherDataDisplay = - clock.largeClock.config.hasCustomWeatherDataDisplay == true - hideSmartspace(hasCustomWeatherDataDisplay) + hideSmartspace(clock.largeClock.config.hasCustomWeatherDataDisplay) } companion object { 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 new file mode 100644 index 000000000000..f212a553aeb3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.R +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Models UI state and handles user input for the lockscreen scene. */ +class LockscreenSceneViewModel +@AssistedInject +constructor( + @Application applicationScope: CoroutineScope, + interactorFactory: LockscreenSceneInteractor.Factory, + @Assisted containerName: String, +) { + private val interactor: LockscreenSceneInteractor = interactorFactory.create(containerName) + + /** The icon for the "lock" button on the lockscreen. */ + val lockButtonIcon: StateFlow<Icon> = + interactor.isDeviceLocked + .map { isLocked -> lockIcon(isLocked = isLocked) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = lockIcon(isLocked = interactor.isDeviceLocked.value), + ) + + /** The key of the scene we should switch to when swiping up. */ + val upDestinationSceneKey: StateFlow<SceneKey> = + interactor.isSwipeToDismissEnabled + .map { isSwipeToUnlockEnabled -> upDestinationSceneKey(isSwipeToUnlockEnabled) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = upDestinationSceneKey(interactor.isSwipeToDismissEnabled.value), + ) + + /** Notifies that the lock button on the lock screen was clicked. */ + fun onLockButtonClicked() { + interactor.dismissLockscreen() + } + + /** Notifies that some content on the lock screen was clicked. */ + fun onContentClicked() { + interactor.dismissLockscreen() + } + + private fun upDestinationSceneKey( + isSwipeToUnlockEnabled: Boolean, + ): SceneKey { + return if (isSwipeToUnlockEnabled) SceneKey.Gone else SceneKey.Bouncer + } + + private fun lockIcon( + isLocked: Boolean, + ): Icon { + return Icon.Resource( + res = + if (isLocked) { + R.drawable.ic_device_lock_on + } else { + R.drawable.ic_device_lock_off + }, + contentDescription = + ContentDescription.Resource( + res = + if (isLocked) { + R.string.accessibility_lock_icon + } else { + R.string.accessibility_unlock_button + } + ) + ) + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): LockscreenSceneViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt b/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt new file mode 100644 index 000000000000..3be4499517b9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt @@ -0,0 +1,62 @@ +/* + * 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.log + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel +import com.android.systemui.log.dagger.BouncerLog +import javax.inject.Inject + +private const val TAG = "BouncerLog" + +/** + * Helper class for logging for classes in the [com.android.systemui.keyguard.bouncer] package. + * + * To enable logcat echoing for an entire buffer: + * ``` + * adb shell settings put global systemui/buffer/BouncerLog <logLevel> + * + * ``` + */ +@SysUISingleton +class BouncerLogger @Inject constructor(@BouncerLog private val buffer: LogBuffer) { + fun startBouncerMessageInteractor() { + buffer.log( + TAG, + LogLevel.DEBUG, + "Starting BouncerMessageInteractor.bouncerMessage collector" + ) + } + + fun bouncerMessageUpdated(bouncerMsg: BouncerMessageModel?) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + int1 = bouncerMsg?.message?.messageResId ?: -1 + str1 = bouncerMsg?.message?.message + int2 = bouncerMsg?.secondaryMessage?.messageResId ?: -1 + str2 = bouncerMsg?.secondaryMessage?.message + }, + { "Bouncer message update received: $int1, $str1, $int2, $str2" } + ) + } + + fun bindingBouncerMessageView() { + buffer.log(TAG, LogLevel.DEBUG, "Binding BouncerMessageView") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerLog.kt index 2251a7b243aa..0c2e7319efc1 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerLog.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerLog.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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. @@ -16,10 +16,7 @@ package com.android.systemui.log.dagger -import java.lang.annotation.Documented -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy import javax.inject.Qualifier -/** Logger for the primary and alternative bouncers. */ -@Qualifier @Documented @Retention(RetentionPolicy.RUNTIME) annotation class BouncerLog +/** A [com.android.systemui.log.LogBuffer] for bouncer and its child views. */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class BouncerLog() diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerTableLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerTableLog.kt new file mode 100644 index 000000000000..08df7db65af1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerTableLog.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger + +import java.lang.annotation.Documented +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import javax.inject.Qualifier + +/** Logger for the primary and alternative bouncers. */ +@Qualifier @Documented @Retention(RetentionPolicy.RUNTIME) annotation class BouncerTableLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CarrierTextManagerLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/CarrierTextManagerLog.kt new file mode 100644 index 000000000000..62b80b20a673 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CarrierTextManagerLog.kt @@ -0,0 +1,9 @@ +package com.android.systemui.log.dagger + +import javax.inject.Qualifier + +/** A [LogBuffer] for detailed carrier text logs. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class CarrierTextManagerLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 9be18ace79fa..0261ee53a0ff 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -60,7 +60,7 @@ public class LogModule { if (Compile.IS_DEBUG && notifPipelineFlags.isDevLoggingEnabled()) { maxSize *= 10; } - return factory.create("NotifLog", maxSize, false /* systrace */); + return factory.create("NotifLog", maxSize, Compile.IS_DEBUG /* systrace */); } /** Provides a logging buffer for all logs related to notifications on the lockscreen. */ @@ -373,6 +373,16 @@ public class LogModule { } /** + * Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}. + */ + @Provides + @SysUISingleton + @CarrierTextManagerLog + public static LogBuffer provideCarrierTextManagerLog(LogBufferFactory factory) { + return factory.create("CarrierTextManagerLog", 100); + } + + /** * Provides a {@link LogBuffer} for use by {@link com.android.systemui.ScreenDecorations}. */ @Provides @@ -394,6 +404,17 @@ public class LogModule { } /** + * Provides a {@link LogBuffer} for use by classes in the + * {@link com.android.systemui.keyguard.bouncer} package. + */ + @Provides + @SysUISingleton + @BouncerLog + public static LogBuffer provideBouncerLog(LogBufferFactory factory) { + return factory.create("BouncerLog", 100); + } + + /** * Provides a {@link LogBuffer} for Device State Auto-Rotation logs. */ @Provides @@ -416,9 +437,17 @@ public class LogModule { /** Provides a logging buffer for the primary bouncer. */ @Provides @SysUISingleton - @BouncerLog + @BouncerTableLog public static TableLogBuffer provideBouncerLogBuffer(TableLogBufferFactory factory) { - return factory.create("BouncerLog", 250); + return factory.create("BouncerTableLog", 250); + } + + /** Provides a table logging buffer for the Monitor. */ + @Provides + @SysUISingleton + @MonitorLog + public static TableLogBuffer provideMonitorTableLogBuffer(TableLogBufferFactory factory) { + return factory.create("MonitorLog", 250); } /** diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MonitorLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/MonitorLog.kt new file mode 100644 index 000000000000..b6a4ec935d14 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MonitorLog.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger + +import javax.inject.Qualifier +import kotlin.annotation.Retention + +/** Logger for Monitor. */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class MonitorLog diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt index cabe319d317d..8d622ae1ca03 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt @@ -23,6 +23,7 @@ import com.android.systemui.common.buffer.RingBuffer import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogLevel import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.TableLogBufferBase import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import java.util.Locale @@ -84,7 +85,7 @@ class TableLogBuffer( @Background private val bgDispatcher: CoroutineDispatcher, private val coroutineScope: CoroutineScope, private val localLogcat: LogProxy = LogProxyDefault(), -) : Dumpable { +) : Dumpable, TableLogBufferBase { init { if (maxSize <= 0) { throw IllegalArgumentException("maxSize must be > 0") @@ -177,7 +178,7 @@ class TableLogBuffer( * * @param isInitial see [TableLogBuffer.logChange(String, Boolean, (TableRowLogger) -> Unit]. */ - fun logChange(prefix: String, columnName: String, value: String?, isInitial: Boolean = false) { + override fun logChange(prefix: String, columnName: String, value: String?, isInitial: Boolean) { logChange(systemClock.currentTimeMillis(), prefix, columnName, value, isInitial) } @@ -186,7 +187,7 @@ class TableLogBuffer( * * @param isInitial see [TableLogBuffer.logChange(String, Boolean, (TableRowLogger) -> Unit]. */ - fun logChange(prefix: String, columnName: String, value: Boolean, isInitial: Boolean = false) { + override fun logChange(prefix: String, columnName: String, value: Boolean, isInitial: Boolean) { logChange(systemClock.currentTimeMillis(), prefix, columnName, value, isInitial) } @@ -195,7 +196,7 @@ class TableLogBuffer( * * @param isInitial see [TableLogBuffer.logChange(String, Boolean, (TableRowLogger) -> Unit]. */ - fun logChange(prefix: String, columnName: String, value: Int?, isInitial: Boolean = false) { + override fun logChange(prefix: String, columnName: String, value: Int?, isInitial: Boolean) { logChange(systemClock.currentTimeMillis(), prefix, columnName, value, isInitial) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index fa4211487d1d..5079487a6261 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -43,6 +43,7 @@ import android.media.session.PlaybackState import android.net.Uri import android.os.Parcelable import android.os.Process +import android.os.RemoteException import android.os.UserHandle import android.provider.Settings import android.service.notification.StatusBarNotification @@ -52,6 +53,7 @@ import android.util.Log import android.util.Pair as APair import androidx.media.utils.MediaConstants import com.android.internal.logging.InstanceId +import com.android.internal.statusbar.IStatusBarService import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Dumpable import com.android.systemui.R @@ -137,6 +139,8 @@ internal val EMPTY_SMARTSPACE_MEDIA_DATA = expiryTimeMs = 0, ) +const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank." + fun isMediaNotification(sbn: StatusBarNotification): Boolean { return sbn.notification.isMediaNotification() } @@ -181,6 +185,7 @@ class MediaDataManager( private val logger: MediaUiEventLogger, private val smartspaceManager: SmartspaceManager, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val statusBarService: IStatusBarService, ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener { companion object { @@ -252,6 +257,7 @@ class MediaDataManager( mediaFlags: MediaFlags, logger: MediaUiEventLogger, smartspaceManager: SmartspaceManager, + statusBarService: IStatusBarService, keyguardUpdateMonitor: KeyguardUpdateMonitor, ) : this( context, @@ -277,6 +283,7 @@ class MediaDataManager( logger, smartspaceManager, keyguardUpdateMonitor, + statusBarService, ) private val appChangeReceiver = @@ -378,21 +385,21 @@ class MediaDataManager( fun onNotificationAdded(key: String, sbn: StatusBarNotification) { if (useQsMediaPlayer && isMediaNotification(sbn)) { - var logEvent = false + var isNewlyActiveEntry = false Assert.isMainThread() val oldKey = findExistingEntry(key, sbn.packageName) if (oldKey == null) { val instanceId = logger.getNewInstanceId() val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId) mediaEntries.put(key, temp) - logEvent = true + isNewlyActiveEntry = true } else if (oldKey != key) { // Resume -> active conversion; move to new key val oldData = mediaEntries.remove(oldKey)!! - logEvent = true + isNewlyActiveEntry = true mediaEntries.put(key, oldData) } - loadMediaData(key, sbn, oldKey, logEvent) + loadMediaData(key, sbn, oldKey, isNewlyActiveEntry) } else { onNotificationRemoved(key) } @@ -475,9 +482,9 @@ class MediaDataManager( key: String, sbn: StatusBarNotification, oldKey: String?, - logEvent: Boolean = false + isNewlyActiveEntry: Boolean = false, ) { - backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, logEvent) } + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } } /** Add a listener for changes in this class */ @@ -601,9 +608,11 @@ class MediaDataManager( } } - private fun removeEntry(key: String) { + private fun removeEntry(key: String, logEvent: Boolean = true) { mediaEntries.remove(key)?.let { - logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) + if (logEvent) { + logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) + } } notifyMediaDataRemoved(key) } @@ -751,7 +760,7 @@ class MediaDataManager( key: String, sbn: StatusBarNotification, oldKey: String?, - logEvent: Boolean = false + isNewlyActiveEntry: Boolean = false, ) { val token = sbn.notification.extras.getParcelable( @@ -772,6 +781,42 @@ class MediaDataManager( ) ?: getAppInfoFromPackage(sbn.packageName) + // App name + val appName = getAppName(sbn, appInfo) + + // Song name + var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song == null) { + song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song == null) { + song = HybridGroupManager.resolveTitle(notif) + } + if (song.isNullOrBlank()) { + if (mediaFlags.isMediaTitleRequired(sbn.packageName, sbn.user)) { + // App is required to provide a title: cancel the underlying notification + try { + statusBarService.onNotificationError( + sbn.packageName, + sbn.tag, + sbn.id, + sbn.uid, + sbn.initialPid, + MEDIA_TITLE_ERROR_MESSAGE, + sbn.user.identifier + ) + } catch (e: RemoteException) { + Log.e(TAG, "cancelNotification failed: $e") + } + // Only add log for media removed if active media is updated with invalid title. + foregroundExecutor.execute { removeEntry(key, !isNewlyActiveEntry) } + return + } else { + // For apps that don't have the title requirement yet, add a placeholder + song = context.getString(R.string.controls_media_empty_title, appName) + } + } + // Album art var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } if (artworkBitmap == null) { @@ -787,21 +832,9 @@ class MediaDataManager( Icon.createWithBitmap(artworkBitmap) } - // App name - val appName = getAppName(sbn, appInfo) - // App Icon val smallIcon = sbn.notification.smallIcon - // Song name - var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) - if (song == null) { - song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) - } - if (song == null) { - song = HybridGroupManager.resolveTitle(notif) - } - // Explicit Indicator var isExplicit = false if (mediaFlags.isExplicitIndicatorEnabled()) { @@ -873,7 +906,7 @@ class MediaDataManager( val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() val appUid = appInfo?.uid ?: Process.INVALID_UID - if (logEvent) { + if (isNewlyActiveEntry) { logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) } else if (playbackLocation != currentEntry?.playbackLocation) { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 8f0ac28382a4..9eda7ae2586d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -1235,6 +1235,8 @@ public class MediaControlPanel { if ((buttonId == R.id.actionPrev && semanticActions.getReservePrev()) || (buttonId == R.id.actionNext && semanticActions.getReserveNext())) { notVisibleValue = ConstraintSet.INVISIBLE; + mMediaViewHolder.getAction(buttonId).setFocusable(visible); + mMediaViewHolder.getAction(buttonId).setClickable(visible); } else { notVisibleValue = ConstraintSet.GONE; } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 9bc66f6c98d0..3751c60b06d5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -64,4 +64,9 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { /** Check whether we allow remote media to generate resume controls */ fun isRemoteResumeAllowed() = featureFlags.isEnabled(Flags.MEDIA_REMOTE_RESUME) + + /** Check whether app is required to provide a non-empty media title */ + fun isMediaTitleRequired(packageName: String, user: UserHandle): Boolean { + return StatusBarManager.isMediaTitleRequiredForApp(packageName, user) + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index d949cf56ff0e..316c903eed5b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -72,102 +72,67 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { super.onCreateViewHolder(viewGroup, viewType); - if (mController.isAdvancedLayoutSupported()) { - switch (viewType) { - case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: - return new MediaGroupDividerViewHolder(mHolderView); - case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: - case MediaItem.MediaItemType.TYPE_DEVICE: - default: - return new MediaDeviceViewHolder(mHolderView); - } - } else { - return new MediaDeviceViewHolder(mHolderView); + switch (viewType) { + case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: + return new MediaGroupDividerViewHolder(mHolderView); + case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: + case MediaItem.MediaItemType.TYPE_DEVICE: + default: + return new MediaDeviceViewHolder(mHolderView); } } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - if (mController.isAdvancedLayoutSupported()) { - if (position >= mMediaItemList.size()) { - if (DEBUG) { - Log.d(TAG, "Incorrect position: " + position + " list size: " - + mMediaItemList.size()); - } - return; + if (position >= mMediaItemList.size()) { + if (DEBUG) { + Log.d(TAG, "Incorrect position: " + position + " list size: " + + mMediaItemList.size()); } - MediaItem currentMediaItem = mMediaItemList.get(position); - switch (currentMediaItem.getMediaItemType()) { - case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: - ((MediaGroupDividerViewHolder) viewHolder).onBind(currentMediaItem.getTitle()); - break; - case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: - ((MediaDeviceViewHolder) viewHolder).onBind(CUSTOMIZED_ITEM_PAIR_NEW); - break; - case MediaItem.MediaItemType.TYPE_DEVICE: - ((MediaDeviceViewHolder) viewHolder).onBind( - currentMediaItem.getMediaDevice().get(), - position); - break; - default: - Log.d(TAG, "Incorrect position: " + position); - } - } else { - final int size = mController.getMediaDevices().size(); - if (position == size) { + return; + } + MediaItem currentMediaItem = mMediaItemList.get(position); + switch (currentMediaItem.getMediaItemType()) { + case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: + ((MediaGroupDividerViewHolder) viewHolder).onBind(currentMediaItem.getTitle()); + break; + case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: ((MediaDeviceViewHolder) viewHolder).onBind(CUSTOMIZED_ITEM_PAIR_NEW); - } else if (position < size) { + break; + case MediaItem.MediaItemType.TYPE_DEVICE: ((MediaDeviceViewHolder) viewHolder).onBind( - ((List<MediaDevice>) (mController.getMediaDevices())).get(position), + currentMediaItem.getMediaDevice().get(), position); - } else if (DEBUG) { + break; + default: Log.d(TAG, "Incorrect position: " + position); - } } } @Override public long getItemId(int position) { - if (mController.isAdvancedLayoutSupported()) { - if (position >= mMediaItemList.size()) { - Log.d(TAG, "Incorrect position for item id: " + position); - return position; - } - MediaItem currentMediaItem = mMediaItemList.get(position); - return currentMediaItem.getMediaDevice().isPresent() - ? currentMediaItem.getMediaDevice().get().getId().hashCode() - : position; - } - final int size = mController.getMediaDevices().size(); - if (position == size) { - return -1; - } else if (position < size) { - return ((List<MediaDevice>) (mController.getMediaDevices())) - .get(position).getId().hashCode(); - } else if (DEBUG) { + if (position >= mMediaItemList.size()) { Log.d(TAG, "Incorrect position for item id: " + position); + return position; } - return position; + MediaItem currentMediaItem = mMediaItemList.get(position); + return currentMediaItem.getMediaDevice().isPresent() + ? currentMediaItem.getMediaDevice().get().getId().hashCode() + : position; } @Override public int getItemViewType(int position) { - if (mController.isAdvancedLayoutSupported() - && position >= mMediaItemList.size()) { + if (position >= mMediaItemList.size()) { Log.d(TAG, "Incorrect position for item type: " + position); return MediaItem.MediaItemType.TYPE_GROUP_DIVIDER; } - return mController.isAdvancedLayoutSupported() - ? mMediaItemList.get(position).getMediaItemType() - : super.getItemViewType(position); + return mMediaItemList.get(position).getMediaItemType(); } @Override public int getItemCount() { - // Add extra one for "pair new" - return mController.isAdvancedLayoutSupported() - ? mMediaItemList.size() - : mController.getMediaDevices().size() + 1; + return mMediaItemList.size(); } class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder { @@ -203,17 +168,14 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { // Set different layout for each device if (device.isMutingExpectedDevice() && !mController.isCurrentConnectedDeviceRemote()) { - if (!mController.isAdvancedLayoutSupported()) { - updateTitleIcon(R.drawable.media_output_icon_volume, - mController.getColorItemContent()); - } - initMutingExpectedDevice(); + updateTitleIcon(R.drawable.media_output_icon_volume, + mController.getColorItemContent()); mCurrentActivePosition = position; updateFullItemClickListener(v -> onItemClick(v, device)); setSingleLineLayout(getItemTitle(device)); + initMutingExpectedDevice(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - && mController.isSubStatusSupported() - && mController.isAdvancedLayoutSupported() && device.hasSubtext()) { + && device.hasSubtext()) { boolean isActiveWithOngoingSession = (device.hasOngoingSession() && (currentlyConnected || isDeviceIncluded( mController.getSelectedMediaDevice(), device))); @@ -284,10 +246,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { // selected device in group boolean isDeviceDeselectable = isDeviceIncluded( mController.getDeselectableMediaDevice(), device); - if (!mController.isAdvancedLayoutSupported()) { - updateTitleIcon(R.drawable.media_output_icon_volume, - mController.getColorItemContent()); - } + updateTitleIcon(R.drawable.media_output_icon_volume, + mController.getColorItemContent()); updateGroupableCheckBox(true, isDeviceDeselectable, device); updateEndClickArea(device, isDeviceDeselectable); setUpContentDescriptionForView(mContainerLayout, false, device); @@ -305,8 +265,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { updateFullItemClickListener(v -> cancelMuteAwaitConnection()); setSingleLineLayout(getItemTitle(device)); } else if (mController.isCurrentConnectedDeviceRemote() - && !mController.getSelectableMediaDevice().isEmpty() - && mController.isAdvancedLayoutSupported()) { + && !mController.getSelectableMediaDevice().isEmpty()) { //If device is connected and there's other selectable devices, layout as // one of selected devices. updateTitleIcon(R.drawable.media_output_icon_volume, @@ -334,36 +293,27 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { //groupable device setUpDeviceIcon(device); updateGroupableCheckBox(false, true, device); - if (mController.isAdvancedLayoutSupported()) { - updateEndClickArea(device, true); - } - updateFullItemClickListener(mController.isAdvancedLayoutSupported() - ? v -> onItemClick(v, device) - : v -> onGroupActionTriggered(true, device)); + updateEndClickArea(device, true); + updateFullItemClickListener(v -> onItemClick(v, device)); setSingleLineLayout(getItemTitle(device), false /* showSeekBar */, false /* showProgressBar */, true /* showCheckBox */, true /* showEndTouchArea */); } else { setUpDeviceIcon(device); setSingleLineLayout(getItemTitle(device)); - if (mController.isAdvancedLayoutSupported() - && mController.isSubStatusSupported()) { - Drawable deviceStatusIcon = - device.hasOngoingSession() ? mContext.getDrawable( - R.drawable.ic_sound_bars_anim) - : Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior( - device, - mContext); - if (deviceStatusIcon != null) { - updateDeviceStatusIcon(deviceStatusIcon); - mStatusIcon.setVisibility(View.VISIBLE); - } - updateSingleLineLayoutContentAlpha( - updateClickActionBasedOnSelectionBehavior(device) - ? DEVICE_CONNECTED_ALPHA : DEVICE_DISCONNECTED_ALPHA); - } else { - updateFullItemClickListener(v -> onItemClick(v, device)); + Drawable deviceStatusIcon = + device.hasOngoingSession() ? mContext.getDrawable( + R.drawable.ic_sound_bars_anim) + : Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior( + device, + mContext); + if (deviceStatusIcon != null) { + updateDeviceStatusIcon(deviceStatusIcon); + mStatusIcon.setVisibility(View.VISIBLE); } + updateSingleLineLayoutContentAlpha( + updateClickActionBasedOnSelectionBehavior(device) + ? DEVICE_CONNECTED_ALPHA : DEVICE_DISCONNECTED_ALPHA); } } } @@ -400,10 +350,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } public void updateEndClickAreaColor(int color) { - if (mController.isAdvancedLayoutSupported()) { - mEndTouchArea.setBackgroundTintList( - ColorStateList.valueOf(color)); - } + mEndTouchArea.setBackgroundTintList( + ColorStateList.valueOf(color)); } private boolean updateClickActionBasedOnSelectionBehavior(MediaDevice device) { @@ -440,10 +388,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null); mEndTouchArea.setImportantForAccessibility( View.IMPORTANT_FOR_ACCESSIBILITY_YES); - if (mController.isAdvancedLayoutSupported()) { - mEndTouchArea.setBackgroundTintList( - ColorStateList.valueOf(mController.getColorItemBackground())); - } + mEndTouchArea.setBackgroundTintList( + ColorStateList.valueOf(mController.getColorItemBackground())); setUpContentDescriptionForView(mEndTouchArea, true, device); } @@ -473,10 +419,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { mTitleIcon.setImageDrawable(addDrawable); mTitleIcon.setImageTintList( ColorStateList.valueOf(mController.getColorItemContent())); - if (mController.isAdvancedLayoutSupported()) { - mIconAreaLayout.setBackgroundTintList( - ColorStateList.valueOf(mController.getColorItemBackground())); - } + mIconAreaLayout.setBackgroundTintList( + ColorStateList.valueOf(mController.getColorItemBackground())); mContainerLayout.setOnClickListener(mController::launchBluetoothPairing); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 151dbb2746aa..01f790422bc3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -47,7 +47,6 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.RecyclerView; -import com.android.settingslib.Utils; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.R; @@ -90,9 +89,8 @@ public abstract class MediaOutputBaseAdapter extends public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { mContext = viewGroup.getContext(); - mHolderView = LayoutInflater.from(mContext).inflate( - mController.isAdvancedLayoutSupported() ? MediaItem.getMediaLayoutId(viewType) - : R.layout.media_output_list_item, viewGroup, false); + mHolderView = LayoutInflater.from(mContext).inflate(MediaItem.getMediaLayoutId(viewType), + viewGroup, false); return null; } @@ -174,15 +172,9 @@ public abstract class MediaOutputBaseAdapter extends mStatusIcon = view.requireViewById(R.id.media_output_item_status); mCheckBox = view.requireViewById(R.id.check_box); mEndTouchArea = view.requireViewById(R.id.end_action_area); - if (mController.isAdvancedLayoutSupported()) { - mEndClickIcon = view.requireViewById(R.id.media_output_item_end_click_icon); - mVolumeValueText = view.requireViewById(R.id.volume_value); - mIconAreaLayout = view.requireViewById(R.id.icon_area); - } else { - mVolumeValueText = null; - mIconAreaLayout = null; - mEndClickIcon = null; - } + mEndClickIcon = view.requireViewById(R.id.media_output_item_end_click_icon); + mVolumeValueText = view.requireViewById(R.id.volume_value); + mIconAreaLayout = view.requireViewById(R.id.icon_area); initAnimator(); } @@ -194,13 +186,10 @@ public abstract class MediaOutputBaseAdapter extends mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); mContainerLayout.setOnClickListener(null); mContainerLayout.setContentDescription(null); - mTitleIcon.setOnClickListener(null); mTitleText.setTextColor(mController.getColorItemContent()); mSubTitleText.setTextColor(mController.getColorItemContent()); mTwoLineTitleText.setTextColor(mController.getColorItemContent()); - if (mController.isAdvancedLayoutSupported()) { - mVolumeValueText.setTextColor(mController.getColorItemContent()); - } + mVolumeValueText.setTextColor(mController.getColorItemContent()); mSeekBar.setProgressTintList( ColorStateList.valueOf(mController.getColorSeekbarProgress())); } @@ -231,12 +220,10 @@ public abstract class MediaOutputBaseAdapter extends mItemLayout.setBackgroundTintList( ColorStateList.valueOf(isActive ? mController.getColorConnectedItemBackground() : mController.getColorItemBackground())); - if (mController.isAdvancedLayoutSupported()) { - mIconAreaLayout.setBackgroundTintList( - ColorStateList.valueOf(showSeekBar ? mController.getColorSeekbarProgress() - : showProgressBar ? mController.getColorConnectedItemBackground() - : mController.getColorItemBackground())); - } + mIconAreaLayout.setBackgroundTintList( + ColorStateList.valueOf(showSeekBar ? mController.getColorSeekbarProgress() + : showProgressBar ? mController.getColorConnectedItemBackground() + : mController.getColorItemBackground())); mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE); mSeekBar.setAlpha(1); mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE); @@ -247,12 +234,10 @@ public abstract class MediaOutputBaseAdapter extends mTitleText.setVisibility(View.VISIBLE); mCheckBox.setVisibility(showCheckBox ? View.VISIBLE : View.GONE); mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE); - if (mController.isAdvancedLayoutSupported()) { - ViewGroup.MarginLayoutParams params = - (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams(); - params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable() - : mController.getItemMarginEndDefault(); - } + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams(); + params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable() + : mController.getItemMarginEndDefault(); mTitleIcon.setBackgroundTintList( ColorStateList.valueOf(mController.getColorItemContent())); } @@ -273,34 +258,28 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setAlpha(1); mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE); final Drawable backgroundDrawable; - if (mController.isAdvancedLayoutSupported() && mController.isSubStatusSupported()) { - backgroundDrawable = mContext.getDrawable( - showSeekBar || isFakeActive ? R.drawable.media_output_item_background_active - : R.drawable.media_output_item_background).mutate(); - backgroundDrawable.setTint( - showSeekBar || isFakeActive ? mController.getColorConnectedItemBackground() - : mController.getColorItemBackground()); - mIconAreaLayout.setBackgroundTintList( - ColorStateList.valueOf(showProgressBar || isFakeActive - ? mController.getColorConnectedItemBackground() - : showSeekBar ? mController.getColorSeekbarProgress() - : mController.getColorItemBackground())); - if (showSeekBar) { - updateSeekbarProgressBackground(); - } - //update end click area by isActive - mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE); - mEndClickIcon.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE); - ViewGroup.MarginLayoutParams params = - (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams(); - params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable() - : mController.getItemMarginEndDefault(); - } else { - backgroundDrawable = mContext.getDrawable( - R.drawable.media_output_item_background) - .mutate(); - backgroundDrawable.setTint(mController.getColorItemBackground()); + backgroundDrawable = mContext.getDrawable( + showSeekBar || isFakeActive ? R.drawable.media_output_item_background_active + : R.drawable.media_output_item_background).mutate(); + mItemLayout.setBackgroundTintList(ColorStateList.valueOf( + showSeekBar || isFakeActive ? mController.getColorConnectedItemBackground() + : mController.getColorItemBackground() + )); + mIconAreaLayout.setBackgroundTintList( + ColorStateList.valueOf(showProgressBar || isFakeActive + ? mController.getColorConnectedItemBackground() + : showSeekBar ? mController.getColorSeekbarProgress() + : mController.getColorItemBackground())); + if (showSeekBar) { + updateSeekbarProgressBackground(); } + //update end click area by isActive + mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE); + mEndClickIcon.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE); + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams(); + params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable() + : mController.getItemMarginEndDefault(); mItemLayout.setBackground(backgroundDrawable); mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE); mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE); @@ -318,15 +297,11 @@ public abstract class MediaOutputBaseAdapter extends .findDrawableByLayerId(android.R.id.progress); final GradientDrawable progressDrawable = (GradientDrawable) clipDrawable.getDrawable(); - if (mController.isAdvancedLayoutSupported()) { - progressDrawable.setCornerRadii( - new float[]{0, 0, mController.getActiveRadius(), - mController.getActiveRadius(), - mController.getActiveRadius(), - mController.getActiveRadius(), 0, 0}); - } else { - progressDrawable.setCornerRadius(mController.getActiveRadius()); - } + progressDrawable.setCornerRadii( + new float[]{0, 0, mController.getActiveRadius(), + mController.getActiveRadius(), + mController.getActiveRadius(), + mController.getActiveRadius(), 0, 0}); } void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) { @@ -337,19 +312,14 @@ public abstract class MediaOutputBaseAdapter extends } mSeekBar.setMaxVolume(device.getMaxVolume()); final int currentVolume = device.getCurrentVolume(); - if (mSeekBar.getVolume() != currentVolume) { - if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) { - if (mController.isAdvancedLayoutSupported()) { + if (!mIsDragging) { + if (mSeekBar.getVolume() != currentVolume) { + if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) { updateTitleIcon(currentVolume == 0 ? R.drawable.media_output_icon_volume_off : R.drawable.media_output_icon_volume, mController.getColorItemContent()); } else { - animateCornerAndVolume(mSeekBar.getProgress(), - MediaOutputSeekbar.scaleVolumeToProgress(currentVolume)); - } - } else { - if (!mVolumeAnimator.isStarted()) { - if (mController.isAdvancedLayoutSupported()) { + if (!mVolumeAnimator.isStarted()) { int percentage = (int) ((double) currentVolume * VOLUME_PERCENTAGE_SCALE_SIZE / (double) mSeekBar.getMax()); @@ -358,18 +328,19 @@ public abstract class MediaOutputBaseAdapter extends } else { updateUnmutedVolumeIcon(); } + mSeekBar.setVolume(currentVolume); } - mSeekBar.setVolume(currentVolume); } + } else if (currentVolume == 0) { + mSeekBar.resetVolume(); + updateMutedVolumeIcon(); } - } else if (mController.isAdvancedLayoutSupported() && currentVolume == 0) { - mSeekBar.resetVolume(); - updateMutedVolumeIcon(); } if (mIsInitVolumeFirstTime) { mIsInitVolumeFirstTime = false; } mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + boolean mStartFromMute = false; @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (device == null || !fromUser) { @@ -377,48 +348,43 @@ public abstract class MediaOutputBaseAdapter extends } int progressToVolume = MediaOutputSeekbar.scaleProgressToVolume(progress); int deviceVolume = device.getCurrentVolume(); - if (mController.isAdvancedLayoutSupported()) { - int percentage = - (int) ((double) progressToVolume * VOLUME_PERCENTAGE_SCALE_SIZE - / (double) seekBar.getMax()); - mVolumeValueText.setText(mContext.getResources().getString( - R.string.media_output_dialog_volume_percentage, percentage)); - mVolumeValueText.setVisibility(View.VISIBLE); + int percentage = + (int) ((double) progressToVolume * VOLUME_PERCENTAGE_SCALE_SIZE + / (double) seekBar.getMax()); + mVolumeValueText.setText(mContext.getResources().getString( + R.string.media_output_dialog_volume_percentage, percentage)); + mVolumeValueText.setVisibility(View.VISIBLE); + if (mStartFromMute) { + updateUnmutedVolumeIcon(); + mStartFromMute = false; } if (progressToVolume != deviceVolume) { mController.adjustVolume(device, progressToVolume); - if (mController.isAdvancedLayoutSupported() && deviceVolume == 0) { - updateUnmutedVolumeIcon(); - } } } @Override public void onStartTrackingTouch(SeekBar seekBar) { - if (mController.isAdvancedLayoutSupported()) { - mTitleIcon.setVisibility(View.INVISIBLE); - mVolumeValueText.setVisibility(View.VISIBLE); - } + mTitleIcon.setVisibility(View.INVISIBLE); + mVolumeValueText.setVisibility(View.VISIBLE); + int currentVolume = MediaOutputSeekbar.scaleProgressToVolume( + seekBar.getProgress()); + mStartFromMute = (currentVolume == 0); mIsDragging = true; } @Override public void onStopTrackingTouch(SeekBar seekBar) { - if (mController.isAdvancedLayoutSupported()) { - int currentVolume = MediaOutputSeekbar.scaleProgressToVolume( - seekBar.getProgress()); - int percentage = - (int) ((double) currentVolume * VOLUME_PERCENTAGE_SCALE_SIZE - / (double) seekBar.getMax()); - if (percentage == 0) { - seekBar.setProgress(0); - updateMutedVolumeIcon(); - } else { - updateUnmutedVolumeIcon(); - } - mTitleIcon.setVisibility(View.VISIBLE); - mVolumeValueText.setVisibility(View.GONE); + int currentVolume = MediaOutputSeekbar.scaleProgressToVolume( + seekBar.getProgress()); + if (currentVolume == 0) { + seekBar.setProgress(0); + updateMutedVolumeIcon(); + } else { + updateUnmutedVolumeIcon(); } + mTitleIcon.setVisibility(View.VISIBLE); + mVolumeValueText.setVisibility(View.GONE); mController.logInteractionAdjustVolume(device); mIsDragging = false; } @@ -443,23 +409,26 @@ public abstract class MediaOutputBaseAdapter extends void updateTitleIcon(@DrawableRes int id, int color) { mTitleIcon.setImageDrawable(mContext.getDrawable(id)); mTitleIcon.setImageTintList(ColorStateList.valueOf(color)); - if (mController.isAdvancedLayoutSupported()) { - mIconAreaLayout.setBackgroundTintList( - ColorStateList.valueOf(mController.getColorSeekbarProgress())); - } + mIconAreaLayout.setBackgroundTintList( + ColorStateList.valueOf(mController.getColorSeekbarProgress())); } void updateIconAreaClickListener(View.OnClickListener listener) { - mTitleIcon.setOnClickListener(listener); + mIconAreaLayout.setOnClickListener(listener); } void initMutingExpectedDevice() { disableSeekBar(); + updateTitleIcon(R.drawable.media_output_icon_volume, + mController.getColorItemContent()); final Drawable backgroundDrawable = mContext.getDrawable( R.drawable.media_output_item_background_active) .mutate(); - backgroundDrawable.setTint(mController.getColorConnectedItemBackground()); mItemLayout.setBackground(backgroundDrawable); + mItemLayout.setBackgroundTintList( + ColorStateList.valueOf(mController.getColorConnectedItemBackground())); + mIconAreaLayout.setBackgroundTintList( + ColorStateList.valueOf(mController.getColorConnectedItemBackground())); } private void animateCornerAndVolume(int fromProgress, int toProgress) { @@ -469,24 +438,18 @@ public abstract class MediaOutputBaseAdapter extends (ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable()) .findDrawableByLayerId(android.R.id.progress); final GradientDrawable targetBackgroundDrawable = - (GradientDrawable) (mController.isAdvancedLayoutSupported() - ? mIconAreaLayout.getBackground() - : clipDrawable.getDrawable()); + (GradientDrawable) (mIconAreaLayout.getBackground()); mCornerAnimator.addUpdateListener(animation -> { float value = (float) animation.getAnimatedValue(); layoutBackgroundDrawable.setCornerRadius(value); - if (mController.isAdvancedLayoutSupported()) { - if (toProgress == 0) { - targetBackgroundDrawable.setCornerRadius(value); - } else { - targetBackgroundDrawable.setCornerRadii(new float[]{ - value, - value, - 0, 0, 0, 0, value, value - }); - } - } else { + if (toProgress == 0) { targetBackgroundDrawable.setCornerRadius(value); + } else { + targetBackgroundDrawable.setCornerRadii(new float[]{ + value, + value, + 0, 0, 0, 0, value, value + }); } }); mVolumeAnimator.setIntValues(fromProgress, toProgress); @@ -530,20 +493,10 @@ public abstract class MediaOutputBaseAdapter extends }); } - Drawable getSpeakerDrawable() { - final Drawable drawable = mContext.getDrawable(R.drawable.ic_speaker_group_black_24dp) - .mutate(); - drawable.setTint(Utils.getColorStateListDefaultColor(mContext, - R.color.media_dialog_item_main_content)); - return drawable; - } - protected void disableSeekBar() { mSeekBar.setEnabled(false); mSeekBar.setOnTouchListener((v, event) -> true); - if (mController.isAdvancedLayoutSupported()) { - updateIconAreaClickListener(null); - } + updateIconAreaClickListener(null); } private void enableSeekBar(MediaDevice device) { @@ -551,14 +504,15 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setOnTouchListener((v, event) -> false); updateIconAreaClickListener((v) -> { if (device.getCurrentVolume() == 0) { + mSeekBar.setVolume(UNMUTE_DEFAULT_VOLUME); mController.adjustVolume(device, UNMUTE_DEFAULT_VOLUME); updateUnmutedVolumeIcon(); - mTitleIcon.setOnTouchListener(((iconV, event) -> false)); + mIconAreaLayout.setOnTouchListener(((iconV, event) -> false)); } else { mSeekBar.resetVolume(); mController.adjustVolume(device, 0); updateMutedVolumeIcon(); - mTitleIcon.setOnTouchListener(((iconV, event) -> { + mIconAreaLayout.setOnTouchListener(((iconV, event) -> { mSeekBar.dispatchTouchEvent(event); return false; })); @@ -574,7 +528,6 @@ public abstract class MediaOutputBaseAdapter extends return; } mTitleIcon.setImageIcon(icon); - icon.setTint(mController.getColorItemContent()); mTitleIcon.setImageTintList( ColorStateList.valueOf(mController.getColorItemContent())); }); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 770e4dfe0d7d..0a5b4b3066a0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -101,6 +101,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements private Button mAppButton; private int mListMaxHeight; private int mItemHeight; + private int mListPaddingTop; private WallpaperColors mWallpaperColors; private boolean mShouldLaunchLeBroadcastDialog; private boolean mIsLeBroadcastCallbackRegistered; @@ -111,7 +112,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> { ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams(); - int totalItemsHeight = mAdapter.getItemCount() * mItemHeight; + int totalItemsHeight = mAdapter.getItemCount() * mItemHeight + + mListPaddingTop; int correctHeight = Math.min(totalItemsHeight, mListMaxHeight); // Set max height for list if (correctHeight != params.height) { @@ -220,6 +222,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements R.dimen.media_output_dialog_list_max_height); mItemHeight = context.getResources().getDimensionPixelSize( R.dimen.media_output_dialog_list_item_height); + mListPaddingTop = mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_list_padding_top); mExecutor = Executors.newSingleThreadExecutor(); } @@ -266,10 +270,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements dismiss(); }); mAppButton.setOnClickListener(mMediaOutputController::tryToLaunchMediaApplication); - if (mMediaOutputController.isAdvancedLayoutSupported()) { - mMediaMetadataSectionLayout.setOnClickListener( - mMediaOutputController::tryToLaunchMediaApplication); - } + mMediaMetadataSectionLayout.setOnClickListener( + mMediaOutputController::tryToLaunchMediaApplication); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt index cdd00f99fa29..a1e9995dd695 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt @@ -29,6 +29,7 @@ import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.flags.FeatureFlags import com.android.systemui.media.nearby.NearbyMediaDevicesManager import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import java.util.Optional import javax.inject.Inject @@ -49,7 +50,8 @@ class MediaOutputBroadcastDialogFactory @Inject constructor( private val audioManager: AudioManager, private val powerExemptionManager: PowerExemptionManager, private val keyGuardManager: KeyguardManager, - private val featureFlags: FeatureFlags + private val featureFlags: FeatureFlags, + private val userTracker: UserTracker ) { var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null @@ -61,7 +63,7 @@ class MediaOutputBroadcastDialogFactory @Inject constructor( val controller = MediaOutputController(context, packageName, mediaSessionManager, lbm, starter, notifCollection, dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager, - powerExemptionManager, keyGuardManager, featureFlags) + powerExemptionManager, keyGuardManager, featureFlags, userTracker) val dialog = MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller) mediaOutputBroadcastDialog = dialog diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index 822644b8e573..cc75478ef506 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -49,6 +49,7 @@ import android.media.MediaRoute2Info; import android.media.NearbyDevice; import android.media.RoutingSessionInfo; import android.media.session.MediaController; +import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.IBinder; @@ -82,10 +83,10 @@ import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.monet.ColorScheme; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -130,8 +131,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, private final Object mMediaDevicesLock = new Object(); @VisibleForTesting final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); - @VisibleForTesting - final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>(); final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>(); private final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); private final AudioManager mAudioManager; @@ -165,6 +164,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, private float mInactiveRadius; private float mActiveRadius; private FeatureFlags mFeatureFlags; + private UserTracker mUserTracker; public enum BroadcastNotifyDialog { ACTION_FIRST_LAUNCH, @@ -181,7 +181,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, AudioManager audioManager, PowerExemptionManager powerExemptionManager, KeyguardManager keyGuardManager, - FeatureFlags featureFlags) { + FeatureFlags featureFlags, + UserTracker userTracker) { mContext = context; mPackageName = packageName; mMediaSessionManager = mediaSessionManager; @@ -192,6 +193,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mPowerExemptionManager = powerExemptionManager; mKeyGuardManager = keyGuardManager; mFeatureFlags = featureFlags; + mUserTracker = userTracker; InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); @@ -224,7 +226,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, void start(@NonNull Callback cb) { synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mMediaDevices.clear(); mMediaItemList.clear(); } mNearbyDeviceInfoMap.clear(); @@ -232,16 +233,13 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mNearbyMediaDevicesManager.registerNearbyDevicesCallback(this); } if (!TextUtils.isEmpty(mPackageName)) { - for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) { - if (TextUtils.equals(controller.getPackageName(), mPackageName)) { - mMediaController = controller; - mMediaController.unregisterCallback(mCb); - if (mMediaController.getPlaybackState() != null) { - mCurrentState = mMediaController.getPlaybackState().getState(); - } - mMediaController.registerCallback(mCb); - break; + mMediaController = getMediaController(); + if (mMediaController != null) { + mMediaController.unregisterCallback(mCb); + if (mMediaController.getPlaybackState() != null) { + mCurrentState = mMediaController.getPlaybackState().getState(); } + mMediaController.registerCallback(mCb); } } if (mMediaController == null) { @@ -275,7 +273,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mLocalMediaManager.stopScan(); synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mMediaDevices.clear(); mMediaItemList.clear(); } if (mNearbyMediaDevicesManager != null) { @@ -284,12 +281,31 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, mNearbyDeviceInfoMap.clear(); } + private MediaController getMediaController() { + for (NotificationEntry entry : mNotifCollection.getAllNotifs()) { + final Notification notification = entry.getSbn().getNotification(); + if (notification.isMediaNotification() + && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) { + MediaSession.Token token = notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token.class); + return new MediaController(mContext, token); + } + } + for (MediaController controller : mMediaSessionManager.getActiveSessionsForUser(null, + mUserTracker.getUserHandle())) { + if (TextUtils.equals(controller.getPackageName(), mPackageName)) { + return controller; + } + } + return null; + } + @Override public void onDeviceListUpdate(List<MediaDevice> devices) { - boolean isListEmpty = - isAdvancedLayoutSupported() ? mMediaItemList.isEmpty() : mMediaDevices.isEmpty(); + boolean isListEmpty = mMediaItemList.isEmpty(); if (isListEmpty || !mIsRefreshing) { - buildMediaDevices(devices); + buildMediaItems(devices); mCallback.onDeviceListChanged(); } else { synchronized (mMediaDevicesLock) { @@ -304,11 +320,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, public void onSelectedDeviceStateChanged(MediaDevice device, @LocalMediaManager.MediaDeviceState int state) { mCallback.onRouteChanged(); - if (isAdvancedLayoutSupported()) { - mMetricLogger.logOutputItemSuccess(device.toString(), new ArrayList<>(mMediaItemList)); - } else { - mMetricLogger.logOutputSuccess(device.toString(), new ArrayList<>(mMediaDevices)); - } + mMetricLogger.logOutputItemSuccess(device.toString(), new ArrayList<>(mMediaItemList)); } @Override @@ -319,11 +331,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, @Override public void onRequestFailed(int reason) { mCallback.onRouteChanged(); - if (isAdvancedLayoutSupported()) { - mMetricLogger.logOutputItemFailure(new ArrayList<>(mMediaItemList), reason); - } else { - mMetricLogger.logOutputFailure(new ArrayList<>(mMediaDevices), reason); - } + mMetricLogger.logOutputItemFailure(new ArrayList<>(mMediaItemList), reason); } /** @@ -342,7 +350,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } try { synchronized (mMediaDevicesLock) { - mMediaDevices.removeIf(MediaDevice::isMutingExpectedDevice); mMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); } mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice()); @@ -547,7 +554,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, void refreshDataSetIfNeeded() { if (mNeedRefresh) { - buildMediaDevices(mCachedMediaDevices); + buildMediaItems(mCachedMediaDevices); mCallback.onDeviceListChanged(); mNeedRefresh = false; } @@ -597,75 +604,9 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, return mItemMarginEndSelectable; } - private void buildMediaDevices(List<MediaDevice> devices) { - if (isAdvancedLayoutSupported()) { - buildMediaItems(devices); - } else { - buildDefaultMediaDevices(devices); - } - } - - private void buildDefaultMediaDevices(List<MediaDevice> devices) { - synchronized (mMediaDevicesLock) { - attachRangeInfo(devices); - Collections.sort(devices, Comparator.naturalOrder()); - // For the first time building list, to make sure the top device is the connected - // device. - if (mMediaDevices.isEmpty()) { - boolean needToHandleMutingExpectedDevice = - hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); - final MediaDevice connectedMediaDevice = - needToHandleMutingExpectedDevice ? null - : getCurrentConnectedMediaDevice(); - if (connectedMediaDevice == null) { - if (DEBUG) { - Log.d(TAG, "No connected media device or muting expected device exist."); - } - if (needToHandleMutingExpectedDevice) { - for (MediaDevice device : devices) { - if (device.isMutingExpectedDevice()) { - mMediaDevices.add(0, device); - } else { - mMediaDevices.add(device); - } - } - } else { - mMediaDevices.addAll(devices); - } - return; - } - for (MediaDevice device : devices) { - if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) { - mMediaDevices.add(0, device); - } else { - mMediaDevices.add(device); - } - } - return; - } - // To keep the same list order - final List<MediaDevice> targetMediaDevices = new ArrayList<>(); - for (MediaDevice originalDevice : mMediaDevices) { - for (MediaDevice newDevice : devices) { - if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) { - targetMediaDevices.add(newDevice); - break; - } - } - } - if (targetMediaDevices.size() != devices.size()) { - devices.removeAll(targetMediaDevices); - targetMediaDevices.addAll(devices); - } - mMediaDevices.clear(); - mMediaDevices.addAll(targetMediaDevices); - } - } - private void buildMediaItems(List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { - if (!isRouteProcessSupported() || (isRouteProcessSupported() - && !mLocalMediaManager.isPreferenceRouteListingExist())) { + if (!mLocalMediaManager.isPreferenceRouteListingExist()) { attachRangeInfo(devices); Collections.sort(devices, Comparator.naturalOrder()); } @@ -789,18 +730,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, && (currentConnectedMediaDevice.isHostForOngoingSession()); } - public boolean isAdvancedLayoutSupported() { - return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT); - } - - public boolean isRouteProcessSupported() { - return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING); - } - - public boolean isSubStatusSupported() { - return mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_DEVICE_STATUS); - } - List<MediaDevice> getGroupMediaDevices() { final List<MediaDevice> selectedDevices = getSelectedMediaDevice(); final List<MediaDevice> selectableDevices = getSelectableMediaDevice(); @@ -845,10 +774,6 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, }); } - Collection<MediaDevice> getMediaDevices() { - return mMediaDevices; - } - public List<MediaItem> getMediaItemList() { return mMediaItemList; } @@ -935,19 +860,11 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, boolean isAnyDeviceTransferring() { synchronized (mMediaDevicesLock) { - if (isAdvancedLayoutSupported()) { - for (MediaItem mediaItem : mMediaItemList) { - if (mediaItem.getMediaDevice().isPresent() - && mediaItem.getMediaDevice().get().getState() - == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { - return true; - } - } - } else { - for (MediaDevice device : mMediaDevices) { - if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { - return true; - } + for (MediaItem mediaItem : mMediaItemList) { + if (mediaItem.getMediaDevice().isPresent() + && mediaItem.getMediaDevice().get().getState() + == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { + return true; } } } @@ -1011,7 +928,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, MediaOutputController controller = new MediaOutputController(mContext, mPackageName, mMediaSessionManager, mLocalBluetoothManager, mActivityStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), - mAudioManager, mPowerExemptionManager, mKeyGuardManager, mFeatureFlags); + mAudioManager, mPowerExemptionManager, mKeyGuardManager, mFeatureFlags, + mUserTracker); MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true, broadcastSender, controller); dialog.show(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt index 7dbf876bb377..802488668f16 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -32,6 +32,7 @@ import com.android.systemui.media.nearby.NearbyMediaDevicesManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.flags.FeatureFlags +import com.android.systemui.settings.UserTracker import java.util.Optional import javax.inject.Inject @@ -51,7 +52,8 @@ class MediaOutputDialogFactory @Inject constructor( private val audioManager: AudioManager, private val powerExemptionManager: PowerExemptionManager, private val keyGuardManager: KeyguardManager, - private val featureFlags: FeatureFlags + private val featureFlags: FeatureFlags, + private val userTracker: UserTracker ) { companion object { private const val INTERACTION_JANK_TAG = "media_output" @@ -67,7 +69,7 @@ class MediaOutputDialogFactory @Inject constructor( context, packageName, mediaSessionManager, lbm, starter, notifCollection, dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager, - powerExemptionManager, keyGuardManager, featureFlags) + powerExemptionManager, keyGuardManager, featureFlags, userTracker) val dialog = MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller, uiEventLogger) mediaOutputDialog = dialog diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index edab56e8d9b9..1fd11bd61700 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -323,7 +323,6 @@ class BackPanelController internal constructor( if (isFlungAwayFromEdge(endX = event.x) || previousXTranslation > params.staticTriggerThreshold ) { - updateArrowState(GestureState.ACTIVE) updateArrowState(GestureState.FLUNG) } else { updateArrowState(GestureState.CANCELLED) @@ -331,8 +330,11 @@ class BackPanelController internal constructor( } GestureState.INACTIVE -> { if (isFlungAwayFromEdge(endX = event.x)) { + // This is called outside of updateArrowState so that + // BackAnimationController can immediately evaluate state + // instead of after the flung delay + backCallback.setTriggerBack(true) mainHandler.postDelayed(MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION) { - updateArrowState(GestureState.ACTIVE) updateArrowState(GestureState.FLUNG) } } else { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 42de7f0f3a8b..5818fd0634a2 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -63,6 +63,8 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.window.BackEvent; +import androidx.annotation.DimenRes; + import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.policy.GestureNavigationSettingsObserver; import com.android.systemui.R; @@ -225,7 +227,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // The slop to distinguish between horizontal and vertical motion private float mTouchSlop; // The threshold for back swipe full progress. - private float mBackSwipeProgressThreshold; + private float mBackSwipeLinearThreshold; + private float mNonLinearFactor; // Duration after which we consider the event as longpress. private final int mLongPressTimeout; private int mStartingQuickstepRotation = -1; @@ -471,11 +474,19 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack final float backGestureSlop = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.BACK_GESTURE_SLOP_MULTIPLIER, 0.75f); mTouchSlop = mViewConfiguration.getScaledTouchSlop() * backGestureSlop; - mBackSwipeProgressThreshold = res.getDimension( + mBackSwipeLinearThreshold = res.getDimension( R.dimen.navigation_edge_action_progress_threshold); + mNonLinearFactor = getDimenFloat(res, + R.dimen.back_progress_non_linear_factor); updateBackAnimationThresholds(); } + private float getDimenFloat(Resources res, @DimenRes int resId) { + TypedValue typedValue = new TypedValue(); + res.getValue(resId, typedValue, true); + return typedValue.getFloat(); + } + public void updateNavigationBarOverlayExcludeRegion(Rect exclude) { mNavBarOverlayExcludedBounds.set(exclude); } @@ -1116,8 +1127,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack if (mBackAnimation == null) { return; } - mBackAnimation.setSwipeThresholds( - Math.min(mDisplaySize.x, mBackSwipeProgressThreshold)); + int maxDistance = mDisplaySize.x; + float linearDistance = Math.min(maxDistance, mBackSwipeLinearThreshold); + mBackAnimation.setSwipeThresholds(linearDistance, maxDistance, mNonLinearFactor); } private boolean sendEvent(int action, int code) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt index 182ece7cd328..6d881d527ce4 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt @@ -132,7 +132,7 @@ data class EdgePanelParams(private var resources: Resources) { entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f) entryWidthTowardsEdgeInterpolator = PathInterpolator(1f, -3f, 1f, 1.2f) - activeWidthInterpolator = PathInterpolator(.56f, -0.39f, .18f, 1.46f) + activeWidthInterpolator = PathInterpolator(.7f, -0.24f, .48f, 1.21f) arrowAngleInterpolator = entryWidthInterpolator horizontalTranslationInterpolator = PathInterpolator(0.2f, 1.0f, 1.0f, 1.0f) verticalTranslationInterpolator = PathInterpolator(.5f, 1.15f, .41f, .94f) diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 9eb3d2d8b48e..25272ae097a1 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -250,6 +250,11 @@ constructor( * Widget Picker to all users. */ fun setNoteTaskShortcutEnabled(value: Boolean, user: UserHandle) { + if (!userManager.isUserUnlocked(user)) { + debugLog { "setNoteTaskShortcutEnabled call but user locked: user=$user" } + return + } + val componentName = ComponentName(context, CreateNoteTaskShortcutActivity::class.java) val enabledState = @@ -305,6 +310,10 @@ constructor( /** @see OnRoleHoldersChangedListener */ fun onRoleHoldersChanged(roleName: String, user: UserHandle) { if (roleName != ROLE_NOTES) return + if (!userManager.isUserUnlocked(user)) { + debugLog { "onRoleHoldersChanged call but user locked: role=$roleName, user=$user" } + return + } if (user == userTracker.userHandle) { updateNoteTaskAsUser(user) diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index 7bb615b8d866..221ff65e4dfe 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -18,8 +18,13 @@ package com.android.systemui.notetask import android.app.role.RoleManager import android.os.UserHandle import android.view.KeyEvent -import androidx.annotation.VisibleForTesting +import android.view.KeyEvent.KEYCODE_N +import android.view.KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.notetask.NoteTaskEntryPoint.KEYBOARD_SHORTCUT +import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.CommandQueue import com.android.wm.shell.bubbles.Bubbles @@ -35,35 +40,82 @@ constructor( private val roleManager: RoleManager, private val commandQueue: CommandQueue, private val optionalBubbles: Optional<Bubbles>, + private val userTracker: UserTracker, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @Background private val backgroundExecutor: Executor, @NoteTaskEnabledKey private val isEnabled: Boolean, - private val userTracker: UserTracker, ) { - @VisibleForTesting - val callbacks = - object : CommandQueue.Callbacks { - override fun handleSystemKey(key: KeyEvent) { - if (key.keyCode == KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL) { - controller.showNoteTask(NoteTaskEntryPoint.TAIL_BUTTON) - } else if ( - key.keyCode == KeyEvent.KEYCODE_N && key.isMetaPressed && key.isCtrlPressed - ) { - controller.showNoteTask(NoteTaskEntryPoint.KEYBOARD_SHORTCUT) - } - } - } - + /** Initializes note task related features and glue it with other parts of the SystemUI. */ fun initialize() { // Guard against feature not being enabled or mandatory dependencies aren't available. if (!isEnabled || optionalBubbles.isEmpty) return - controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle) + initializeHandleSystemKey() + initializeOnRoleHoldersChanged() + initializeOnUserUnlocked() + } + + /** + * Initializes a callback for [CommandQueue] which will redirect [KeyEvent] from a Stylus to + * [NoteTaskController], ensure custom actions can be triggered (i.e., keyboard shortcut). + */ + private fun initializeHandleSystemKey() { + val callbacks = + object : CommandQueue.Callbacks { + override fun handleSystemKey(key: KeyEvent) { + key.toNoteTaskEntryPointOrNull()?.let(controller::showNoteTask) + } + } commandQueue.addCallback(callbacks) + } + + /** + * Initializes the [RoleManager] role holder changed listener to ensure [NoteTaskController] + * will always update whenever the role holder app changes. Keep in mind that a role may change + * by direct user interaction (i.e., user goes to settings and change it) or by indirect + * interaction (i.e., the current role holder app is uninstalled). + */ + private fun initializeOnRoleHoldersChanged() { roleManager.addOnRoleHoldersChangedListenerAsUser( backgroundExecutor, controller::onRoleHoldersChanged, UserHandle.ALL, ) } + + /** + * Initializes a [KeyguardUpdateMonitor] listener that will ensure [NoteTaskController] is in + * correct state during system initialization (after a direct boot user unlocked event). + * + * Once the system is unlocked, we will force trigger [NoteTaskController.onRoleHoldersChanged] + * with a hardcoded [RoleManager.ROLE_NOTES] for the current user. + */ + private fun initializeOnUserUnlocked() { + if (keyguardUpdateMonitor.isUserUnlocked(userTracker.userId)) { + controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle) + } else { + keyguardUpdateMonitor.registerCallback(onUserUnlockedCallback) + } + } + + // KeyguardUpdateMonitor.registerCallback uses a weak reference, so we need a hard reference. + private val onUserUnlockedCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onUserUnlocked() { + controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle) + keyguardUpdateMonitor.removeCallback(this) + } + } } + +/** + * Maps a [KeyEvent] to a [NoteTaskEntryPoint]. If the [KeyEvent] does not represent a + * [NoteTaskEntryPoint], returns null. + */ +private fun KeyEvent.toNoteTaskEntryPointOrNull(): NoteTaskEntryPoint? = + when { + keyCode == KEYCODE_STYLUS_BUTTON_TAIL -> TAIL_BUTTON + keyCode == KEYCODE_N && isMetaPressed && isCtrlPressed -> KEYBOARD_SHORTCUT + else -> null + } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index 46724adee848..d8899796b336 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -24,6 +24,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.LinearLayout; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.FontSizeUtils; import com.android.systemui.R; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.SignalState; @@ -198,13 +199,23 @@ public class QuickQSPanel extends QSPanel { @Override public boolean updateResources() { - mCellHeightResId = R.dimen.qs_quick_tile_size; + mResourceCellHeightResId = R.dimen.qs_quick_tile_size; boolean b = super.updateResources(); mMaxAllowedRows = getResources().getInteger(R.integer.quick_qs_panel_max_rows); return b; } @Override + protected void estimateCellHeight() { + FontSizeUtils.updateFontSize(mTempTextView, R.dimen.qs_tile_text_size); + int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + mTempTextView.measure(unspecifiedSpec, unspecifiedSpec); + int padding = mContext.getResources().getDimensionPixelSize(R.dimen.qs_tile_padding); + // the QQS only have 1 label + mEstimatedCellHeight = mTempTextView.getMeasuredHeight() + padding * 2; + } + + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateResources(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java index 269a158c6b87..19bf0188c9d2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java @@ -9,10 +9,12 @@ import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.TextView; import androidx.annotation.Nullable; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.FontSizeUtils; import com.android.systemui.R; import com.android.systemui.qs.QSPanel.QSTileLayout; import com.android.systemui.qs.QSPanelControllerBase.TileRecord; @@ -29,9 +31,10 @@ public class TileLayout extends ViewGroup implements QSTileLayout { protected int mColumns; protected int mCellWidth; - protected int mCellHeightResId = R.dimen.qs_tile_height; + protected int mResourceCellHeightResId = R.dimen.qs_tile_height; + protected int mResourceCellHeight; + protected int mEstimatedCellHeight; protected int mCellHeight; - protected int mMaxCellHeight; protected int mCellMarginHorizontal; protected int mCellMarginVertical; protected int mSidePadding; @@ -49,6 +52,8 @@ public class TileLayout extends ViewGroup implements QSTileLayout { private float mSquishinessFraction = 1f; protected int mLastTileBottom; + protected TextView mTempTextView; + public TileLayout(Context context) { this(context, null); } @@ -57,6 +62,7 @@ public class TileLayout extends ViewGroup implements QSTileLayout { super(context, attrs); mLessRows = ((Settings.System.getInt(context.getContentResolver(), "qs_less_rows", 0) != 0) || useQsMediaPlayer(context)); + mTempTextView = new TextView(context); updateResources(); } @@ -120,14 +126,19 @@ public class TileLayout extends ViewGroup implements QSTileLayout { } public boolean updateResources() { - final Resources res = mContext.getResources(); + Resources res = getResources(); mResourceColumns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns)); - mMaxCellHeight = mContext.getResources().getDimensionPixelSize(mCellHeightResId); + mResourceCellHeight = res.getDimensionPixelSize(mResourceCellHeightResId); mCellMarginHorizontal = res.getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal); mSidePadding = useSidePadding() ? mCellMarginHorizontal / 2 : 0; mCellMarginVertical= res.getDimensionPixelSize(R.dimen.qs_tile_margin_vertical); mMaxAllowedRows = Math.max(1, getResources().getInteger(R.integer.quick_settings_max_rows)); - if (mLessRows) mMaxAllowedRows = Math.max(mMinRows, mMaxAllowedRows - 1); + if (mLessRows) { + mMaxAllowedRows = Math.max(mMinRows, mMaxAllowedRows - 1); + } + // update estimated cell height under current font scaling + mTempTextView.dispatchConfigurationChanged(mContext.getResources().getConfiguration()); + estimateCellHeight(); if (updateColumns()) { requestLayout(); return true; @@ -211,8 +222,23 @@ public class TileLayout extends ViewGroup implements QSTileLayout { return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); } + // Estimate the height for the tile with 2 labels (general case) under current font scaling. + protected void estimateCellHeight() { + FontSizeUtils.updateFontSize(mTempTextView, R.dimen.qs_tile_text_size); + int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + mTempTextView.measure(unspecifiedSpec, unspecifiedSpec); + int padding = mContext.getResources().getDimensionPixelSize(R.dimen.qs_tile_padding); + mEstimatedCellHeight = mTempTextView.getMeasuredHeight() * 2 + padding * 2; + } + protected int getCellHeight() { - return mMaxCellHeight; + // Compare estimated height with resource height and return the larger one. + // If estimated height > resource height, it means the resource height is not enough + // for the tile content under current font scaling. Therefore, we need to use the estimated + // height to have a full tile content view. + // If estimated height <= resource height, we can use the resource height for tile to keep + // the same UI as original behavior. + return Math.max(mResourceCellHeight, mEstimatedCellHeight); } private void layoutTileRecords(int numRecords, boolean forLayout) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index f7e736698e26..abeb5af352fc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -153,14 +153,16 @@ public class InternetDialogController implements AccessPointController.AccessPoi @VisibleForTesting /** Should be accessible only to the main thread. */ final Map<Integer, TelephonyDisplayInfo> mSubIdTelephonyDisplayInfoMap = new HashMap<>(); + @VisibleForTesting + /** Should be accessible only to the main thread. */ + final Map<Integer, TelephonyManager> mSubIdTelephonyManagerMap = new HashMap<>(); + @VisibleForTesting + /** Should be accessible only to the main thread. */ + final Map<Integer, TelephonyCallback> mSubIdTelephonyCallbackMap = new HashMap<>(); private WifiManager mWifiManager; private Context mContext; private SubscriptionManager mSubscriptionManager; - /** Should be accessible only to the main thread. */ - private Map<Integer, TelephonyManager> mSubIdTelephonyManagerMap = new HashMap<>(); - /** Should be accessible only to the main thread. */ - private Map<Integer, TelephonyCallback> mSubIdTelephonyCallbackMap = new HashMap<>(); private TelephonyManager mTelephonyManager; private ConnectivityManager mConnectivityManager; private CarrierConfigTracker mCarrierConfigTracker; @@ -320,6 +322,9 @@ public class InternetDialogController implements AccessPointController.AccessPoi Log.e(TAG, "Unexpected null telephony call back for Sub " + tm.getSubscriptionId()); } } + mSubIdTelephonyManagerMap.clear(); + mSubIdTelephonyCallbackMap.clear(); + mSubIdTelephonyDisplayInfoMap.clear(); mSubscriptionManager.removeOnSubscriptionsChangedListener( mOnSubscriptionsChangedListener); mAccessPointController.removeAccessPointCallback(this); diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt new file mode 100644 index 000000000000..36dec1d112b9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.viewmodel + +import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Models UI state and handles user input for the quick settings scene. */ +class QuickSettingsSceneViewModel +@AssistedInject +constructor( + lockscreenSceneInteractorFactory: LockscreenSceneInteractor.Factory, + @Assisted containerName: String, +) { + private val lockscreenSceneInteractor: LockscreenSceneInteractor = + lockscreenSceneInteractorFactory.create(containerName) + + /** Notifies that some content in quick settings was clicked. */ + fun onContentClicked() { + lockscreenSceneInteractor.dismissLockscreen() + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): QuickSettingsSceneViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java index dc3c8203d1a2..6912114140b0 100644 --- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java @@ -16,12 +16,16 @@ package com.android.systemui.reardisplay; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.TestApi; import android.content.Context; +import android.content.res.Configuration; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManagerGlobal; import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.LinearLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.CoreStartable; @@ -70,6 +74,7 @@ public class RearDisplayDialogController implements CoreStartable, CommandQueue. @VisibleForTesting SystemUIDialog mRearDisplayEducationDialog; + @Nullable LinearLayout mDialogViewContainer; @Inject public RearDisplayDialogController(Context context, CommandQueue commandQueue, @@ -90,26 +95,51 @@ public class RearDisplayDialogController implements CoreStartable, CommandQueue. createAndShowDialog(); } + @Override + public void onConfigurationChanged(Configuration newConfig) { + if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing() + && mDialogViewContainer != null) { + // Refresh the dialog view when configuration is changed. + Context dialogContext = mRearDisplayEducationDialog.getContext(); + View dialogView = createDialogView(dialogContext); + mDialogViewContainer.removeAllViews(); + mDialogViewContainer.addView(dialogView); + } + } + private void createAndShowDialog() { mServiceNotified = false; Context dialogContext = mRearDisplayEducationDialog.getContext(); + View dialogView = createDialogView(dialogContext); + + mDialogViewContainer = new LinearLayout(dialogContext); + mDialogViewContainer.setLayoutParams( + new LinearLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + mDialogViewContainer.setOrientation(LinearLayout.VERTICAL); + mDialogViewContainer.addView(dialogView); + + mRearDisplayEducationDialog.setView(mDialogViewContainer); + + configureDialogButtons(); + + mRearDisplayEducationDialog.show(); + } + + private View createDialogView(Context context) { View dialogView; if (mStartedFolded) { - dialogView = View.inflate(dialogContext, + dialogView = View.inflate(context, R.layout.activity_rear_display_education, null); } else { - dialogView = View.inflate(dialogContext, + dialogView = View.inflate(context, R.layout.activity_rear_display_education_opened, null); } LottieAnimationView animationView = dialogView.findViewById( R.id.rear_display_folded_animation); animationView.setRepeatCount(mAnimationRepeatCount); - mRearDisplayEducationDialog.setView(dialogView); - - configureDialogButtons(); - - mRearDisplayEducationDialog.show(); + return dialogView; } /** @@ -164,6 +194,7 @@ public class RearDisplayDialogController implements CoreStartable, CommandQueue. mServiceNotified = true; mDeviceStateManagerGlobal.unregisterDeviceStateCallback(mDeviceStateManagerCallback); mDeviceStateManagerGlobal.onStateRequestOverlayDismissed(shouldCancelRequest); + mDialogViewContainer = null; } /** diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt new file mode 100644 index 000000000000..0ed8b21c100e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -0,0 +1,28 @@ +/* + * 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.scene + +import com.android.systemui.scene.shared.page.SceneModule +import dagger.Module + +@Module( + includes = + [ + SceneModule::class, + ], +) +object SceneContainerFrameworkModule diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt index 9ef439d72118..e7811e39aa6f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt @@ -32,8 +32,8 @@ sealed class SceneKey( */ object Gone : SceneKey("gone") - /** The lock screen is the scene that shows when the device is locked. */ - object LockScreen : SceneKey("lockscreen") + /** The lockscreen is the scene that shows when the device is locked. */ + object Lockscreen : SceneKey("lockscreen") /** * The shade is the scene whose primary purpose is to show a scrollable list of notifications. diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index 8c01bae43c3d..5ad16f0a6078 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -293,10 +293,9 @@ public class ImageExporter { final ContentValues values = createMetadata(time, format, fileName); Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - if (UserHandle.myUserId() != owner.getIdentifier()) { - baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier()); - } - Uri uri = resolver.insert(baseUri, values); + Uri uriWithUserId = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier()); + + Uri uri = resolver.insert(uriWithUserId, values); if (uri == null) { throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java index 72613249552a..2f96f6c656c4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java @@ -247,7 +247,7 @@ public class AppClipsActivity extends ComponentActivity { } updateImageDimensions(); - mViewModel.saveScreenshotThenFinish(drawable, bounds); + mViewModel.saveScreenshotThenFinish(drawable, bounds, getUser()); } private void setResultThenFinish(Uri uri) { @@ -255,6 +255,11 @@ public class AppClipsActivity extends ComponentActivity { return; } + // Grant permission here instead of in the trampoline activity because this activity can run + // as work profile user so the URI can belong to the work profile user while the trampoline + // activity always runs as main user. + grantUriPermission(mCallingPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + Bundle data = new Bundle(); data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java index e1619dc9b6ee..afc8bff91766 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java @@ -19,7 +19,7 @@ package com.android.systemui.screenshot.appclips; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.view.Display; +import android.os.UserManager; import androidx.annotation.Nullable; @@ -27,6 +27,7 @@ import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.ServiceConnector; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.settings.DisplayTracker; import javax.inject.Inject; @@ -35,14 +36,20 @@ import javax.inject.Inject; class AppClipsCrossProcessHelper { private final ServiceConnector<IAppClipsScreenshotHelperService> mProxyConnector; + private final DisplayTracker mDisplayTracker; @Inject - AppClipsCrossProcessHelper(@Application Context context) { - mProxyConnector = new ServiceConnector.Impl<IAppClipsScreenshotHelperService>(context, + AppClipsCrossProcessHelper(@Application Context context, UserManager userManager, + DisplayTracker displayTracker) { + // Start a service as main user so that even if the app clips activity is running as work + // profile user the service is able to use correct instance of Bubbles to grab a screenshot + // excluding the bubble layer. + mProxyConnector = new ServiceConnector.Impl<>(context, new Intent(context, AppClipsScreenshotHelperService.class), Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY - | Context.BIND_NOT_VISIBLE, context.getUserId(), + | Context.BIND_NOT_VISIBLE, userManager.getMainUser().getIdentifier(), IAppClipsScreenshotHelperService.Stub::asInterface); + mDisplayTracker = displayTracker; } /** @@ -56,7 +63,9 @@ class AppClipsCrossProcessHelper { try { AndroidFuture<ScreenshotHardwareBufferInternal> future = mProxyConnector.postForResult( - service -> service.takeScreenshot(Display.DEFAULT_DISPLAY)); + service -> + // Take a screenshot of the default display of the user. + service.takeScreenshot(mDisplayTracker.getDefaultDisplayId())); return future.get().createBitmapThenCloseBuffer(); } catch (Exception e) { return null; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java index 0487cbc995dd..f00803c6d64b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java @@ -21,7 +21,6 @@ import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; -import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; @@ -34,6 +33,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserInfo; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; @@ -82,6 +82,8 @@ public class AppClipsTrampolineActivity extends Activity { private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName(); static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI"; + @VisibleForTesting + static final String EXTRA_USE_WP_USER = TAG + "USE_WP_USER"; static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE"; static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER"; static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME"; @@ -98,6 +100,7 @@ public class AppClipsTrampolineActivity extends Activity { private final ResultReceiver mResultReceiver; private Intent mKillAppClipsBroadcastIntent; + private UserHandle mNotesAppUser; @Inject public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags, @@ -165,15 +168,21 @@ public class AppClipsTrampolineActivity extends Activity { return; } + mNotesAppUser = getUser(); + if (getIntent().getBooleanExtra(EXTRA_USE_WP_USER, /* defaultValue= */ false)) { + // Get the work profile user internally instead of passing around via intent extras as + // this activity is exported apps could potentially mess around with intent extras. + mNotesAppUser = getWorkProfileUser().orElse(mNotesAppUser); + } + String callingPackageName = getCallingPackage(); Intent intent = new Intent().setComponent(componentName) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver) .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName); - try { - // Start the App Clips activity. - startActivity(intent); + // Start the App Clips activity for the user corresponding to the notes app user. + startActivityAsUser(intent, mNotesAppUser); // Set up the broadcast intent that will inform the above App Clips activity to finish // when this trampoline activity is finished. @@ -198,6 +207,13 @@ public class AppClipsTrampolineActivity extends Activity { } } + private Optional<UserHandle> getWorkProfileUser() { + return mUserTracker.getUserProfiles().stream() + .filter(profile -> mUserManager.isManagedProfile(profile.id)) + .findFirst() + .map(UserInfo::getUserHandle); + } + private void maybeStartActivityForWPUser() { UserHandle mainUser = mUserManager.getMainUser(); if (mainUser == null) { @@ -205,9 +221,13 @@ public class AppClipsTrampolineActivity extends Activity { return; } - // Start the activity as the main user with activity result forwarding. + // Start the activity as the main user with activity result forwarding. Set the intent extra + // so that the newly started trampoline activity starts the actual app clips activity as the + // work profile user. Starting the app clips activity as the work profile user is required + // to save the screenshot in work profile user storage and grant read permission to the URI. startActivityAsUser( new Intent(this, AppClipsTrampolineActivity.class) + .putExtra(EXTRA_USE_WP_USER, /* value= */ true) .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT), mainUser); } @@ -221,7 +241,7 @@ public class AppClipsTrampolineActivity extends Activity { int callingPackageUid = 0; try { callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName, - APPLICATION_INFO_FLAGS, mUserTracker.getUserId()).uid; + APPLICATION_INFO_FLAGS, mNotesAppUser.getIdentifier()).uid; } catch (NameNotFoundException e) { Log.d(TAG, "Couldn't find notes app UID " + e); } @@ -254,14 +274,14 @@ public class AppClipsTrampolineActivity extends Activity { if (statusCode == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) { Uri uri = resultData.getParcelable(EXTRA_SCREENSHOT_URI, Uri.class); - convertedData.setData(uri).addFlags(FLAG_GRANT_READ_URI_PERMISSION); + convertedData.setData(uri); } // Broadcast no longer required, setting it to null. mKillAppClipsBroadcastIntent = null; // Expand the note bubble before returning the result. - mNoteTaskController.showNoteTask(NoteTaskEntryPoint.APP_CLIPS); + mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mNotesAppUser); setResult(RESULT_OK, convertedData); finish(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java index 4cbca28a4032..b0e4cc978ae2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -26,7 +26,7 @@ import android.graphics.Rect; import android.graphics.RenderNode; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Process; +import android.os.UserHandle; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; @@ -110,16 +110,14 @@ final class AppClipsViewModel extends ViewModel { * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to * {@link LiveData}. */ - void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds) { + void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds, UserHandle user) { mBgExecutor.execute(() -> { // Render the screenshot bitmap in background. Bitmap screenshotBitmap = renderBitmap(screenshotDrawable, bounds); // Export and save the screenshot in background. - // TODO(b/267310185): Save to work profile UserHandle. ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export( - mBgExecutor, UUID.randomUUID(), screenshotBitmap, - Process.myUserHandle()); + mBgExecutor, UUID.randomUUID(), screenshotBitmap, user); // Get the result and update state on main thread. exportFuture.addListener(() -> { diff --git a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java index 233667335b72..9235fcc8202f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java @@ -84,7 +84,7 @@ public class DebugDrawable extends Drawable { Color.YELLOW, "calculatePanelHeightShade()"); drawDebugInfo(canvas, (int) mQsController.calculateNotificationsTopPadding( - mNotificationPanelViewController.isExpanding(), + mNotificationPanelViewController.isExpandingOrCollapsing(), mNotificationPanelViewController.getKeyguardNotificationStaticPadding(), mNotificationPanelViewController.getExpandedFraction()), Color.MAGENTA, "calculateNotificationsTopPadding()"); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index af12bc2ca9f8..0fdd7ca1564f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -233,7 +233,6 @@ import javax.inject.Inject; import javax.inject.Provider; import kotlin.Unit; - import kotlinx.coroutines.CoroutineDispatcher; @CentralSurfacesComponent.CentralSurfacesScope @@ -415,7 +414,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final KeyguardClockPositionAlgorithm.Result mClockPositionResult = new KeyguardClockPositionAlgorithm.Result(); - private boolean mIsExpanding; + /** + * Indicates shade (or just QS) is expanding or collapsing but doesn't fully cover KEYGUARD + * state when shade can be expanded with swipe down or swipe down from the top to full QS. + */ + private boolean mIsExpandingOrCollapsing; /** * Indicates drag starting height when swiping down or up on heads-up notifications. @@ -1862,7 +1865,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void expandToNotifications() { - if (mSplitShadeEnabled && (isShadeFullyExpanded() || isExpanding())) { + if (mSplitShadeEnabled && (isShadeFullyExpanded() || isExpandingOrCollapsing())) { return; } if (mQsController.getExpanded()) { @@ -2109,6 +2112,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump ? QUICK_SETTINGS : ( mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK); if (!isFalseTouch(x, y, interactionType)) { + mShadeLog.logFlingExpands(vel, vectorVel, interactionType, + this.mFlingAnimationUtils.getMinVelocityPxPerSecond(), + mExpandedFraction > 0.5f, mAllowExpandForSmallExpansion); if (Math.abs(vectorVel) < this.mFlingAnimationUtils.getMinVelocityPxPerSecond()) { expands = shouldExpandWhenNotFlinging(); } else { @@ -2269,7 +2275,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump void requestScrollerTopPaddingUpdate(boolean animate) { mNotificationStackScrollLayoutController.updateTopPadding( - mQsController.calculateNotificationsTopPadding(mIsExpanding, + mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing, getKeyguardNotificationStaticPadding(), mExpandedFraction), animate); if (isKeyguardShowing() && mKeyguardBypassController.getBypassEnabled()) { @@ -2322,7 +2328,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } int maxHeight; if (mQsController.isExpandImmediate() || mQsController.getExpanded() - || mIsExpanding && mQsController.getExpandedWhenExpandingStarted() + || mIsExpandingOrCollapsing && mQsController.getExpandedWhenExpandingStarted() || mPulsing || mSplitShadeEnabled) { maxHeight = mQsController.calculatePanelHeightExpanded( mClockPositionResult.stackScrollerPadding); @@ -2342,8 +2348,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return maxHeight; } - public boolean isExpanding() { - return mIsExpanding; + @Override + public boolean isExpandingOrCollapsing() { + float lockscreenExpansionProgress = mQsController.getLockscreenShadeDragProgress(); + return mIsExpandingOrCollapsing + || (0 < lockscreenExpansionProgress && lockscreenExpansionProgress < 1); } private void onHeightUpdated(float expandedHeight) { @@ -2355,7 +2364,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); } if (!mQsController.getExpanded() || mQsController.isExpandImmediate() - || mIsExpanding && mQsController.getExpandedWhenExpandingStarted()) { + || mIsExpandingOrCollapsing && mQsController.getExpandedWhenExpandingStarted()) { // Updating the clock position will set the top padding which might // trigger a new panel height and re-position the clock. // This is a circular dependency and should be avoided, otherwise we'll have @@ -2493,7 +2502,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mNotificationStackScrollLayoutController.onExpansionStopped(); mHeadsUpManager.onExpandingFinished(); mConversationNotificationManager.onNotificationPanelExpandStateChanged(isFullyCollapsed()); - mIsExpanding = false; + mIsExpandingOrCollapsing = false; mMediaHierarchyManager.setCollapsingShadeFromQS(false); mMediaHierarchyManager.setQsExpanded(mQsController.getExpanded()); if (isFullyCollapsed()) { @@ -2908,6 +2917,10 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump && mBarState == StatusBarState.SHADE; } + private boolean isPanelVisibleBecauseScrimIsAnimatingOff() { + return mUnlockedScreenOffAnimationController.isAnimationPlaying(); + } + @Override public boolean shouldHideStatusBarIconsWhenExpanded() { if (mIsLaunchAnimationRunning) { @@ -3199,7 +3212,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump ipw.print("mDisplayTopInset="); ipw.println(mDisplayTopInset); ipw.print("mDisplayRightInset="); ipw.println(mDisplayRightInset); ipw.print("mDisplayLeftInset="); ipw.println(mDisplayLeftInset); - ipw.print("mIsExpanding="); ipw.println(mIsExpanding); + ipw.print("mIsExpandingOrCollapsing="); ipw.println(mIsExpandingOrCollapsing); ipw.print("mHeadsUpStartHeight="); ipw.println(mHeadsUpStartHeight); ipw.print("mListenForHeadsUp="); ipw.println(mListenForHeadsUp); ipw.print("mNavigationBarBottomHeight="); ipw.println(mNavigationBarBottomHeight); @@ -3431,7 +3444,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump void notifyExpandingStarted() { if (!mExpanding) { mExpanding = true; - mIsExpanding = true; + mIsExpandingOrCollapsing = true; mQsController.onExpandingStarted(mQsController.getFullyExpanded()); } } @@ -3492,7 +3505,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump * gesture), we always play haptic. */ private void maybeVibrateOnOpening(boolean openingWithTouch) { - if (mVibrateOnOpening) { + if (mVibrateOnOpening && mBarState != KEYGUARD && mBarState != SHADE_LOCKED) { if (!openingWithTouch || !mHasVibratedOnOpen) { mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); mHasVibratedOnOpen = true; @@ -3792,7 +3805,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } else if (mBarState == SHADE_LOCKED) { return true; } else { - // case of two finger swipe from the top of keyguard + // case of swipe from the top of keyguard to expanded QS return mQsController.computeExpansionFraction() == 1; } } @@ -3967,6 +3980,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump || isPanelVisibleBecauseOfHeadsUp() || mTracking || mHeightAnimator != null + || isPanelVisibleBecauseScrimIsAnimatingOff() && !mIsSpringBackAnimation; } @@ -4044,7 +4058,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump * shade QS are always expanded */ private void closeQsIfPossible() { - boolean openOrOpening = isShadeFullyExpanded() || isExpanding(); + boolean openOrOpening = isShadeFullyExpanded() || isExpandingOrCollapsing(); if (!(mSplitShadeEnabled && openOrOpening)) { mQsController.closeQs(); } @@ -4767,7 +4781,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // If pulse is expanding already, let's give it the touch. There are situations // where the panel starts expanding even though we're also pulsing - boolean pulseShouldGetTouch = (!mIsExpanding + boolean pulseShouldGetTouch = (!mIsExpandingOrCollapsing && !mQsController.shouldQuickSettingsIntercept(mDownX, mDownY, 0)) || mPulseExpansionHandler.isExpanding(); if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) { @@ -4933,7 +4947,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mShadeLog.logHasVibrated(mHasVibratedOnOpen, mExpandedFraction); } addMovement(event); - if (!isFullyCollapsed() && !isOnKeyguard()) { + if (!isFullyCollapsed()) { maybeVibrateOnOpening(true /* openingWithTouch */); } float h = y - mInitialExpandY; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 46f12105e032..0c800d456f3c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -333,7 +333,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mLpChanged.preferredMinDisplayRefreshRate = 0; } Trace.setCounter("display_set_preferred_refresh_rate", - (long) mKeyguardPreferredRefreshRate); + (long) mLpChanged.preferredMaxDisplayRefreshRate); } else if (mKeyguardMaxRefreshRate > 0) { boolean bypassOnKeyguard = mKeyguardBypassController.getBypassEnabled() && state.statusBarState == StatusBarState.KEYGUARD diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index cb3fa15655b2..2f7644eb5c82 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -397,19 +397,15 @@ public class NotificationShadeWindowViewController { return true; } - if (handled) { - return true; - } - if (mMultiShadeMotionEventInteractor != null) { // This interactor is not null only if the dual shade feature is enabled. return mMultiShadeMotionEventInteractor.onTouchEvent(ev, mView.getWidth()); } else if (mDragDownHelper.isDragDownEnabled() || mDragDownHelper.isDraggingDown()) { // we still want to finish our drag down gesture when locking the screen - return mDragDownHelper.onTouchEvent(ev); + return mDragDownHelper.onTouchEvent(ev) || handled; } else { - return false; + return handled; } } @@ -480,7 +476,9 @@ public class NotificationShadeWindowViewController { setDragDownHelper(mLockscreenShadeTransitionController.getTouchHelper()); mDepthController.setRoot(mView); - mShadeExpansionStateManager.addExpansionListener(mDepthController); + ShadeExpansionChangeEvent currentState = + mShadeExpansionStateManager.addExpansionListener(mDepthController); + mDepthController.onPanelExpansionChanged(currentState); } public NotificationShadeWindowView getView() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 31b361f83758..81fe3ca82257 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -218,7 +218,7 @@ class NotificationsQSContainerController @Inject constructor( containerPadding = 0 stackScrollMargin = bottomStableInsets + notificationsBottomMargin } - val qsContainerPadding = if (!(isQSCustomizing || isQSDetailShowing)) { + val qsContainerPadding = if (!isQSDetailShowing) { // We also want this padding in the bottom in these cases if (splitShadeEnabled) { stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java index abdd1a94d93c..8672260790c3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java @@ -28,6 +28,7 @@ import static com.android.systemui.shade.NotificationPanelViewController.FLING_H import static com.android.systemui.shade.NotificationPanelViewController.QS_PARALLAX_AMOUNT; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; +import static com.android.systemui.util.DumpUtilsKt.asIndenting; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -37,6 +38,7 @@ import android.content.res.Resources; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; import android.view.MotionEvent; @@ -50,6 +52,8 @@ import android.view.WindowMetrics; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; +import androidx.annotation.NonNull; + import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; @@ -59,9 +63,11 @@ import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.SystemBarUtils; import com.android.keyguard.FaceAuthApiRequestReason; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; @@ -97,13 +103,15 @@ import com.android.systemui.util.LargeScreenUtils; import dagger.Lazy; +import java.io.PrintWriter; + import javax.inject.Inject; /** Handles QuickSettings touch handling, expansion and animation state * TODO (b/264460656) make this dumpable */ @CentralSurfacesComponent.CentralSurfacesScope -public class QuickSettingsController { +public class QuickSettingsController implements Dumpable { public static final String TAG = "QuickSettingsController"; private QS mQs; @@ -328,6 +336,7 @@ public class QuickSettingsController { FeatureFlags featureFlags, InteractionJankMonitor interactionJankMonitor, ShadeLogger shadeLog, + DumpManager dumpManager, KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, ShadeRepository shadeRepository, CastController castController @@ -377,6 +386,7 @@ public class QuickSettingsController { mShadeRepository = shadeRepository; mLockscreenShadeTransitionController.addCallback(new LockscreenShadeTransitionCallback()); + dumpManager.registerDumpable(this); } @VisibleForTesting @@ -1903,6 +1913,7 @@ public class QuickSettingsController { if (mSplitShadeEnabled) { // TODO:(b/269742565) remove below log Log.wtfStack(TAG, "FLING_COLLAPSE called in split shade"); } + setExpandImmediate(false); target = getMinExpansionHeight(); break; case FLING_HIDE: @@ -2014,6 +2025,143 @@ public class QuickSettingsController { (int) ((y - getInitialTouchY()) / displayDensity), (int) (vel / displayDensity)); } + @Override + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + pw.println(TAG + ":"); + IndentingPrintWriter ipw = asIndenting(pw); + ipw.increaseIndent(); + ipw.print("mIsFullWidth="); + ipw.println(mIsFullWidth); + ipw.print("mTouchSlop="); + ipw.println(mTouchSlop); + ipw.print("mSlopMultiplier="); + ipw.println(mSlopMultiplier); + ipw.print("mBarState="); + ipw.println(mBarState); + ipw.print("mStatusBarMinHeight="); + ipw.println(mStatusBarMinHeight); + ipw.print("mScrimEnabled="); + ipw.println(mScrimEnabled); + ipw.print("mScrimCornerRadius="); + ipw.println(mScrimCornerRadius); + ipw.print("mScreenCornerRadius="); + ipw.println(mScreenCornerRadius); + ipw.print("mUseLargeScreenShadeHeader="); + ipw.println(mUseLargeScreenShadeHeader); + ipw.print("mLargeScreenShadeHeaderHeight="); + ipw.println(mLargeScreenShadeHeaderHeight); + ipw.print("mDisplayRightInset="); + ipw.println(mDisplayRightInset); + ipw.print("mDisplayLeftInset="); + ipw.println(mDisplayLeftInset); + ipw.print("mSplitShadeEnabled="); + ipw.println(mSplitShadeEnabled); + ipw.print("mLockscreenNotificationPadding="); + ipw.println(mLockscreenNotificationPadding); + ipw.print("mSplitShadeNotificationsScrimMarginBottom="); + ipw.println(mSplitShadeNotificationsScrimMarginBottom); + ipw.print("mDozing="); + ipw.println(mDozing); + ipw.print("mEnableClipping="); + ipw.println(mEnableClipping); + ipw.print("mFalsingThreshold="); + ipw.println(mFalsingThreshold); + ipw.print("mTransitionToFullShadePosition="); + ipw.println(mTransitionToFullShadePosition); + ipw.print("mCollapsedOnDown="); + ipw.println(mCollapsedOnDown); + ipw.print("mShadeExpandedHeight="); + ipw.println(mShadeExpandedHeight); + ipw.print("mLastShadeFlingWasExpanding="); + ipw.println(mLastShadeFlingWasExpanding); + ipw.print("mInitialHeightOnTouch="); + ipw.println(mInitialHeightOnTouch); + ipw.print("mInitialTouchX="); + ipw.println(mInitialTouchX); + ipw.print("mInitialTouchY="); + ipw.println(mInitialTouchY); + ipw.print("mTouchAboveFalsingThreshold="); + ipw.println(mTouchAboveFalsingThreshold); + ipw.print("mTracking="); + ipw.println(mTracking); + ipw.print("mTrackingPointer="); + ipw.println(mTrackingPointer); + ipw.print("mExpanded="); + ipw.println(mExpanded); + ipw.print("mFullyExpanded="); + ipw.println(mFullyExpanded); + ipw.print("mExpandImmediate="); + ipw.println(mExpandImmediate); + ipw.print("mExpandedWhenExpandingStarted="); + ipw.println(mExpandedWhenExpandingStarted); + ipw.print("mAnimatingHiddenFromCollapsed="); + ipw.println(mAnimatingHiddenFromCollapsed); + ipw.print("mVisible="); + ipw.println(mVisible); + ipw.print("mExpansionHeight="); + ipw.println(mExpansionHeight); + ipw.print("mMinExpansionHeight="); + ipw.println(mMinExpansionHeight); + ipw.print("mMaxExpansionHeight="); + ipw.println(mMaxExpansionHeight); + ipw.print("mShadeExpandedFraction="); + ipw.println(mShadeExpandedFraction); + ipw.print("mPeekHeight="); + ipw.println(mPeekHeight); + ipw.print("mLastOverscroll="); + ipw.println(mLastOverscroll); + ipw.print("mExpansionFromOverscroll="); + ipw.println(mExpansionFromOverscroll); + ipw.print("mExpansionEnabledPolicy="); + ipw.println(mExpansionEnabledPolicy); + ipw.print("mExpansionEnabledAmbient="); + ipw.println(mExpansionEnabledAmbient); + ipw.print("mQuickQsHeaderHeight="); + ipw.println(mQuickQsHeaderHeight); + ipw.print("mTwoFingerExpandPossible="); + ipw.println(mTwoFingerExpandPossible); + ipw.print("mConflictingExpansionGesture="); + ipw.println(mConflictingExpansionGesture); + ipw.print("mAnimatorExpand="); + ipw.println(mAnimatorExpand); + ipw.print("mCachedGestureInsets="); + ipw.println(mCachedGestureInsets); + ipw.print("mTransitioningToFullShadeProgress="); + ipw.println(mTransitioningToFullShadeProgress); + ipw.print("mDistanceForFullShadeTransition="); + ipw.println(mDistanceForFullShadeTransition); + ipw.print("mStackScrollerOverscrolling="); + ipw.println(mStackScrollerOverscrolling); + ipw.print("mAnimating="); + ipw.println(mAnimating); + ipw.print("mIsTranslationResettingAnimator="); + ipw.println(mIsTranslationResettingAnimator); + ipw.print("mIsPulseExpansionResettingAnimator="); + ipw.println(mIsPulseExpansionResettingAnimator); + ipw.print("mTranslationForFullShadeTransition="); + ipw.println(mTranslationForFullShadeTransition); + ipw.print("mAnimateNextNotificationBounds="); + ipw.println(mAnimateNextNotificationBounds); + ipw.print("mNotificationBoundsAnimationDelay="); + ipw.println(mNotificationBoundsAnimationDelay); + ipw.print("mNotificationBoundsAnimationDuration="); + ipw.println(mNotificationBoundsAnimationDuration); + ipw.print("mLastClippingTopBound="); + ipw.println(mLastClippingTopBound); + ipw.print("mLastNotificationsTopPadding="); + ipw.println(mLastNotificationsTopPadding); + ipw.print("mLastNotificationsClippingTopBound="); + ipw.println(mLastNotificationsClippingTopBound); + ipw.print("mLastNotificationsClippingTopBoundNssl="); + ipw.println(mLastNotificationsClippingTopBoundNssl); + ipw.print("mInterceptRegion="); + ipw.println(mInterceptRegion); + ipw.print("mClippingAnimationEndBounds="); + ipw.println(mClippingAnimationEndBounds); + ipw.print("mLastClipBounds="); + ipw.println(mLastClipBounds); + } + /** */ public FragmentHostManager.FragmentListener getQsFragmentListener() { return new QsFragmentListener(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java index e08bc33c1ccd..d0a3cbbf0b02 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java @@ -78,6 +78,11 @@ public interface ShadeController { boolean isShadeFullyOpen(); /** + * Returns whether shade or QS are currently opening or collapsing. + */ + boolean isExpandingOrCollapsing(); + + /** * Add a runnable for NotificationPanelView to post when the panel is expanded. * * @param action the action to post diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java index c71467b99961..d00dab633014 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java @@ -164,6 +164,11 @@ public final class ShadeControllerImpl implements ShadeController { } @Override + public boolean isExpandingOrCollapsing() { + return mNotificationPanelViewController.isExpandingOrCollapsing(); + } + + @Override public void postOnShadeExpanded(Runnable executable) { mNotificationPanelViewController.addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index a048f543d476..2db47aeb9e90 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -49,23 +49,14 @@ class ShadeExpansionStateManager @Inject constructor() : ShadeStateEvents { private var dragDownPxAmount: Float = 0f /** - * Adds a listener that will be notified when the panel expansion fraction has changed. + * Adds a listener that will be notified when the panel expansion fraction has changed and + * returns the current state in a ShadeExpansionChangeEvent for legacy purposes (b/23035507). * - * Listener will also be immediately notified with the current values. - */ - fun addExpansionListener(listener: ShadeExpansionListener) { - addShadeExpansionListener(listener) - listener.onPanelExpansionChanged( - ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount) - ) - } - - /** - * Adds a listener that will be notified when the panel expansion fraction has changed. * @see #addExpansionListener */ - fun addShadeExpansionListener(listener: ShadeExpansionListener) { + fun addExpansionListener(listener: ShadeExpansionListener): ShadeExpansionChangeEvent { expansionListeners.add(listener) + return ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount) } /** Removes an expansion listener. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index 25073c1b64db..2b772e372f77 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -253,6 +253,31 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { ) } + fun logFlingExpands( + vel: Float, + vectorVel: Float, + interactionType: Int, + minVelocityPxPerSecond: Float, + expansionOverHalf: Boolean, + allowExpandForSmallExpansion: Boolean + ) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + int1 = interactionType + long1 = vel.toLong() + long2 = vectorVel.toLong() + double1 = minVelocityPxPerSecond.toDouble() + bool1 = expansionOverHalf + bool2 = allowExpandForSmallExpansion + }, + { "NPVC flingExpands called with vel: $long1, vectorVel: $long2, " + + "interactionType: $int1, minVelocityPxPerSecond: $double1 " + + "expansionOverHalf: $bool1, allowExpandForSmallExpansion: $bool2" } + ) + } + fun flingQs(flingType: Int, isClick: Boolean) { buffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index d5a9e953c914..f75047c2072a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -48,7 +48,7 @@ interface ShadeViewController { fun expandToNotifications() /** Returns whether the shade is expanding or collapsing itself or quick settings. */ - val isExpanding: Boolean + val isExpandingOrCollapsing: Boolean /** * Returns whether the shade height is greater than zero (i.e. partially or fully expanded), diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java index 0ebcfa2a1876..fc1e87ad3e45 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java @@ -141,6 +141,7 @@ public class ShadeCarrierGroupController { mCarrierTextManager = carrierTextManagerBuilder .setShowAirplaneMode(false) .setShowMissingSim(false) + .setDebugLocationString("Shade") .build(); mCarrierConfigTracker = carrierConfigTracker; mSlotIndexResolver = slotIndexResolver; diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt index 44c8e48c65d4..ebb9935ca813 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -67,7 +67,8 @@ constructor(shadeExpansionStateManager: ShadeExpansionStateManager) : ShadeRepos } } - shadeExpansionStateManager.addExpansionListener(callback) + val currentState = shadeExpansionStateManager.addExpansionListener(callback) + callback.onPanelExpansionChanged(currentState) trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info") awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt index 41be526ec13e..ec16109fe1c7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt @@ -63,7 +63,9 @@ constructor( updateResources() } }) - shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) + val currentState = + shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) + onPanelExpansionChanged(currentState) shadeExpansionStateManager.addStateListener(this::onPanelStateChanged) dumpManager.registerCriticalDumpable("ShadeTransitionController") { printWriter, _ -> dump(printWriter) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt new file mode 100644 index 000000000000..8a96a4764e66 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -0,0 +1,72 @@ +/* + * 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.shade.ui.viewmodel + +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Models UI state and handles user input for the shade scene. */ +class ShadeSceneViewModel +@AssistedInject +constructor( + @Application private val applicationScope: CoroutineScope, + lockscreenSceneInteractorFactory: LockscreenSceneInteractor.Factory, + @Assisted private val containerName: String, +) { + private val lockScreenInteractor: LockscreenSceneInteractor = + lockscreenSceneInteractorFactory.create(containerName) + + /** The key of the scene we should switch to when swiping up. */ + val upDestinationSceneKey: StateFlow<SceneKey> = + lockScreenInteractor.isDeviceLocked + .map { isLocked -> upDestinationSceneKey(isLocked = isLocked) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = + upDestinationSceneKey( + isLocked = lockScreenInteractor.isDeviceLocked.value, + ), + ) + + /** Notifies that some content in the shade was clicked. */ + fun onContentClicked() { + lockScreenInteractor.dismissLockscreen() + } + + private fun upDestinationSceneKey( + isLocked: Boolean, + ): SceneKey { + return if (isLocked) SceneKey.Lockscreen else SceneKey.Gone + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): ShadeSceneViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 0ea257010751..12420ff5f481 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -186,7 +186,7 @@ public class KeyguardIndicationController { private boolean mPowerPluggedInDock; private boolean mPowerCharged; - private boolean mBatteryOverheated; + private boolean mBatteryDefender; private boolean mEnableBatteryDefender; private boolean mIncompatibleCharger; private int mChargingSpeed; @@ -921,7 +921,7 @@ public class KeyguardIndicationController { */ protected String computePowerIndication() { int chargingId; - if (mBatteryOverheated) { + if (mBatteryDefender) { chargingId = R.string.keyguard_plugged_in_charging_limited; String percentage = NumberFormat.getPercentInstance().format(mBatteryLevel / 100f); return mContext.getResources().getString(chargingId, percentage); @@ -1093,9 +1093,9 @@ public class KeyguardIndicationController { mChargingSpeed = status.getChargingSpeed(mContext); mBatteryLevel = status.level; mBatteryPresent = status.present; - mBatteryOverheated = status.isOverheated(); + mBatteryDefender = status.isBatteryDefender(); // when the battery is overheated, device doesn't charge so only guard on pluggedIn: - mEnableBatteryDefender = mBatteryOverheated && status.isPluggedIn(); + mEnableBatteryDefender = mBatteryDefender && status.isPluggedIn(); mIncompatibleCharger = status.incompatibleCharger.orElse(false); try { mChargingTimeRemaining = mPowerPluggedIn @@ -1106,7 +1106,7 @@ public class KeyguardIndicationController { } mKeyguardLogger.logRefreshBatteryInfo(isChargingOrFull, mPowerPluggedIn, mBatteryLevel, - mBatteryOverheated); + mBatteryDefender); updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt index 43f78c3166e4..e5849c05a534 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt @@ -87,7 +87,8 @@ class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : Status } } -class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent { +/** open only for testing purposes. (See [FakeStatusEvent.kt]) */ +open class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent { override var contentDescription: String? = null override val priority = 100 override var forceVisible = true @@ -107,9 +108,9 @@ class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent { } override fun shouldUpdateFromEvent(other: StatusEvent?): Boolean { - return other is PrivacyEvent && - (other.privacyItems != privacyItems || - other.contentDescription != contentDescription) + return other is PrivacyEvent && (other.privacyItems != privacyItems || + other.contentDescription != contentDescription || + (other.forceVisible && !forceVisible)) } override fun updateFromEvent(other: StatusEvent?) { @@ -122,5 +123,7 @@ class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent { privacyChip?.contentDescription = other.contentDescription privacyChip?.privacyList = other.privacyItems + + if (other.forceVisible) forceVisible = true } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt index f7a4feafee25..0a18f2d89d87 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt @@ -153,6 +153,7 @@ constructor( ) } currentlyDisplayedEvent?.updateFromEvent(event) + if (event.forceVisible) hasPersistentDot = true } else if (scheduledEvent.value?.shouldUpdateFromEvent(event) == true) { if (DEBUG) { Log.d( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 950dbd9300b3..7cc917f3b0b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.lockscreen import android.app.PendingIntent -import android.app.WallpaperManager import android.app.smartspace.SmartspaceConfig import android.app.smartspace.SmartspaceManager import android.app.smartspace.SmartspaceSession @@ -56,7 +55,6 @@ import com.android.systemui.plugins.WeatherData import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.settings.UserTracker import com.android.systemui.shared.regionsampling.RegionSampler -import com.android.systemui.shared.regionsampling.UpdateColorCallback import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DATE_SMARTSPACE_DATA_PLUGIN import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.WEATHER_SMARTSPACE_DATA_PLUGIN import com.android.systemui.statusbar.phone.KeyguardBypassController @@ -120,7 +118,7 @@ constructor( private val regionSamplingEnabled = featureFlags.isEnabled(Flags.REGION_SAMPLING) - private var isContentUpdatedOnce = false + private var isRegionSamplersCreated = false private var showNotifications = false private var showSensitiveContentForCurrentUser = false private var showSensitiveContentForManagedUser = false @@ -128,7 +126,6 @@ constructor( // TODO(b/202758428): refactor so that we can test color updates via region samping, similar to // how we test color updates when theme changes (See testThemeChangeUpdatesTextColor). - private val updateFun: UpdateColorCallback = { updateTextColorFromRegionSampler() } // TODO: Move logic into SmartspaceView var stateChangeListener = object : View.OnAttachStateChangeListener { @@ -144,6 +141,9 @@ constructor( override fun onViewDetachedFromWindow(v: View) { smartspaceViews.remove(v as SmartspaceView) + regionSamplers[v]?.stopRegionSampler() + regionSamplers.remove(v as SmartspaceView) + if (smartspaceViews.isEmpty()) { disconnect() } @@ -170,7 +170,7 @@ constructor( val filteredTargets = targets.filter(::filterSmartspaceTarget) plugin?.onTargetsAvailable(filteredTargets) - if (!isContentUpdatedOnce) { + if (!isRegionSamplersCreated) { for (v in smartspaceViews) { if (regionSamplingEnabled) { var regionSampler = RegionSampler( @@ -178,15 +178,14 @@ constructor( uiExecutor, bgExecutor, regionSamplingEnabled, - updateFun - ) + isLockscreen = true, + ) { updateTextColorFromRegionSampler() } initializeTextColors(regionSampler) regionSamplers[v] = regionSampler regionSampler.startRegionSampler() } - updateTextColorFromWallpaper() } - isContentUpdatedOnce = true + isRegionSamplersCreated = true } } @@ -504,18 +503,16 @@ constructor( } private fun updateTextColorFromRegionSampler() { - smartspaceViews.forEach { - val textColor = regionSamplers.get(it)?.currentForegroundColor() + regionSamplers.forEach { (view, region) -> + val textColor = region.currentForegroundColor() if (textColor != null) { - it.setPrimaryTextColor(textColor) + view.setPrimaryTextColor(textColor) } } } private fun updateTextColorFromWallpaper() { - val wallpaperManager = WallpaperManager.getInstance(context) - if (!regionSamplingEnabled || wallpaperManager.lockScreenWallpaperExists() || - regionSamplers.isEmpty()) { + if (!regionSamplingEnabled || regionSamplers.isEmpty()) { val wallpaperTextColor = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor) smartspaceViews.forEach { it.setPrimaryTextColor(wallpaperTextColor) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt index 18ee4816fac9..59fc387c4608 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt @@ -67,7 +67,10 @@ class ShadeViewDiffer( fun getViewLabel(view: View): String = nodes.values.firstOrNull { node -> node.view === view }?.label ?: view.toString() - private fun detachChildren(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) { + private fun detachChildren( + parentNode: ShadeNode, + specMap: Map<NodeController, NodeSpec> + ) = traceSection("detachChildren") { val views = nodes.values.associateBy { it.view } fun detachRecursively(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) { val parentSpec = specMap[parentNode.controller] @@ -124,7 +127,10 @@ class ShadeViewDiffer( } } - private fun attachChildren(parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec>) { + private fun attachChildren( + parentNode: ShadeNode, + specMap: Map<NodeController, NodeSpec> + ): Unit = traceSection("attachChildren") { val parentSpec = checkNotNull(specMap[parentNode.controller]) for ((index, childSpec) in parentSpec.children.withIndex()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 8af488ea443d..417d4acccf4e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -321,7 +321,8 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView protected void setBackgroundTintColor(int color) { if (color != mCurrentBackgroundTint) { mCurrentBackgroundTint = color; - if (color == mNormalColor) { + // TODO(282173943): re-enable this tinting optimization when Resources are thread-safe + if (false && color == mNormalColor) { // We don't need to tint a normal notification color = 0; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java index 6bbeebfdb431..0989df61a5e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java @@ -16,11 +16,15 @@ package com.android.systemui.statusbar.notification.row; +import static android.graphics.PorterDuff.Mode.SRC_ATOP; + import android.annotation.ColorInt; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.ColorFilter; +import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.IndentingPrintWriter; @@ -157,10 +161,20 @@ public class FooterView extends StackScrollerDecorView { */ public void updateColors() { Resources.Theme theme = mContext.getTheme(); - int textColor = getResources().getColor(R.color.notif_pill_text, theme); - mClearAllButton.setBackground(theme.getDrawable(R.drawable.notif_footer_btn_background)); + final @ColorInt int textColor = getResources().getColor(R.color.notif_pill_text, theme); + final Drawable clearAllBg = theme.getDrawable(R.drawable.notif_footer_btn_background); + final Drawable manageBg = theme.getDrawable(R.drawable.notif_footer_btn_background); + // TODO(b/282173943): Remove redundant tinting once Resources are thread-safe + final @ColorInt int buttonBgColor = + Utils.getColorAttrDefaultColor(mContext, com.android.internal.R.attr.colorSurface); + final ColorFilter bgColorFilter = new PorterDuffColorFilter(buttonBgColor, SRC_ATOP); + if (buttonBgColor != 0) { + clearAllBg.setColorFilter(bgColorFilter); + manageBg.setColorFilter(bgColorFilter); + } + mClearAllButton.setBackground(clearAllBg); mClearAllButton.setTextColor(textColor); - mManageButton.setBackground(theme.getDrawable(R.drawable.notif_footer_btn_background)); + mManageButton.setBackground(manageBg); mManageButton.setTextColor(textColor); final @ColorInt int labelTextColor = Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index ae7c216adcbb..b0f3f598cb91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -93,6 +93,12 @@ public class AmbientState implements Dumpable { private boolean mAppearing; private float mPulseHeight = MAX_PULSE_HEIGHT; + /** + * The ExpandableNotificationRow that is pulsing, or the one that was pulsing + * when the device started to transition from AOD to LockScreen. + */ + private ExpandableNotificationRow mPulsingRow; + /** Fraction of lockscreen to shade animation (on lockscreen swipe down). */ private float mFractionToShade; @@ -564,6 +570,19 @@ public class AmbientState implements Dumpable { return mPulsing && entry.isAlerting(); } + public void setPulsingRow(ExpandableNotificationRow row) { + mPulsingRow = row; + } + + /** + * @param row The row to check + * @return true if row is the pulsing row when the device started to transition from AOD to lock + * screen + */ + public boolean isPulsingRow(ExpandableView row) { + return mPulsingRow == row; + } + public boolean isPanelTracking() { return mPanelTracking; } 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 92d767a419f1..6f1c378f429d 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 @@ -548,7 +548,7 @@ public class StackScrollAlgorithm { ExpandableViewState viewState = view.getViewState(); viewState.location = ExpandableViewState.LOCATION_UNKNOWN; - final float expansionFraction = getExpansionFractionWithoutShelf( + float expansionFraction = getExpansionFractionWithoutShelf( algorithmState, ambientState); // Add gap between sections. @@ -619,6 +619,11 @@ public class StackScrollAlgorithm { updateViewWithShelf(view, viewState, shelfStart); } } + // Avoid pulsing notification flicker during AOD to LS + // A pulsing notification is already expanded, no need to expand it again with animation + if (ambientState.isPulsingRow(view)) { + expansionFraction = 1.0f; + } // Clip height of view right before shelf. viewState.height = (int) (getMaxAllowedChildHeight(view) * expansionFraction); } @@ -700,9 +705,11 @@ public class StackScrollAlgorithm { && !(child instanceof FooterView); } - private void updatePulsingStates(StackScrollAlgorithmState algorithmState, + @VisibleForTesting + void updatePulsingStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); + ExpandableNotificationRow pulsingRow = null; for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); if (!(child instanceof ExpandableNotificationRow)) { @@ -714,6 +721,19 @@ public class StackScrollAlgorithm { } ExpandableViewState viewState = row.getViewState(); viewState.hidden = false; + pulsingRow = row; + } + + // Set AmbientState#pulsingRow to the current pulsing row when on AOD. + // Set AmbientState#pulsingRow=null when on lockscreen, since AmbientState#pulsingRow + // is only used for skipping the unfurl animation for (the notification that was already + // showing at full height on AOD) during the AOD=>lockscreen transition, where + // dozeAmount=[1f, 0f). We also need to reset the pulsingRow once it is no longer used + // because it will interfere with future unfurling animations - for example, during the + // LS=>AOD animation, the pulsingRow may stay at full height when it should squish with the + // rest of the stack. + if (ambientState.getDozeAmount() == 0.0f || ambientState.getDozeAmount() == 1.0f) { + ambientState.setPulsingRow(pulsingRow); } } 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 bbb4f2449330..df1a47abea98 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -306,6 +306,18 @@ constructor( intent: Intent, onlyProvisioned: Boolean, dismissShade: Boolean, + ) { + activityStarterInternal.startActivityDismissingKeyguard( + intent = intent, + onlyProvisioned = onlyProvisioned, + dismissShade = dismissShade, + ) + } + + override fun startActivityDismissingKeyguard( + intent: Intent, + onlyProvisioned: Boolean, + dismissShade: Boolean, disallowEnterPictureInPictureWhileLaunching: Boolean, callback: ActivityStarter.Callback?, flags: Int, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index bd7840d447cf..2d8f371aadac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -469,13 +469,13 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp case MODE_DISMISS_BOUNCER: Trace.beginSection("MODE_DISMISS_BOUNCER"); mKeyguardViewController.notifyKeyguardAuthenticated( - false /* strongAuth */); + false /* primaryAuth */); Trace.endSection(); break; case MODE_UNLOCK_COLLAPSING: Trace.beginSection("MODE_UNLOCK_COLLAPSING"); mKeyguardViewController.notifyKeyguardAuthenticated( - false /* strongAuth */); + false /* primaryAuth */); Trace.endSection(); break; case MODE_SHOW_BOUNCER: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 4e690696b5d3..86bf7cb6fa2f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -20,7 +20,6 @@ import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIO import android.annotation.Nullable; import android.app.ActivityOptions; -import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -32,6 +31,7 @@ import android.view.KeyEvent; import android.view.RemoteAnimationAdapter; import android.view.View; import android.view.ViewGroup; +import android.window.RemoteTransition; import android.window.SplashScreen; import androidx.annotation.NonNull; @@ -43,15 +43,14 @@ import com.android.internal.statusbar.RegisterStatusBarResult; import com.android.keyguard.AuthKeyguardMessageArea; import com.android.systemui.Dumpable; import com.android.systemui.animation.ActivityLaunchAnimator; -import com.android.systemui.animation.RemoteTransitionAdapter; import com.android.systemui.navigationbar.NavigationBarView; -import com.android.systemui.plugins.ActivityStarter.Callback; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.qs.QSPanelController; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.ShadeViewController; +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; import com.android.systemui.statusbar.LightRevealScrim; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -160,8 +159,9 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { if (animationAdapter != null) { if (ENABLE_SHELL_TRANSITIONS) { options = ActivityOptions.makeRemoteTransition( - RemoteTransitionAdapter.adaptRemoteAnimation(animationAdapter, - "SysUILaunch")); + new RemoteTransition( + RemoteAnimationRunnerCompat.wrap(animationAdapter.getRunner()), + animationAdapter.getCallingApplication(), "SysUILaunch")); } else { options = ActivityOptions.makeRemoteAnimation(animationAdapter); } @@ -233,35 +233,8 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { boolean isShadeDisabled(); - /** Starts an activity. Please use ActivityStarter instead of using these methods directly. */ - void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, - int flags); - - /** Starts an activity. Please use ActivityStarter instead of using these methods directly. */ - void startActivity(Intent intent, boolean dismissShade); - - /** Starts an activity. Please use ActivityStarter instead of using these methods directly. */ - void startActivity(Intent intent, boolean dismissShade, - @Nullable ActivityLaunchAnimator.Controller animationController); - - /** Starts an activity. Please use ActivityStarter instead of using these methods directly. */ - void startActivity(Intent intent, boolean dismissShade, - @Nullable ActivityLaunchAnimator.Controller animationController, - boolean showOverLockscreenWhenLocked); - - /** Starts an activity. Please use ActivityStarter instead of using these methods directly. */ - void startActivity(Intent intent, boolean dismissShade, - @Nullable ActivityLaunchAnimator.Controller animationController, - boolean showOverLockscreenWhenLocked, UserHandle userHandle); - boolean isLaunchingActivityOverLockscreen(); - /** Starts an activity. Please use ActivityStarter instead of using these methods directly. */ - void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade); - - /** Starts an activity. Please use ActivityStarter instead of using these methods directly. */ - void startActivity(Intent intent, boolean dismissShade, Callback callback); - boolean isWakeUpComingFromTouch(); void onKeyguardViewManagerStatesUpdated(); @@ -322,122 +295,12 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { float getDisplayHeight(); - /** Starts an activity intent that dismisses keyguard. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void startActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned, - boolean dismissShade, int flags); - - /** Starts an activity intent that dismisses keyguard. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void startActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned, - boolean dismissShade); - - /** Starts an activity intent that dismisses keyguard. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void startActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned, - boolean dismissShade, boolean disallowEnterPictureInPictureWhileLaunching, - Callback callback, int flags, - @Nullable ActivityLaunchAnimator.Controller animationController, - UserHandle userHandle); - - /** Starts an activity intent that dismisses keyguard. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void startActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned, - boolean dismissShade, boolean disallowEnterPictureInPictureWhileLaunching, - Callback callback, int flags, - @Nullable ActivityLaunchAnimator.Controller animationController, - UserHandle userHandle, @Nullable String customMessage); - void readyForKeyguardDone(); - void executeRunnableDismissingKeyguard(Runnable runnable, - Runnable cancelAction, - boolean dismissShade, - boolean afterKeyguardGone, - boolean deferred); - - void executeRunnableDismissingKeyguard(Runnable runnable, - Runnable cancelAction, - boolean dismissShade, - boolean afterKeyguardGone, - boolean deferred, - boolean willAnimateOnKeyguard, - @Nullable String customMessage); - void resetUserExpandedStates(); - /** - * Dismisses Keyguard and executes an action afterwards. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancelAction, - boolean afterKeyguardGone); - - /** - * Dismisses Keyguard and executes an action afterwards. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancelAction, - boolean afterKeyguardGone, @Nullable String customMessage); - void setLockscreenUser(int newUserId); - /** - * Starts a QS runnable on the main thread and dismisses keyguard. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void postQSRunnableDismissingKeyguard(Runnable runnable); - - /** - * Starts an activity on the main thread with a delay. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void postStartActivityDismissingKeyguard(PendingIntent intent); - - /** - * Starts an activity on the main thread with a delay. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void postStartActivityDismissingKeyguard(PendingIntent intent, - @Nullable ActivityLaunchAnimator.Controller animationController); - - /** - * Starts an activity on the main thread with a delay. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void postStartActivityDismissingKeyguard(Intent intent, int delay); - - /** - * Starts an activity on the main thread with a delay. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void postStartActivityDismissingKeyguard(Intent intent, int delay, - @Nullable ActivityLaunchAnimator.Controller animationController); - - /** - * Starts an activity on the main thread with a delay. - * - * Please use ActivityStarter instead of using these methods directly. - */ - void postStartActivityDismissingKeyguard(Intent intent, int delay, - @Nullable ActivityLaunchAnimator.Controller animationController, - @Nullable String customMessage); - void showKeyguard(); boolean hideKeyguard(); @@ -531,18 +394,6 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { void awakenDreams(); - void startPendingIntentDismissingKeyguard(PendingIntent intent); - - void startPendingIntentDismissingKeyguard( - PendingIntent intent, @Nullable Runnable intentSentUiThreadCallback); - - void startPendingIntentDismissingKeyguard(PendingIntent intent, - Runnable intentSentUiThreadCallback, View associatedView); - - void startPendingIntentDismissingKeyguard( - PendingIntent intent, @Nullable Runnable intentSentUiThreadCallback, - @Nullable ActivityLaunchAnimator.Controller animationController); - void clearNotificationEffects(); boolean isBouncerShowing(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 37e77766c889..0ccc81981e58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -339,7 +339,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mHeadsUpManager.unpinAll(true /* userUnpinned */); mMetricsLogger.count("panel_open", 1); } else if (!mQsController.getExpanded() - && !mShadeViewController.isExpanding()) { + && !mShadeViewController.isExpandingOrCollapsing()) { mQsController.flingQs(0 /* velocity */, ShadeViewController.FLING_EXPAND); mMetricsLogger.count("panel_open_qs", 1); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 263566e69c88..0402d4f88205 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -31,7 +31,6 @@ import static androidx.lifecycle.Lifecycle.State.RESUMED; import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME; import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL; -import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT; @@ -43,8 +42,6 @@ import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode import android.annotation.Nullable; import android.app.ActivityManager; -import android.app.ActivityOptions; -import android.app.ActivityTaskManager; import android.app.IWallpaperManager; import android.app.KeyguardManager; import android.app.Notification; @@ -52,7 +49,6 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.StatusBarManager; import android.app.TaskInfo; -import android.app.TaskStackBuilder; import android.app.UiModeManager; import android.app.WallpaperInfo; import android.app.WallpaperManager; @@ -138,7 +134,6 @@ import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController; import com.android.systemui.animation.ActivityLaunchAnimator; -import com.android.systemui.animation.DelegateLaunchAnimatorController; import com.android.systemui.assist.AssistManager; import com.android.systemui.biometrics.AuthRippleController; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -168,7 +163,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.notetask.NoteTaskController; -import com.android.systemui.plugins.ActivityStarter.Callback; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.FalsingManager; @@ -192,6 +187,7 @@ import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.QuickSettingsController; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionChangeEvent; +import com.android.systemui.shade.ShadeExpansionListener; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shade.ShadeLogger; import com.android.systemui.shade.ShadeSurface; @@ -542,6 +538,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final WallpaperManager mWallpaperManager; private final UserTracker mUserTracker; private final Provider<FingerprintManager> mFingerprintManager; + private final ActivityStarter mActivityStarter; private CentralSurfacesComponent mCentralSurfacesComponent; @@ -819,7 +816,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { LightRevealScrim lightRevealScrim, AlternateBouncerInteractor alternateBouncerInteractor, UserTracker userTracker, - Provider<FingerprintManager> fingerprintManager + Provider<FingerprintManager> fingerprintManager, + ActivityStarter activityStarter ) { mContext = context; mNotificationsController = notificationsController; @@ -906,6 +904,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mAlternateBouncerInteractor = alternateBouncerInteractor; mUserTracker = userTracker; mFingerprintManager = fingerprintManager; + mActivityStarter = activityStarter; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; mStartingSurfaceOptional = startingSurfaceOptional; @@ -915,7 +914,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mScreenOffAnimationController = screenOffAnimationController; - mShadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged); + ShadeExpansionListener shadeExpansionListener = this::onPanelExpansionChanged; + ShadeExpansionChangeEvent currentState = + mShadeExpansionStateManager.addExpansionListener(shadeExpansionListener); + shadeExpansionListener.onPanelExpansionChanged(currentState); + mShadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged); mActivityIntentHelper = new ActivityIntentHelper(mContext); @@ -1224,6 +1227,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // By default turning off the screen also closes the shade. // We want to make sure that the shade status is kept after folding/unfolding. boolean isShadeOpen = mShadeController.isShadeFullyOpen(); + boolean isShadeExpandingOrCollapsing = mShadeController.isExpandingOrCollapsing(); boolean leaveOpen = isShadeOpen && !willGoToSleep && mState == SHADE; if (DEBUG) { Log.d(TAG, String.format( @@ -1231,14 +1235,15 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { + "isFolded=%s, " + "willGoToSleep=%s, " + "isShadeOpen=%s, " + + "isShadeExpandingOrCollapsing=%s, " + "leaveOpen=%s", - isFolded, willGoToSleep, isShadeOpen, leaveOpen)); + isFolded, willGoToSleep, isShadeOpen, isShadeExpandingOrCollapsing, leaveOpen)); } if (leaveOpen) { // below makes shade stay open when going from folded to unfolded mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); } - if (mState != SHADE && isShadeOpen) { + if (mState != SHADE && (isShadeOpen || isShadeExpandingOrCollapsing)) { // When device state changes on KEYGUARD/SHADE_LOCKED we don't want to keep the state of // the shade and instead we open clean state of keyguard with shade closed. // Normally some parts of QS state (like expanded/collapsed) are persisted and @@ -1266,7 +1271,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) { mNotificationIconAreaController.setupShelf(mNotificationShelfController); } - mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator); + ShadeExpansionChangeEvent currentState = + mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator); + mWakeUpCoordinator.onPanelExpansionChanged(currentState); // Allow plugins to reference DarkIconDispatcher and StatusBarStateController mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class); @@ -1432,12 +1439,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { message.write(SystemProperties.get("ro.serialno")); message.write("\n"); - startActivityDismissingKeyguard(Intent.createChooser(new Intent(Intent.ACTION_SEND) - .setType("*/*") - .putExtra(Intent.EXTRA_SUBJECT, "Rejected touch report") - .putExtra(Intent.EXTRA_STREAM, session) - .putExtra(Intent.EXTRA_TEXT, message.toString()), - "Share rejected touch report") + mActivityStarter.startActivityDismissingKeyguard(Intent.createChooser(new Intent( + Intent.ACTION_SEND) + .setType("*/*") + .putExtra(Intent.EXTRA_SUBJECT, "Rejected touch " + + "report") + .putExtra(Intent.EXTRA_STREAM, session) + .putExtra(Intent.EXTRA_TEXT, message.toString()), + "Share rejected touch report") .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), true /* onlyProvisioned */, true /* dismissShade */); }); @@ -1794,133 +1803,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return (mDisabled1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0; } - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, - int flags) { - startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, flags); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startActivity(Intent intent, boolean dismissShade) { - startActivityDismissingKeyguard(intent, false /* onlyProvisioned */, dismissShade); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startActivity(Intent intent, boolean dismissShade, - @androidx.annotation.Nullable ActivityLaunchAnimator.Controller animationController) { - startActivity(intent, dismissShade, animationController, false); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startActivity(Intent intent, boolean dismissShade, - @Nullable ActivityLaunchAnimator.Controller animationController, - boolean showOverLockscreenWhenLocked) { - startActivity(intent, dismissShade, animationController, showOverLockscreenWhenLocked, - getActivityUserHandle(intent)); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startActivity(Intent intent, boolean dismissShade, - @Nullable ActivityLaunchAnimator.Controller animationController, - boolean showOverLockscreenWhenLocked, UserHandle userHandle) { - // Make sure that we dismiss the keyguard if it is directly dismissable or when we don't - // want to show the activity above it. - if (mKeyguardStateController.isUnlocked() || !showOverLockscreenWhenLocked) { - startActivityDismissingKeyguard(intent, false, dismissShade, - false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, - 0 /* flags */, animationController, userHandle); - return; - } - - boolean animate = - animationController != null && shouldAnimateLaunch(true /* isActivityIntent */, - showOverLockscreenWhenLocked); - - ActivityLaunchAnimator.Controller controller = null; - if (animate) { - // Wrap the animation controller to dismiss the shade and set - // mIsLaunchingActivityOverLockscreen during the animation. - ActivityLaunchAnimator.Controller delegate = wrapAnimationController( - animationController, dismissShade, /* isLaunchForActivity= */ true); - controller = new DelegateLaunchAnimatorController(delegate) { - @Override - public void onIntentStarted(boolean willAnimate) { - getDelegate().onIntentStarted(willAnimate); - - if (willAnimate) { - CentralSurfacesImpl.this.mIsLaunchingActivityOverLockscreen = true; - } - } - - @Override - public void onLaunchAnimationStart(boolean isExpandingFullyAbove) { - super.onLaunchAnimationStart(isExpandingFullyAbove); - - // Double check that the keyguard is still showing and not going away, but if so - // set the keyguard occluded. Typically, WM will let KeyguardViewMediator know - // directly, but we're overriding that to play the custom launch animation, so - // we need to take care of that here. The unocclude animation is not overridden, - // so WM will call KeyguardViewMediator's unocclude animation runner when the - // activity is exited. - if (mKeyguardStateController.isShowing() - && !mKeyguardStateController.isKeyguardGoingAway()) { - Log.d(TAG, "Setting occluded = true in #startActivity."); - mKeyguardViewMediator.setOccluded(true /* isOccluded */, - true /* animate */); - } - } - - @Override - public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) { - // 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. - CentralSurfacesImpl.this.mIsLaunchingActivityOverLockscreen = false; - getDelegate().onLaunchAnimationEnd(isExpandingFullyAbove); - } - - @Override - public void onLaunchAnimationCancelled(@Nullable Boolean newKeyguardOccludedState) { - if (newKeyguardOccludedState != null) { - mKeyguardViewMediator.setOccluded( - newKeyguardOccludedState, false /* animate */); - } - - // 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. - CentralSurfacesImpl.this.mIsLaunchingActivityOverLockscreen = false; - getDelegate().onLaunchAnimationCancelled(newKeyguardOccludedState); - } - }; - } else if (dismissShade) { - // The animation will take care of dismissing the shade at the end of the animation. If - // we don't animate, collapse it directly. - collapseShade(); - } - - // We should exit the dream to prevent the activity from starting below the - // dream. - if (mKeyguardUpdateMonitor.isDreaming()) { - awakenDreams(); - } - - mActivityLaunchAnimator.startIntentWithAnimation(controller, animate, - intent.getPackage(), showOverLockscreenWhenLocked, (adapter) -> TaskStackBuilder - .create(mContext) - .addNextIntent(intent) - .startActivities( - CentralSurfaces.getActivityOptions(getDisplayId(), adapter), - userHandle)); - } - /** * Whether we are currently animating an activity launch above the lockscreen (occluding * activity). @@ -1931,18 +1813,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } @Override - public void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade) { - startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade); - } - - @Override - public void startActivity(Intent intent, boolean dismissShade, Callback callback) { - startActivityDismissingKeyguard(intent, false, dismissShade, - false /* disallowEnterPictureInPictureWhileLaunching */, callback, 0, - null /* animationController */, getActivityUserHandle(intent)); - } - - @Override public boolean isWakeUpComingFromTouch() { return mWakeUpComingFromTouch; } @@ -2421,228 +2291,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return mDisplayId; } - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned, - boolean dismissShade, int flags) { - startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, - false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, - flags, null /* animationController */, getActivityUserHandle(intent)); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned, - boolean dismissShade) { - startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, 0); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned, - boolean dismissShade, boolean disallowEnterPictureInPictureWhileLaunching, - Callback callback, int flags, - @androidx.annotation.Nullable ActivityLaunchAnimator.Controller animationController, - UserHandle userHandle) { - startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, - disallowEnterPictureInPictureWhileLaunching, callback, flags, animationController, - userHandle, null /* customMessage */); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned, - final boolean dismissShade, final boolean disallowEnterPictureInPictureWhileLaunching, - final Callback callback, int flags, - @Nullable ActivityLaunchAnimator.Controller animationController, - final UserHandle userHandle, @Nullable String customMessage) { - if (onlyProvisioned && !mDeviceProvisionedController.isDeviceProvisioned()) return; - - final boolean willLaunchResolverActivity = - mActivityIntentHelper.wouldLaunchResolverActivity(intent, - mLockscreenUserManager.getCurrentUserId()); - - boolean animate = - animationController != null && !willLaunchResolverActivity && shouldAnimateLaunch( - true /* isActivityIntent */); - ActivityLaunchAnimator.Controller animController = - animationController != null ? wrapAnimationController(animationController, - dismissShade, /* isLaunchForActivity= */ true) : null; - - // If we animate, we will dismiss the shade only once the animation is done. This is taken - // care of by the StatusBarLaunchAnimationController. - boolean dismissShadeDirectly = dismissShade && animController == null; - - Runnable runnable = () -> { - mAssistManagerLazy.get().hideAssist(); - intent.setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.addFlags(flags); - int[] result = new int[]{ActivityManager.START_CANCELED}; - - mActivityLaunchAnimator.startIntentWithAnimation(animController, - animate, intent.getPackage(), (adapter) -> { - ActivityOptions options = new ActivityOptions( - CentralSurfaces.getActivityOptions(mDisplayId, adapter)); - - // We know that the intent of the caller is to dismiss the keyguard and - // this runnable is called right after the keyguard is solved, so we tell - // WM that we should dismiss it to avoid flickers when opening an activity - // that can also be shown over the keyguard. - options.setDismissKeyguard(); - options.setDisallowEnterPictureInPictureWhileLaunching( - disallowEnterPictureInPictureWhileLaunching); - if (CameraIntents.isInsecureCameraIntent(intent)) { - // Normally an activity will set it's requested rotation - // animation on its window. However when launching an activity - // causes the orientation to change this is too late. In these cases - // the default animation is used. This doesn't look good for - // the camera (as it rotates the camera contents out of sync - // with physical reality). So, we ask the WindowManager to - // force the crossfade animation if an orientation change - // happens to occur during the launch. - options.setRotationAnimationHint( - WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS); - } - if (Settings.Panel.ACTION_VOLUME.equals(intent.getAction())) { - // Settings Panel is implemented as activity(not a dialog), so - // underlying app is paused and may enter picture-in-picture mode - // as a result. - // So we need to disable picture-in-picture mode here - // if it is volume panel. - options.setDisallowEnterPictureInPictureWhileLaunching(true); - } - - try { - result[0] = ActivityTaskManager.getService().startActivityAsUser( - null, mContext.getBasePackageName(), - mContext.getAttributionTag(), - intent, - intent.resolveTypeIfNeeded(mContext.getContentResolver()), - null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, - options.toBundle(), userHandle.getIdentifier()); - } catch (RemoteException e) { - Log.w(TAG, "Unable to start activity", e); - } - return result[0]; - }); - - if (callback != null) { - callback.onActivityStarted(result[0]); - } - }; - Runnable cancelRunnable = () -> { - if (callback != null) { - callback.onActivityStarted(ActivityManager.START_CANCELED); - } - }; - // Do not deferKeyguard when occluded because, when keyguard is occluded, - // we do not launch the activity until keyguard is done. - boolean occluded = mKeyguardStateController.isShowing() - && mKeyguardStateController.isOccluded(); - boolean deferred = !occluded; - executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShadeDirectly, - willLaunchResolverActivity, deferred /* deferred */, animate, - customMessage /* customMessage */); - } - - /** - * Return a {@link ActivityLaunchAnimator.Controller} wrapping {@code animationController} so - * that: - * - if it launches in the notification shade window and {@code dismissShade} is true, then - * the shade will be instantly dismissed at the end of the animation. - * - if it launches in status bar window, it will make the status bar window match the device - * size during the animation (that way, the animation won't be clipped by the status bar - * size). - * - * @param animationController the controller that is wrapped and will drive the main animation. - * @param dismissShade whether the notification shade will be dismissed at the end of the - * animation. This is ignored if {@code animationController} is not - * animating in the shade window. - * @param isLaunchForActivity whether the launch is for an activity. - * - * Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. - */ - @Nullable - private ActivityLaunchAnimator.Controller wrapAnimationController( - ActivityLaunchAnimator.Controller animationController, boolean dismissShade, - boolean isLaunchForActivity) { - View rootView = animationController.getLaunchContainer().getRootView(); - - Optional<ActivityLaunchAnimator.Controller> controllerFromStatusBar = - mStatusBarWindowController.wrapAnimationControllerIfInStatusBar( - rootView, animationController); - if (controllerFromStatusBar.isPresent()) { - return controllerFromStatusBar.get(); - } - - if (dismissShade) { - // If the view is not in the status bar, then we are animating a view in the shade. - // We have to make sure that we collapse it when the animation ends or is cancelled. - return new StatusBarLaunchAnimatorController(animationController, this, - isLaunchForActivity); - } - - return animationController; - } - @Override public void readyForKeyguardDone() { mStatusBarKeyguardViewManager.readyForKeyguardDone(); } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void executeRunnableDismissingKeyguard(final Runnable runnable, - final Runnable cancelAction, - final boolean dismissShade, - final boolean afterKeyguardGone, - final boolean deferred) { - executeRunnableDismissingKeyguard(runnable, cancelAction, dismissShade, afterKeyguardGone, - deferred, false /* willAnimateOnKeyguard */, null /* customMessage */); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void executeRunnableDismissingKeyguard(final Runnable runnable, - final Runnable cancelAction, - final boolean dismissShade, - final boolean afterKeyguardGone, - final boolean deferred, - final boolean willAnimateOnKeyguard, - @Nullable String customMessage) { - OnDismissAction onDismissAction = new OnDismissAction() { - @Override - public boolean onDismiss() { - if (runnable != null) { - if (mKeyguardStateController.isShowing() - && mKeyguardStateController.isOccluded()) { - mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable); - } else { - mMainExecutor.execute(runnable); - } - } - if (dismissShade) { - if (mShadeController.isExpandedVisible() && !mBouncerShowing) { - mShadeController.animateCollapseShadeDelayed(); - } else { - // Do it after DismissAction has been processed to conserve the needed - // ordering. - mMainExecutor.execute(mShadeController::runPostCollapseRunnables); - } - } - return deferred; - } - - @Override - public boolean willRunAnimationOnKeyguard() { - return willAnimateOnKeyguard; - } - }; - dismissKeyguardThenExecute(onDismissAction, cancelAction, afterKeyguardGone, customMessage); - } - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -2710,49 +2363,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (mKeyguardStateController.isShowing() && requiresShadeOpen) { mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); } - dismissKeyguardThenExecute(action, null /* cancelAction */, + mActivityStarter.dismissKeyguardThenExecute(action, null /* cancelAction */, afterKeyguardGone /* afterKeyguardGone */); } - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - protected void dismissKeyguardThenExecute(OnDismissAction action, boolean afterKeyguardGone) { - dismissKeyguardThenExecute(action, null /* cancelRunnable */, afterKeyguardGone); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancelAction, - boolean afterKeyguardGone) { - dismissKeyguardThenExecute(action, cancelAction, afterKeyguardGone, null); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void dismissKeyguardThenExecute(OnDismissAction action, Runnable cancelAction, - boolean afterKeyguardGone, String customMessage) { - if (!action.willRunAnimationOnKeyguard() - && mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP - && mKeyguardStateController.canDismissLockScreen() - && !mStatusBarStateController.leaveOpenOnKeyguardHide() - && mDozeServiceHost.isPulsing()) { - // Reuse the biometric wake-and-unlock transition if we dismiss keyguard from a pulse. - // TODO: Factor this transition out of BiometricUnlockController. - mBiometricUnlockController.startWakeAndUnlock( - BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING); - } - if (mKeyguardStateController.isShowing()) { - mStatusBarKeyguardViewManager.dismissWithAction(action, cancelAction, - afterKeyguardGone, customMessage); - } else { - // If the keyguard isn't showing but the device is dreaming, we should exit the dream. - if (mKeyguardUpdateMonitor.isDreaming()) { - awakenDreams(); - } - action.onDismiss(); - } - - } - /** * Notify the shade controller that the current user changed * @@ -2928,61 +2542,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { | ((currentlyInsecure ? 1 : 0) << 12); } - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void postQSRunnableDismissingKeyguard(final Runnable runnable) { - mMainExecutor.execute(() -> { - mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); - executeRunnableDismissingKeyguard( - () -> mMainExecutor.execute(runnable), null, false, false, false); - }); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void postStartActivityDismissingKeyguard(PendingIntent intent) { - postStartActivityDismissingKeyguard(intent, null /* animationController */); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void postStartActivityDismissingKeyguard(final PendingIntent intent, - @Nullable ActivityLaunchAnimator.Controller animationController) { - mMainExecutor.execute(() -> startPendingIntentDismissingKeyguard(intent, - null /* intentSentUiThreadCallback */, animationController)); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void postStartActivityDismissingKeyguard(final Intent intent, int delay) { - postStartActivityDismissingKeyguard(intent, delay, null /* animationController */); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void postStartActivityDismissingKeyguard(Intent intent, int delay, - @Nullable ActivityLaunchAnimator.Controller animationController) { - postStartActivityDismissingKeyguard(intent, delay, animationController, - null /* customMessage */); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void postStartActivityDismissingKeyguard(Intent intent, int delay, - @Nullable ActivityLaunchAnimator.Controller animationController, - @Nullable String customMessage) { - mMainExecutor.executeDelayed( - () -> - startActivityDismissingKeyguard(intent, true /* onlyProvisioned */, - true /* dismissShade */, - false /* disallowEnterPictureInPictureWhileLaunching */, - null /* callback */, - 0 /* flags */, - animationController, - getActivityUserHandle(intent), customMessage), - delay); - } - @Override public void showKeyguard() { mStatusBarStateController.setKeyguardRequested(true); @@ -4111,95 +3670,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return willAnimateOnKeyguard; } }; - dismissKeyguardThenExecute(onDismissAction, afterKeyguardGone); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startPendingIntentDismissingKeyguard(final PendingIntent intent) { - startPendingIntentDismissingKeyguard(intent, null); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startPendingIntentDismissingKeyguard( - final PendingIntent intent, @Nullable final Runnable intentSentUiThreadCallback) { - startPendingIntentDismissingKeyguard(intent, intentSentUiThreadCallback, - (ActivityLaunchAnimator.Controller) null); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startPendingIntentDismissingKeyguard(PendingIntent intent, - Runnable intentSentUiThreadCallback, View associatedView) { - ActivityLaunchAnimator.Controller animationController = null; - if (associatedView instanceof ExpandableNotificationRow) { - animationController = mNotificationAnimationProvider.getAnimatorController( - ((ExpandableNotificationRow) associatedView)); - } - - startPendingIntentDismissingKeyguard(intent, intentSentUiThreadCallback, - animationController); - } - - /** Logic is duplicated in {@link ActivityStarterImpl}. Please add it there too. */ - @Override - public void startPendingIntentDismissingKeyguard( - final PendingIntent intent, @Nullable final Runnable intentSentUiThreadCallback, - @Nullable ActivityLaunchAnimator.Controller animationController) { - final boolean willLaunchResolverActivity = intent.isActivity() - && mActivityIntentHelper.wouldPendingLaunchResolverActivity(intent, - mLockscreenUserManager.getCurrentUserId()); - - boolean animate = !willLaunchResolverActivity - && animationController != null - && shouldAnimateLaunch(intent.isActivity()); - - // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we run - // the animation on the keyguard). The animation will take care of (instantly) collapsing - // the shade and hiding the keyguard once it is done. - boolean collapse = !animate; - executeActionDismissingKeyguard(() -> { - try { - // We wrap animationCallback with a StatusBarLaunchAnimatorController so that the - // shade is collapsed after the animation (or when it is cancelled, aborted, etc). - ActivityLaunchAnimator.Controller controller = - animationController != null ? wrapAnimationController( - animationController, /* dismissShade= */ true, intent.isActivity()) - : null; - - mActivityLaunchAnimator.startPendingIntentWithAnimation( - controller, animate, intent.getCreatorPackage(), - (animationAdapter) -> { - ActivityOptions options = new ActivityOptions( - CentralSurfaces.getActivityOptions( - mDisplayId, animationAdapter)); - // TODO b/221255671: restrict this to only be set for notifications - options.setEligibleForLegacyPermissionPrompt(true); - return intent.sendAndReturnResult(null, 0, null, null, null, - null, options.toBundle()); - }); - } catch (PendingIntent.CanceledException e) { - // the stack trace isn't very helpful here. - // Just log the exception message. - Log.w(TAG, "Sending intent failed: " + e); - if (!collapse) { - // executeActionDismissingKeyguard did not collapse for us already. - collapsePanelOnMainThread(); - } - // TODO: Dismiss Keyguard. - } - if (intent.isActivity()) { - mAssistManagerLazy.get().hideAssist(); - } - if (intentSentUiThreadCallback != null) { - postOnUiThread(intentSentUiThreadCallback); - } - }, willLaunchResolverActivity, collapse, animate); - } - - private void postOnUiThread(Runnable runnable) { - mMainExecutor.execute(runnable); + mActivityStarter.dismissKeyguardThenExecute(onDismissAction, /* cancel= */ null, + afterKeyguardGone); } private void onShadeVisibilityChanged(boolean visible) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index f2fbd7d52ac3..df68e7eb037b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -386,7 +386,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mExpansionCallback); mShadeViewController = shadeViewController; if (shadeExpansionStateManager != null) { - shadeExpansionStateManager.addExpansionListener(this); + ShadeExpansionChangeEvent currentState = + shadeExpansionStateManager.addExpansionListener(this); + onPanelExpansionChanged(currentState); } mBypassController = bypassController; mNotificationContainer = notificationContainer; @@ -488,7 +490,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb final boolean hideBouncerOverDream = mDreamOverlayStateController.isOverlayActive() && (mShadeViewController.isExpanded() - || mShadeViewController.isExpanding()); + || mShadeViewController.isExpandingOrCollapsing()); final boolean isUserTrackingStarted = event.getFraction() != EXPANSION_HIDDEN && event.getTracking(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainer.kt new file mode 100644 index 000000000000..ce88a5f5e55f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainer.kt @@ -0,0 +1,47 @@ +/* + * 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.statusbar.phone.ongoingcall + +import android.content.Context +import android.util.AttributeSet +import com.android.systemui.animation.view.LaunchableLinearLayout + +/** + * A container view for the ongoing call chip background. Needed so that we can limit the height of + * the background when the font size is very large (200%), in which case the background would go + * past the bounds of the status bar. + */ +class OngoingCallBackgroundContainer(context: Context, attrs: AttributeSet) : + LaunchableLinearLayout(context, attrs) { + + /** Sets where this view should fetch its max height from. */ + var maxHeightFetcher: (() -> Int)? = null + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val maxHeight = maxHeightFetcher?.invoke() + val chosenHeight = + if (maxHeight != null) { + // Give 1 extra px of space (without it, the background could still be cut off) + measuredHeight.coerceAtMost(maxHeight - 1) + } else { + measuredHeight + } + setMeasuredDimension(measuredWidth, chosenHeight) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index c73cc9efe85c..b3af91d9fb5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -136,6 +136,9 @@ class OngoingCallController @Inject constructor( fun setChipView(chipView: View) { tearDownChipView() this.chipView = chipView + val backgroundView: OngoingCallBackgroundContainer? = + chipView.findViewById(R.id.ongoing_call_chip_background) + backgroundView?.maxHeightFetcher = { statusBarWindowController.get().statusBarHeight } if (hasOngoingCall()) { updateChip() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt index eb20bba0d21f..991ff56e683c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt @@ -21,6 +21,7 @@ import androidx.annotation.VisibleForTesting import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoModeController @@ -62,6 +63,7 @@ import kotlinx.coroutines.flow.stateIn */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton class MobileRepositorySwitcher @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt index b1296179d7f7..e96288ab9ef9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository import android.os.Bundle import androidx.annotation.VisibleForTesting import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoModeController @@ -54,6 +55,7 @@ import kotlinx.coroutines.flow.stateIn */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton class WifiRepositorySwitcher @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java index d10d7cf8460b..3d165912a09c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -156,7 +156,7 @@ public interface BatteryController extends DemoMode, default void onWirelessChargingChanged(boolean isWirlessCharging) { } - default void onIsOverheatedChanged(boolean isOverheated) { + default void onIsBatteryDefenderChanged(boolean isBatteryDefender) { } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index 1e63b2a4b3e1..e69d86c31abc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.policy; -import static android.os.BatteryManager.BATTERY_HEALTH_OVERHEAT; -import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN; -import static android.os.BatteryManager.EXTRA_HEALTH; +import static android.os.BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE; +import static android.os.BatteryManager.CHARGING_POLICY_DEFAULT; +import static android.os.BatteryManager.EXTRA_CHARGING_STATUS; import static android.os.BatteryManager.EXTRA_PRESENT; import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS; @@ -94,7 +94,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC protected boolean mPowerSave; private boolean mAodPowerSave; private boolean mWirelessCharging; - private boolean mIsOverheated = false; + private boolean mIsBatteryDefender = false; private boolean mTestMode = false; @VisibleForTesting boolean mHasReceivedBattery = false; @@ -163,7 +163,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC pw.print(" mPluggedIn="); pw.println(mPluggedIn); pw.print(" mCharging="); pw.println(mCharging); pw.print(" mCharged="); pw.println(mCharged); - pw.print(" mIsOverheated="); pw.println(mIsOverheated); + pw.print(" mIsBatteryDefender="); pw.println(mIsBatteryDefender); pw.print(" mPowerSave="); pw.println(mPowerSave); pw.print(" mStateUnknown="); pw.println(mStateUnknown); } @@ -197,7 +197,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC cb.onPowerSaveChanged(mPowerSave); cb.onBatteryUnknownStateChanged(mStateUnknown); cb.onWirelessChargingChanged(mWirelessCharging); - cb.onIsOverheatedChanged(mIsOverheated); + cb.onIsBatteryDefenderChanged(mIsBatteryDefender); } @Override @@ -236,11 +236,11 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC fireBatteryUnknownStateChanged(); } - int batteryHealth = intent.getIntExtra(EXTRA_HEALTH, BATTERY_HEALTH_UNKNOWN); - boolean isOverheated = batteryHealth == BATTERY_HEALTH_OVERHEAT; - if (isOverheated != mIsOverheated) { - mIsOverheated = isOverheated; - fireIsOverheatedChanged(); + int chargingStatus = intent.getIntExtra(EXTRA_CHARGING_STATUS, CHARGING_POLICY_DEFAULT); + boolean isBatteryDefender = chargingStatus == CHARGING_POLICY_ADAPTIVE_LONGLIFE; + if (isBatteryDefender != mIsBatteryDefender) { + mIsBatteryDefender = isBatteryDefender; + fireIsBatteryDefenderChanged(); } fireBatteryLevelChanged(); @@ -313,8 +313,8 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS; } - public boolean isOverheated() { - return mIsOverheated; + public boolean isBatteryDefender() { + return mIsBatteryDefender; } @Override @@ -428,11 +428,11 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC } } - private void fireIsOverheatedChanged() { + private void fireIsBatteryDefenderChanged() { synchronized (mChangeCallbacks) { final int n = mChangeCallbacks.size(); for (int i = 0; i < n; i++) { - mChangeCallbacks.get(i).onIsOverheatedChanged(mIsOverheated); + mChangeCallbacks.get(i).onIsBatteryDefenderChanged(mIsBatteryDefender); } } } @@ -447,7 +447,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC String plugged = args.getString("plugged"); String powerSave = args.getString("powersave"); String present = args.getString("present"); - String overheated = args.getString("overheated"); + String defender = args.getString("defender"); if (level != null) { mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100); } @@ -462,9 +462,9 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC mStateUnknown = !present.equals("true"); fireBatteryUnknownStateChanged(); } - if (overheated != null) { - mIsOverheated = overheated.equals("true"); - fireIsOverheatedChanged(); + if (defender != null) { + mIsBatteryDefender = defender.equals("true"); + fireIsBatteryDefenderChanged(); } fireBatteryLevelChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java index 5cc3d52f4494..9b0daca2f8ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -157,6 +157,7 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa private String getDeviceString(CachedBluetoothDevice device) { return device.getName() + + " profiles=" + getDeviceProfilesString(device) + " connected=" + device.isConnected() + " active[A2DP]=" + device.isActiveDevice(BluetoothProfile.A2DP) + " active[HEADSET]=" + device.isActiveDevice(BluetoothProfile.HEADSET) @@ -164,6 +165,14 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa + " active[LE_AUDIO]=" + device.isActiveDevice(BluetoothProfile.LE_AUDIO); } + private String getDeviceProfilesString(CachedBluetoothDevice device) { + List<String> profileIds = new ArrayList<>(); + for (LocalBluetoothProfile profile : device.getProfiles()) { + profileIds.add(String.valueOf(profile.getProfileId())); + } + return "[" + String.join(",", profileIds) + "]"; + } + @Override public List<CachedBluetoothDevice> getConnectedDevices() { List<CachedBluetoothDevice> out; @@ -296,7 +305,8 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa for (CachedBluetoothDevice device : getDevices()) { isActive |= device.isActiveDevice(BluetoothProfile.HEADSET) || device.isActiveDevice(BluetoothProfile.A2DP) - || device.isActiveDevice(BluetoothProfile.HEARING_AID); + || device.isActiveDevice(BluetoothProfile.HEARING_AID) + || device.isActiveDevice(BluetoothProfile.LE_AUDIO); } if (mIsActive != isActive) { @@ -315,7 +325,8 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa boolean isConnected = device.isConnectedProfile(profile); if (profileId == BluetoothProfile.HEADSET || profileId == BluetoothProfile.A2DP - || profileId == BluetoothProfile.HEARING_AID) { + || profileId == BluetoothProfile.HEARING_AID + || profileId == BluetoothProfile.LE_AUDIO) { audioProfileConnected |= isConnected; } else { otherProfileConnected |= isConnected; diff --git a/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt index 8e2b05cd9526..b9c24871eac8 100644 --- a/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/telephony/ui/activity/SwitchToManagedProfileForCallActivity.kt @@ -22,14 +22,21 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.os.UserHandle +import android.telecom.TelecomManager import android.util.Log import android.view.WindowManager import com.android.internal.app.AlertActivity import com.android.systemui.R +import javax.inject.Inject /** Dialog shown to the user to switch to managed profile for making a call using work SIM. */ -class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.OnClickListener { +class SwitchToManagedProfileForCallActivity +@Inject +constructor( + private val telecomManager: TelecomManager?, +) : AlertActivity(), DialogInterface.OnClickListener { private lateinit var phoneNumber: Uri + private lateinit var positiveActionIntent: Intent private var managedProfileUserId = UserHandle.USER_NULL override fun onCreate(savedInstanceState: Bundle?) { @@ -37,8 +44,7 @@ class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.O WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS ) super.onCreate(savedInstanceState) - - phoneNumber = intent.getData() + phoneNumber = intent.data ?: Uri.EMPTY managedProfileUserId = intent.getIntExtra( "android.telecom.extra.MANAGED_PROFILE_USER_ID", @@ -48,11 +54,31 @@ class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.O mAlertParams.apply { mTitle = getString(R.string.call_from_work_profile_title) mMessage = getString(R.string.call_from_work_profile_text) - mPositiveButtonText = getString(R.string.call_from_work_profile_action) mNegativeButtonText = getString(R.string.call_from_work_profile_close) mPositiveButtonListener = this@SwitchToManagedProfileForCallActivity mNegativeButtonListener = this@SwitchToManagedProfileForCallActivity } + + // A dialer app may not be available in the managed profile. We try to handle that + // gracefully by redirecting the user to the app market to install a suitable app. + val defaultDialerPackageName: String? = + telecomManager?.getDefaultDialerPackage(UserHandle.of(managedProfileUserId)) + + val (intent, positiveButtonText) = + defaultDialerPackageName?.let { + Intent( + Intent.ACTION_CALL, + phoneNumber, + ) to R.string.call_from_work_profile_action + } + ?: Intent( + Intent.ACTION_VIEW, + Uri.parse(APP_STORE_DIALER_QUERY), + ) to R.string.install_dialer_on_work_profile_action + + positiveActionIntent = intent + mAlertParams.apply { mPositiveButtonText = getString(positiveButtonText) } + setupAlert() } @@ -66,7 +92,7 @@ class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.O private fun switchToManagedProfile() { try { applicationContext.startActivityAsUser( - Intent(Intent.ACTION_CALL, phoneNumber), + positiveActionIntent, ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(), UserHandle.of(managedProfileUserId) ) @@ -77,5 +103,6 @@ class SwitchToManagedProfileForCallActivity : AlertActivity(), DialogInterface.O companion object { private const val TAG = "SwitchToManagedProfileForCallActivity" + private const val APP_STORE_DIALER_QUERY = "market://search?q=dialer" } } diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 4fbbc8915c19..ab6409b5f9b3 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -21,6 +21,8 @@ import android.animation.ValueAnimator import android.content.Context import android.graphics.Rect import android.os.PowerManager +import android.os.Process +import android.os.VibrationAttributes import android.view.Gravity import android.view.MotionEvent import android.view.View @@ -226,7 +228,15 @@ constructor( maybeGetAccessibilityFocus(newInfo, currentView) // ---- Haptics ---- - newInfo.vibrationEffect?.let { vibratorHelper.vibrate(it) } + newInfo.vibrationEffect?.let { + vibratorHelper.vibrate( + Process.myUid(), + context.getApplicationContext().getPackageName(), + it, + newInfo.windowTitle, + VIBRATION_ATTRIBUTES, + ) + } } private fun maybeGetAccessibilityFocus(info: ChipbarInfo?, view: ViewGroup) { @@ -352,6 +362,11 @@ constructor( val loadingView: View, val animator: ObjectAnimator, ) + + companion object { + val VIBRATION_ATTRIBUTES: VibrationAttributes = + VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK) + } } @IdRes private val INFO_TAG = R.id.tag_chipbar_info diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java index 08dbb816b5e9..08b0c647628c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java +++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java @@ -32,11 +32,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.logging.UiEventLogger; -import com.android.settingslib.users.EditUserInfoController; -import com.android.settingslib.users.GrantAdminDialogController; +import com.android.settingslib.users.CreateUserDialogController; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.user.utils.MultiUserActionsEvent; import javax.inject.Inject; @@ -61,20 +59,18 @@ public class CreateUserActivity extends Activity { private static final String EXTRA_IS_KEYGUARD_SHOWING = "extra_is_keyguard_showing"; private final UserCreator mUserCreator; - private final EditUserInfoController mEditUserInfoController; + private CreateUserDialogController mCreateUserDialogController; private final IActivityManager mActivityManager; private final ActivityStarter mActivityStarter; private final UiEventLogger mUiEventLogger; - private Dialog mGrantAdminDialog; private Dialog mSetupUserDialog; private final OnBackInvokedCallback mBackCallback = this::onBackInvoked; - private boolean mGrantAdminRights; @Inject public CreateUserActivity(UserCreator userCreator, - EditUserInfoController editUserInfoController, IActivityManager activityManager, + CreateUserDialogController createUserDialogController, IActivityManager activityManager, ActivityStarter activityStarter, UiEventLogger uiEventLogger) { mUserCreator = userCreator; - mEditUserInfoController = editUserInfoController; + mCreateUserDialogController = createUserDialogController; mActivityManager = activityManager; mActivityStarter = activityStarter; mUiEventLogger = uiEventLogger; @@ -86,19 +82,10 @@ public class CreateUserActivity extends Activity { setShowWhenLocked(true); setContentView(R.layout.activity_create_new_user); if (savedInstanceState != null) { - mEditUserInfoController.onRestoreInstanceState(savedInstanceState); - } - boolean isKeyguardShowing = getIntent().getBooleanExtra(EXTRA_IS_KEYGUARD_SHOWING, true); - // Display grant admin dialog only on unlocked device to admin users if multiple admins - // are allowed on this device. - if (mUserCreator.isMultipleAdminEnabled() && mUserCreator.isUserAdmin() - && !isKeyguardShowing) { - mGrantAdminDialog = buildGrantAdminDialog(); - mGrantAdminDialog.show(); - } else { - mSetupUserDialog = createDialog(); - mSetupUserDialog.show(); + mCreateUserDialogController.onRestoreInstanceState(savedInstanceState); } + mSetupUserDialog = createDialog(); + mSetupUserDialog.show(); getOnBackInvokedDispatcher().registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackCallback); @@ -110,7 +97,7 @@ public class CreateUserActivity extends Activity { outState.putBundle(DIALOG_STATE_KEY, mSetupUserDialog.onSaveInstanceState()); } - mEditUserInfoController.onSaveInstanceState(outState); + mCreateUserDialogController.onSaveInstanceState(outState); super.onSaveInstanceState(outState); } @@ -125,48 +112,21 @@ public class CreateUserActivity extends Activity { private Dialog createDialog() { String defaultUserName = getString(com.android.settingslib.R.string.user_new_user_name); - - return mEditUserInfoController.createDialog( + boolean isKeyguardShowing = getIntent().getBooleanExtra(EXTRA_IS_KEYGUARD_SHOWING, true); + return mCreateUserDialogController.createDialog( this, this::startActivity, - null, - defaultUserName, - getString(com.android.settingslib.R.string.user_add_user), + (mUserCreator.isMultipleAdminEnabled() && mUserCreator.isUserAdmin() + && !isKeyguardShowing), this::addUserNow, this::finish ); } - /** - * Returns dialog that allows to grant user admin rights. - */ - private Dialog buildGrantAdminDialog() { - return new GrantAdminDialogController().createDialog( - this, - (grantAdminRights) -> { - mGrantAdminDialog.dismiss(); - mGrantAdminRights = grantAdminRights; - if (mGrantAdminRights) { - mUiEventLogger.log(MultiUserActionsEvent - .GRANT_ADMIN_FROM_USER_SWITCHER_CREATION_DIALOG); - } else { - mUiEventLogger.log(MultiUserActionsEvent - .NOT_GRANT_ADMIN_FROM_USER_SWITCHER_CREATION_DIALOG); - } - mSetupUserDialog = createDialog(); - mSetupUserDialog.show(); - }, - () -> { - mGrantAdminRights = false; - finish(); - } - ); - } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - mEditUserInfoController.onActivityResult(requestCode, resultCode, data); + mCreateUserDialogController.onActivityResult(requestCode, resultCode, data); } @Override @@ -178,9 +138,6 @@ public class CreateUserActivity extends Activity { if (mSetupUserDialog != null) { mSetupUserDialog.dismiss(); } - if (mGrantAdminDialog != null) { - mGrantAdminDialog.dismiss(); - } finish(); } @@ -190,7 +147,7 @@ public class CreateUserActivity extends Activity { super.onDestroy(); } - private void addUserNow(String userName, Drawable userIcon) { + private void addUserNow(String userName, Drawable userIcon, Boolean isAdmin) { mSetupUserDialog.dismiss(); userName = (userName == null || userName.trim().isEmpty()) ? getString(com.android.settingslib.R.string.user_new_user_name) @@ -198,7 +155,7 @@ public class CreateUserActivity extends Activity { mUserCreator.createUser(userName, userIcon, userInfo -> { - if (mGrantAdminRights) { + if (isAdmin) { mUserCreator.setUserAdmin(userInfo.id); } switchToUser(userInfo.id); @@ -230,7 +187,7 @@ public class CreateUserActivity extends Activity { */ private void startActivity(Intent intent, int requestCode) { mActivityStarter.dismissKeyguardThenExecute(() -> { - mEditUserInfoController.startingActivityForResult(); + mCreateUserDialogController.startingActivityForResult(); startActivityForResult(intent, requestCode); return true; }, /* cancel= */ null, /* afterKeyguardGone= */ true); diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index b2bf9727b534..d8ee686ea60f 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -18,6 +18,7 @@ package com.android.systemui.user; import android.os.UserHandle; +import com.android.settingslib.users.CreateUserDialogController; import com.android.settingslib.users.EditUserInfoController; import com.android.systemui.user.data.repository.UserRepositoryModule; import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule; @@ -45,6 +46,12 @@ public abstract class UserModule { return new EditUserInfoController(FILE_PROVIDER_AUTHORITY); } + /** Provides {@link CreateUserDialogController} */ + @Provides + public static CreateUserDialogController provideCreateUserDialogController() { + return new CreateUserDialogController(FILE_PROVIDER_AUTHORITY); + } + /** * Provides the {@link UserHandle} for the user associated with this System UI process. * diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index c2922c4d6f34..27c348ba329a 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -50,6 +50,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.CreateUserActivity import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.user.data.source.UserRecord @@ -455,13 +456,16 @@ constructor( UserActionModel.ADD_USER -> { uiEventLogger.log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER) val currentUser = repository.getSelectedUserInfo() - showDialog( - ShowDialogRequestModel.ShowAddUserDialog( - userHandle = currentUser.userHandle, - isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), - showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, - dialogShower = dialogShower, - ) + dismissDialog() + activityStarter.startActivity( + CreateUserActivity.createIntentForStart( + applicationContext, + keyguardInteractor.isKeyguardShowing() + ), + /* dismissShade= */ true, + /* animationController */ null, + /* showOverLockscreenWhenLocked */ true, + /* userHandle */ currentUser.getUserHandle(), ) } UserActionModel.ADD_SUPERVISED_USER -> { diff --git a/packages/SystemUI/src/com/android/systemui/util/BinderLogger.kt b/packages/SystemUI/src/com/android/systemui/util/BinderLogger.kt new file mode 100644 index 000000000000..e21f0aaf5eca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/BinderLogger.kt @@ -0,0 +1,282 @@ +/* + * 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 + +import android.os.Binder +import android.os.Binder.ProxyTransactListener +import android.os.Build +import android.os.IBinder +import android.os.StrictMode +import android.os.StrictMode.ThreadPolicy +import android.os.Trace.TRACE_TAG_APP +import android.os.Trace.asyncTraceForTrackBegin +import android.os.Trace.asyncTraceForTrackEnd +import android.util.Log +import com.android.settingslib.utils.ThreadUtils +import com.android.systemui.CoreStartable +import com.android.systemui.DejankUtils +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import javax.inject.Inject +import kotlin.random.Random.Default.nextInt + +@SysUISingleton +class BinderLogger +@Inject +constructor( + private val featureFlags: FeatureFlags, +) : CoreStartable, ProxyTransactListener { + + override fun start() { + // This feature is only allowed on "userdebug" and "eng" builds + if (Build.IS_USER) return + if (!featureFlags.isEnabled(Flags.WARN_ON_BLOCKING_BINDER_TRANSACTIONS)) return + if (DejankUtils.STRICT_MODE_ENABLED) { + Log.e( + TAG, + "Feature disabled; persist.sysui.strictmode (DejankUtils) and " + + "WARN_ON_BLOCKING_BINDER_TRANSACTIONS (BinderLogger) are incompatible" + ) + return + } + Binder.setProxyTransactListener(this) + val policyBuilder = ThreadPolicy.Builder().detectCustomSlowCalls().penaltyLog() + StrictMode.setThreadPolicy(policyBuilder.build()) + } + + override fun onTransactStarted(binder: IBinder, transactionCode: Int, flags: Int): Any? { + // Ignore one-way binder transactions + if (flags and IBinder.FLAG_ONEWAY != 0) return null + // Ignore anything not on the main thread + if (!ThreadUtils.isMainThread()) return null + + // To make it easier to debug, log the most likely cause of the blocking binder transaction + // by parsing the stack trace. + val tr = Throwable() + val analysis = BinderTransactionAnalysis.fromStackTrace(tr.stackTrace) + val traceCookie = nextInt() + asyncTraceForTrackBegin(TRACE_TAG_APP, TRACK_NAME, analysis.traceMessage, traceCookie) + if (analysis.isSystemUi) { + StrictMode.noteSlowCall(analysis.logMessage) + } else { + Log.v(TAG, analysis.logMessage, tr) + } + return traceCookie + } + + override fun onTransactStarted(binder: IBinder, transactionCode: Int): Any? { + return null + } + + override fun onTransactEnded(o: Any?) { + if (o is Int) { + asyncTraceForTrackEnd(TRACE_TAG_APP, TRACK_NAME, o) + } + } + + /** + * Class for finding the origin of a binder transaction from a stack trace and creating an error + * message that indicates 1) whether or not the call originated from System UI, and 2) the name + * of the binder transaction and where it was called. + * + * There are two types of stack traces: + * 1. Stack traces that originate from System UI, which look like the following: ``` + * android.os.BinderProxy.transact(BinderProxy.java:541) + * android.content.pm.BaseParceledListSlice.<init>(BaseParceledListSlice.java:94) + * android.content.pm.ParceledListSlice.<init>(ParceledListSlice.java:42) + * android.content.pm.ParceledListSlice.<init>(Unknown Source:0) + * android.content.pm.ParceledListSlice$1.createFromParcel(ParceledListSlice.java:80) + * android.content.pm.ParceledListSlice$1.createFromParcel(ParceledListSlice.java:78) + * android.os.Parcel.readTypedObject(Parcel.java:3982) + * android.content.pm.IPackageManager$Stub$Proxy.getInstalledPackages(IPackageManager.java:5029) + * com.android.systemui.ExampleClass.runTwoWayBinderIPC(ExampleClass.kt:343) ... ``` Most of + * these binder transactions contain a reference to a `$Stub$Proxy`, but some don't. For + * example: ``` android.os.BinderProxy.transact(BinderProxy.java:541) + * android.content.ContentProviderProxy.query(ContentProviderNative.java:479) + * android.content.ContentResolver.query(ContentResolver.java:1219) + * com.android.systemui.power.PowerUI.refreshEstimateIfNeeded(PowerUI.java:383) ``` In both + * cases, we identify the call closest to a sysui package to make the error more clear. + * 2. Stack traces that originate outside of System UI, which look like the following: ``` + * android.os.BinderProxy.transact(BinderProxy.java:541) + * android.service.notification.IStatusBarNotificationHolder$Stub$Proxy.get(IStatusBarNotificationHolder.java:121) + * android.service.notification.NotificationListenerService$NotificationListenerWrapper.onNotificationPosted(NotificationListenerService.java:1396) + * android.service.notification.INotificationListener$Stub.onTransact(INotificationListener.java:248) + * android.os.Binder.execTransactInternal(Binder.java:1285) + * android.os.Binder.execTransact(Binder.java:1244) ``` + * + * @param isSystemUi Whether or not the call originated from System UI. If it didn't, it's due + * to internal implementation details of a core framework API. + * @param cause The cause of the binder transaction + * @param binderCall The binder transaction itself + */ + private class BinderTransactionAnalysis + constructor( + val isSystemUi: Boolean, + cause: StackTraceElement?, + binderCall: StackTraceElement? + ) { + val logMessage: String + val traceMessage: String + + init { + val callName = + (if (isSystemUi) getSimpleCallRefWithFileAndLineNumber(cause) + else "${getSimpleCallRef(cause)}()") + " -> ${getBinderCallRef(binderCall)}" + logMessage = + "Blocking binder transaction detected" + + (if (!isSystemUi) ", but the call did not originate from System UI" else "") + + ": $callName" + traceMessage = "${if (isSystemUi) "sysui" else "core"}: $callName" + } + + companion object { + fun fromStackTrace(stackTrace: Array<StackTraceElement>): BinderTransactionAnalysis { + if (stackTrace.size < 2) { + return BinderTransactionAnalysis(false, null, null) + } + var previousStackElement: StackTraceElement = stackTrace.first() + + // The stack element corresponding to the binder transaction. For example: + // `android.content.pm.IPackageManager$Stub$Proxy.getInstalledPackages(IPackageManager.java:50)` + var binderTransaction: StackTraceElement? = null + // The stack element that caused the binder transaction in the first place. + // For example: + // `com.android.systemui.ExampleClass.runTwoWayBinderIPC(ExampleClass.kt:343)` + var causeOfBinderTransaction: StackTraceElement? = null + + // Iterate starting from the top of the stack (BinderProxy.transact). + for (i in 1 until stackTrace.size) { + val stackElement = stackTrace[i] + if (previousStackElement.className?.endsWith("\$Stub\$Proxy") == true) { + binderTransaction = previousStackElement + causeOfBinderTransaction = stackElement + } + // As a heuristic, find the top-most call from a sysui package (besides this + // class) and blame it + val className = stackElement.className + if ( + className != BinderLogger::class.java.name && + (className.startsWith(SYSUI_PKG) || className.startsWith(KEYGUARD_PKG)) + ) { + causeOfBinderTransaction = stackElement + + return BinderTransactionAnalysis( + true, + causeOfBinderTransaction, + // If an IInterface.Stub.Proxy-like call has not been identified yet, + // blame the previous call in the stack. + binderTransaction ?: previousStackElement + ) + } + previousStackElement = stackElement + } + // If we never found a call originating from sysui, report it with an explanation + // that we could not find a culprit in sysui. + return BinderTransactionAnalysis(false, causeOfBinderTransaction, binderTransaction) + } + } + } + + companion object { + private const val TAG: String = "SystemUIBinder" + private const val TRACK_NAME = "Blocking Binder Transactions" + private const val SYSUI_PKG = "com.android.systemui" + private const val KEYGUARD_PKG = "com.android.keyguard" + private const val UNKNOWN = "<unknown>" + + /** + * Start of the source file for any R8 build within AOSP. + * + * TODO(b/213833843): Allow configuration of the prefix via a build variable. + */ + private const val AOSP_SOURCE_FILE_MARKER = "go/retraceme " + + /** Start of the source file for any R8 compiler build. */ + private const val R8_SOURCE_FILE_MARKER = "R8_" + + /** + * Returns a short string for a [StackTraceElement] that references a binder transaction. + * For example, a stack frame of + * `android.content.pm.IPackageManager$Stub$Proxy.getInstalledPackages(IPackageManager.java:50)` + * would return `android.content.pm.IPackageManager#getInstalledPackages()` + */ + private fun getBinderCallRef(stackFrame: StackTraceElement?): String = + if (stackFrame != null) "${getBinderClassName(stackFrame)}#${stackFrame.methodName}()" + else UNKNOWN + + /** + * Returns the class name of a [StackTraceElement], removing any `$Stub$Proxy` suffix, but + * still including the package name. This makes binder class names more readable. For + * example, for a stack element of + * `android.content.pm.IPackageManager$Stub$Proxy.getInstalledPackages(IPackageManager.java:50)` + * this would return `android.content.pm.IPackageManager` + */ + private fun getBinderClassName(stackFrame: StackTraceElement): String { + val className = stackFrame.className + val stubDefIndex = className.indexOf("\$Stub\$Proxy") + return if (stubDefIndex > 0) className.substring(0, stubDefIndex) else className + } + + /** + * Returns a short string for a [StackTraceElement], including the file name and line + * number. + * + * If the source file needs retracing, this falls back to the default `toString()` method so + * that it can be read by the `retrace` command-line tool (`m retrace`). + */ + private fun getSimpleCallRefWithFileAndLineNumber(stackFrame: StackTraceElement?): String = + if (stackFrame != null) { + with(stackFrame) { + if ( + fileName == null || + fileName.startsWith(AOSP_SOURCE_FILE_MARKER) || + fileName.startsWith(R8_SOURCE_FILE_MARKER) + ) { + // If the source file needs retracing, use the default toString() method for + // compatibility with the retrace command-line tool + "at $stackFrame" + } else { + "at ${getSimpleCallRef(stackFrame)}($fileName:$lineNumber)" + } + } + } else UNKNOWN + + /** + * Returns a short string for a [StackTraceElement] including it's class name and method + * name. For example, if the stack frame is + * `com.android.systemui.ExampleController.makeBlockingBinderIPC(ExampleController.kt:343)` + * this would return `ExampleController#makeBlockingBinderIPC()` + */ + private fun getSimpleCallRef(stackFrame: StackTraceElement?): String = + if (stackFrame != null) "${getSimpleClassName(stackFrame)}#${stackFrame.methodName}" + else UNKNOWN + + /** + * Returns a short string for a [StackTraceElement] including just its class name without + * any package name. For example, if the stack frame is + * `com.android.systemui.ExampleController.makeBlockingBinderIPC(ExampleController.kt:343)` + * this would return `ExampleController` + */ + private fun getSimpleClassName(stackFrame: StackTraceElement): String = + try { + with(Class.forName(stackFrame.className)) { + canonicalName?.substring(packageName.length + 1) + } + } finally {} ?: stackFrame.className + } +} diff --git a/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt new file mode 100644 index 000000000000..038fddc1f7a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt @@ -0,0 +1,16 @@ +package com.android.systemui.utils + +import android.view.WindowManagerGlobal +import javax.inject.Inject + +/** Interface to talk to [WindowManagerGlobal] */ +class GlobalWindowManager @Inject constructor() { + /** + * Sends a trim memory command to [WindowManagerGlobal]. + * + * @param level One of levels from [ComponentCallbacks2] starting with TRIM_ + */ + fun trimMemory(level: Int) { + WindowManagerGlobal.getInstance().trimMemory(level) + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java index 5557efa97e3e..48a8d1bc90d7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java @@ -21,6 +21,8 @@ import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE; import static android.telephony.SubscriptionManager.DATA_ROAMING_ENABLE; import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertTrue; @@ -30,13 +32,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.provider.Settings; import android.telephony.ServiceState; @@ -47,8 +54,10 @@ import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.text.TextUtils; +import com.android.keyguard.logging.CarrierTextManagerLogger; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dump.LogBufferHelperKt; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository; import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel; @@ -110,6 +119,10 @@ public class CarrierTextManagerTest extends SysuiTestCase { private CarrierTextManager mCarrierTextManager; + private CarrierTextManagerLogger mLogger = + new CarrierTextManagerLogger( + LogBufferHelperKt.logcatLogBuffer("CarrierTextManagerLog")); + private Void checkMainThread(InvocationOnMock inv) { assertThat(mMainExecutor.isExecuting()).isTrue(); assertThat(mBgExecutor.isExecuting()).isFalse(); @@ -144,7 +157,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { mCarrierTextManager = new CarrierTextManager.Builder( mContext, mContext.getResources(), mWifiRepository, mTelephonyManager, mTelephonyListenerManager, mWakefulnessLifecycle, mMainExecutor, - mBgExecutor, mKeyguardUpdateMonitor) + mBgExecutor, mKeyguardUpdateMonitor, mLogger) .setShowAirplaneMode(true) .setShowMissingSim(true) .build(); @@ -183,6 +196,47 @@ public class CarrierTextManagerTest extends SysuiTestCase { assertEquals(AIRPLANE_MODE_TEXT, captor.getValue().carrierText); } + /** regression test for b/281706473, caused by sending NULL plmn / spn to the logger */ + @Test + public void testAirplaneMode_noSim_nullPlmn_nullSpn_doesNotCrash() { + // GIVEN - sticy broadcast that returns a null PLMN and null SPN + Intent stickyIntent = new Intent(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED); + stickyIntent.putExtra(TelephonyManager.EXTRA_SHOW_PLMN, true); + stickyIntent.removeExtra(TelephonyManager.EXTRA_PLMN); + stickyIntent.putExtra(TelephonyManager.EXTRA_SHOW_SPN, true); + stickyIntent.removeExtra(TelephonyManager.EXTRA_SPN); + + mCarrierTextManager = new CarrierTextManager.Builder( + getContextSpyForStickyBroadcast(stickyIntent), + mContext.getResources(), + mWifiRepository, + mTelephonyManager, + mTelephonyListenerManager, + mWakefulnessLifecycle, + mMainExecutor, + mBgExecutor, + mKeyguardUpdateMonitor, + mLogger + ) + .setShowAirplaneMode(true) + .setShowMissingSim(true) + .build(); + + // GIVEN - airplane mode is off (causing CTM to fetch the sticky broadcast) + Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); + reset(mCarrierTextCallback); + List<SubscriptionInfo> list = new ArrayList<>(); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); + when(mKeyguardUpdateMonitor.getSimState(0)) + .thenReturn(TelephonyManager.SIM_STATE_NOT_READY); + mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); + + // WHEN CTM fetches the broadcast and attempts to log the result, no crash results + mCarrierTextManager.updateCarrierText(); + + // No assert, this test should not crash + } + @Test public void testCardIOError() { reset(mCarrierTextCallback); @@ -506,4 +560,10 @@ public class CarrierTextManagerTest extends SysuiTestCase { assertEquals(TEST_CARRIER + SEPARATOR + TEST_CARRIER, captor.getValue().carrierText); } + + private Context getContextSpyForStickyBroadcast(Intent returnVal) { + Context contextSpy = spy(mContext); + doReturn(returnVal).when(contextSpy).registerReceiver(eq(null), any(IntentFilter.class)); + return contextSpy; + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java index 1ba9931e1bc8..ae3a320cde54 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java @@ -40,6 +40,8 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingCollectorFake; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -77,6 +79,7 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { @Mock private EmergencyButtonController mEmergencyButtonController; + private FakeFeatureFlags mFeatureFlags; private KeyguardAbsKeyInputViewController mKeyguardAbsKeyInputViewController; @Before @@ -90,10 +93,18 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { when(mAbsKeyInputView.requireViewById(R.id.bouncer_message_area)) .thenReturn(mKeyguardMessageArea); when(mAbsKeyInputView.getResources()).thenReturn(getContext().getResources()); - mKeyguardAbsKeyInputViewController = new KeyguardAbsKeyInputViewController(mAbsKeyInputView, + mFeatureFlags = new FakeFeatureFlags(); + mFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, false); + mKeyguardAbsKeyInputViewController = createTestObject(); + mKeyguardAbsKeyInputViewController.init(); + reset(mKeyguardMessageAreaController); // Clear out implicit call to init. + } + + private KeyguardAbsKeyInputViewController createTestObject() { + return new KeyguardAbsKeyInputViewController(mAbsKeyInputView, mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mFalsingCollector, - mEmergencyButtonController) { + mEmergencyButtonController, mFeatureFlags) { @Override void resetState() { } @@ -108,8 +119,16 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { return 0; } }; - mKeyguardAbsKeyInputViewController.init(); - reset(mKeyguardMessageAreaController); // Clear out implicit call to init. + } + + @Test + public void withFeatureFlagOn_oldMessage_isHidden() { + mFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true); + KeyguardAbsKeyInputViewController underTest = createTestObject(); + + underTest.init(); + + verify(mKeyguardMessageAreaController).disable(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index d8e2a3842e85..fb738454fc71 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -134,7 +134,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private KeyguardClockSwitchController mController; private View mSliceView; - private LinearLayout mStatusArea; private FakeExecutor mExecutor; @Before @@ -196,8 +195,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mSliceView = new View(getContext()); when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView); - mStatusArea = new LinearLayout(getContext()); - when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(mStatusArea); + when(mView.findViewById(R.id.keyguard_status_area)).thenReturn( + new LinearLayout(getContext())); } @Test @@ -402,15 +401,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { assertNull(mController.getClock()); } - @Test - public void testSetAlpha_setClockAlphaForCLockFace() { - mController.onViewAttached(); - mController.setAlpha(0.5f); - verify(mLargeClockView).setAlpha(0.5f); - verify(mSmallClockView).setAlpha(0.5f); - assertEquals(0.5f, mStatusArea.getAlpha(), 0.0f); - } - private void verifyAttachment(VerificationMode times) { verify(mClockRegistry, times).registerClockChangeListener( any(ClockRegistry.ClockChangeListener.class)); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java index 30e3d09299f2..b3496967f525 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.hardware.display.DisplayManagerGlobal; import android.testing.AndroidTestingRunner; @@ -38,6 +39,7 @@ import com.android.keyguard.dagger.KeyguardStatusViewComponent; import com.android.systemui.SysuiTestCase; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.settings.FakeDisplayTracker; +import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.Before; import org.junit.Test; @@ -58,6 +60,10 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory; @Mock private KeyguardDisplayManager.KeyguardPresentation mKeyguardPresentation; + @Mock + private KeyguardDisplayManager.DeviceStateHelper mDeviceStateHelper; + @Mock + private KeyguardStateController mKeyguardStateController; private Executor mMainExecutor = Runnable::run; private Executor mBackgroundExecutor = Runnable::run; @@ -76,7 +82,7 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mManager = spy(new KeyguardDisplayManager(mContext, () -> mNavigationBarController, mKeyguardStatusViewComponentFactory, mDisplayTracker, mMainExecutor, - mBackgroundExecutor)); + mBackgroundExecutor, mDeviceStateHelper, mKeyguardStateController)); doReturn(mKeyguardPresentation).when(mManager).createPresentation(any()); mDefaultDisplay = new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY, @@ -123,4 +129,13 @@ public class KeyguardDisplayManagerTest extends SysuiTestCase { mManager.show(); verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay)); } + + @Test + public void testShow_concurrentDisplayActive_occluded() { + mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay}); + + when(mDeviceStateHelper.isConcurrentDisplayActive(mSecondaryDisplay)).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(true); + verify(mManager, never()).createPresentation(eq(mSecondaryDisplay)); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java index a35e5b59f765..d4522d003d52 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java @@ -27,6 +27,8 @@ import static org.mockito.Mockito.when; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.text.Editable; +import android.text.TextWatcher; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -93,16 +95,16 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase { } @Test - public void testSetMessage_AnnounceForAccessibility() { - ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class); - when(mKeyguardMessageArea.getText()).thenReturn("abc"); - mMessageAreaController.setMessage("abc"); + public void textChanged_AnnounceForAccessibility() { + ArgumentCaptor<TextWatcher> textWatcherArgumentCaptor = ArgumentCaptor.forClass( + TextWatcher.class); + mMessageAreaController.onViewAttached(); + verify(mKeyguardMessageArea).addTextChangedListener(textWatcherArgumentCaptor.capture()); - verify(mKeyguardMessageArea).setMessage("abc", /* animate= */ true); + textWatcherArgumentCaptor.getValue().afterTextChanged( + Editable.Factory.getInstance().newEditable("abc")); verify(mKeyguardMessageArea).removeCallbacks(any(Runnable.class)); - verify(mKeyguardMessageArea).postDelayed(argumentCaptor.capture(), anyLong()); - argumentCaptor.getValue().run(); - verify(mKeyguardMessageArea).announceForAccessibility("abc"); + verify(mKeyguardMessageArea).postDelayed(any(Runnable.class), anyLong()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index 082c8ccd9657..1a9260c2ede6 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -26,6 +26,8 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.util.concurrency.DelayableExecutor import org.junit.Before import org.junit.Test @@ -76,6 +78,8 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry)) .thenReturn(passwordEntry) `when`(keyguardPasswordView.resources).thenReturn(context.resources) + val fakeFeatureFlags = FakeFeatureFlags() + fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) keyguardPasswordViewController = KeyguardPasswordViewController( keyguardPasswordView, @@ -90,7 +94,8 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { mainExecutor, mContext.resources, falsingCollector, - keyguardViewController) + keyguardViewController, + fakeFeatureFlags) } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index a8d5569a1b98..71a57c78e226 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -26,6 +26,8 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.statusbar.policy.DevicePostureController import org.junit.Before import org.junit.Test @@ -72,6 +74,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { @Mock private lateinit var mPostureController: DevicePostureController private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController + private lateinit var fakeFeatureFlags: FakeFeatureFlags @Before fun setup() { @@ -86,6 +89,8 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { `when`(mKeyguardMessageAreaControllerFactory.create(mKeyguardMessageArea)) .thenReturn(mKeyguardMessageAreaController) `when`(mKeyguardPatternView.resources).thenReturn(context.resources) + fakeFeatureFlags = FakeFeatureFlags() + fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, false) mKeyguardPatternViewController = KeyguardPatternViewController( mKeyguardPatternView, @@ -97,7 +102,17 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { mFalsingCollector, mEmergencyButtonController, mKeyguardMessageAreaControllerFactory, - mPostureController) + mPostureController, + fakeFeatureFlags) + } + + @Test + fun withFeatureFlagOn_oldMessage_isHidden() { + fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + + mKeyguardPatternViewController.init() + + verify<KeyguardMessageAreaController<*>>(mKeyguardMessageAreaController).disable() } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index 0881e61e4c96..cf86c2192352 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -36,6 +36,8 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.SingleTapClassifier; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -98,10 +100,13 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { .thenReturn(mDeleteButton); when(mPinBasedInputView.findViewById(R.id.key_enter)) .thenReturn(mOkButton); + FakeFeatureFlags featureFlags = new FakeFeatureFlags(); + featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true); + mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView, mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener, - mEmergencyButtonController, mFalsingCollector) { + mEmergencyButtonController, mFalsingCollector, featureFlags) { @Override public void onResume(int reason) { super.onResume(reason); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 70476aa088dc..d3b41902499c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -152,9 +152,15 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { `when`(passwordTextView.text).thenReturn("") pinViewController.startAppearAnimation() - verify(deleteButton).visibility = View.INVISIBLE + verify(deleteButton).visibility = View.VISIBLE verify(enterButton).visibility = View.VISIBLE verify(passwordTextView).setUsePinShapes(true) - verify(passwordTextView).setIsPinHinting(true) + verify(passwordTextView).setIsPinHinting(false) + } + + @Test + fun handleLockout_readsNumberOfErrorAttempts() { + pinViewController.handleAttemptLockout(0) + verify(lockPatternUtils).getCurrentFailedPasswordAttempts(anyInt()) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index 65ddb53f748b..d2e5a45c3a93 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -18,6 +18,7 @@ package com.android.keyguard; import static com.android.keyguard.KeyguardSecurityContainer.MODE_DEFAULT; import static com.android.keyguard.KeyguardSecurityContainer.MODE_ONE_HANDED; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE; import static com.google.common.truth.Truth.assertThat; @@ -63,7 +64,9 @@ import com.android.systemui.biometrics.SideFpsController; import com.android.systemui.biometrics.SideFpsUiRequestSource; import com.android.systemui.classifier.FalsingA11yDelegate; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.ActivityStarter; @@ -195,11 +198,15 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { when(mKeyguardPasswordView.getWindowInsetsController()).thenReturn(mWindowInsetsController); when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(SecurityMode.PIN); when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); + FakeFeatureFlags featureFlags = new FakeFeatureFlags(); + featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true); + mKeyguardPasswordViewController = new KeyguardPasswordViewController( (KeyguardPasswordView) mKeyguardPasswordView, mKeyguardUpdateMonitor, SecurityMode.Password, mLockPatternUtils, null, mKeyguardMessageAreaControllerFactory, null, null, mEmergencyButtonController, - null, mock(Resources.class), null, mKeyguardViewController); + null, mock(Resources.class), null, mKeyguardViewController, + featureFlags); mKeyguardSecurityContainerController = new KeyguardSecurityContainerController( mView, mAdminSecondaryLockScreenControllerFactory, mLockPatternUtils, @@ -685,6 +692,14 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER); } + @Test + public void setExpansion_setsAlpha() { + mKeyguardSecurityContainerController.setExpansion(EXPANSION_VISIBLE); + + verify(mView).setAlpha(1f); + verify(mView).setTranslationY(0f); + } + private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() { mKeyguardSecurityContainerController.onViewAttached(); verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index eb86c0590018..a3acc781f2a7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt @@ -27,6 +27,8 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import org.junit.Before import org.junit.Test @@ -71,6 +73,9 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { simPinView = LayoutInflater.from(context).inflate(R.layout.keyguard_sim_pin_view, null) as KeyguardSimPinView + val fakeFeatureFlags = FakeFeatureFlags() + fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + underTest = KeyguardSimPinViewController( simPinView, @@ -83,7 +88,8 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { liftToActivateListener, telephonyManager, falsingCollector, - emergencyButtonController + emergencyButtonController, + fakeFeatureFlags, ) underTest.init() } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index 2dcca55b9318..efcf4ddb5c71 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt @@ -27,6 +27,8 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import org.junit.Before import org.junit.Test @@ -70,6 +72,9 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { simPukView = LayoutInflater.from(context).inflate(R.layout.keyguard_sim_puk_view, null) as KeyguardSimPukView + val fakeFeatureFlags = FakeFeatureFlags() + fakeFeatureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + underTest = KeyguardSimPukViewController( simPukView, @@ -82,7 +87,8 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { liftToActivateListener, telephonyManager, falsingCollector, - emergencyButtonController + emergencyButtonController, + fakeFeatureFlags, ) underTest.init() } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt index a8c281c24700..f8262d43cdbb 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt @@ -24,46 +24,33 @@ class KeyguardStatusViewTest : SysuiTestCase() { get() = keyguardStatusView.findViewById(R.id.status_view_media_container) private val statusViewContainer: ViewGroup get() = keyguardStatusView.findViewById(R.id.status_view_container) - private val clockView: ViewGroup - get() = keyguardStatusView.findViewById(R.id.keyguard_clock_container) private val childrenExcludingMedia get() = statusViewContainer.children.filter { it != mediaView } @Before fun setUp() { - keyguardStatusView = LayoutInflater.from(context) - .inflate(R.layout.keyguard_status_view, /* root= */ null) as KeyguardStatusView + keyguardStatusView = + LayoutInflater.from(context).inflate(R.layout.keyguard_status_view, /* root= */ null) + as KeyguardStatusView } @Test fun setChildrenTranslationYExcludingMediaView_mediaViewIsNotTranslated() { val translationY = 1234f - keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */true) + keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */ true) assertThat(mediaView.translationY).isEqualTo(0) - childrenExcludingMedia.forEach { - assertThat(it.translationY).isEqualTo(translationY) - } + childrenExcludingMedia.forEach { assertThat(it.translationY).isEqualTo(translationY) } } @Test fun setChildrenTranslationYIncludeMediaView() { val translationY = 1234f - keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */false) + keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */ false) - statusViewContainer.children.forEach { - assertThat(it.translationY).isEqualTo(translationY) - } - } - - @Test - fun setAlphaExcludeClock() { - keyguardStatusView.setAlpha(0.5f, /* excludeClock= */true) - assertThat(statusViewContainer.alpha).isNotEqualTo(0.5f) - assertThat(mediaView.alpha).isEqualTo(0.5f) - assertThat(clockView.alpha).isNotEqualTo(0.5f) + statusViewContainer.children.forEach { assertThat(it.translationY).isEqualTo(translationY) } } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index d867df59b786..2f72cb95db98 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -3088,7 +3088,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { private Intent getBatteryIntent() { return new Intent(Intent.ACTION_BATTERY_CHANGED).putExtra( - BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT); + BatteryManager.EXTRA_CHARGING_STATUS, + BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE); } private class TestableKeyguardUpdateMonitor extends KeyguardUpdateMonitor { diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt deleted file mode 100644 index babbe451dd6a..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt +++ /dev/null @@ -1,215 +0,0 @@ -package com.android.systemui - -import android.content.ComponentName -import android.content.Context -import android.content.pm.PackageManager -import android.content.pm.UserInfo -import android.content.res.Resources -import android.testing.AndroidTestingRunner -import androidx.test.filters.SmallTest -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flag -import com.android.systemui.flags.FlagListenable -import com.android.systemui.flags.Flags -import com.android.systemui.flags.ReleasedFlag -import com.android.systemui.flags.UnreleasedFlag -import com.android.systemui.settings.UserTracker -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.kotlinArgumentCaptor -import com.android.systemui.util.mockito.whenever -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.test.TestCoroutineDispatcher -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.clearInvocations -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.MockitoAnnotations - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(AndroidTestingRunner::class) -@SmallTest -class ChooserSelectorTest : SysuiTestCase() { - - private val flagListener = kotlinArgumentCaptor<FlagListenable.Listener>() - - private val testDispatcher = TestCoroutineDispatcher() - private val testScope = CoroutineScope(testDispatcher) - - private lateinit var chooserSelector: ChooserSelector - - @Mock private lateinit var mockContext: Context - @Mock private lateinit var mockProfileContext: Context - @Mock private lateinit var mockUserTracker: UserTracker - @Mock private lateinit var mockPackageManager: PackageManager - @Mock private lateinit var mockResources: Resources - @Mock private lateinit var mockFeatureFlags: FeatureFlags - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - - whenever(mockContext.createContextAsUser(any(), anyInt())).thenReturn(mockProfileContext) - whenever(mockContext.resources).thenReturn(mockResources) - whenever(mockProfileContext.packageManager).thenReturn(mockPackageManager) - whenever(mockResources.getString(anyInt())).thenReturn( - ComponentName("TestPackage", "TestClass").flattenToString()) - whenever(mockUserTracker.userProfiles).thenReturn(listOf(UserInfo(), UserInfo())) - - chooserSelector = ChooserSelector( - mockContext, - mockUserTracker, - mockFeatureFlags, - testScope, - testDispatcher, - ) - } - - @After - fun tearDown() { - testDispatcher.cleanupTestCoroutines() - } - - @Test - fun initialize_registersFlagListenerUntilScopeCancelled() { - // Arrange - - // Act - chooserSelector.start() - - // Assert - verify(mockFeatureFlags).addListener( - eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), - flagListener.capture(), - ) - verify(mockFeatureFlags, never()).removeListener(any()) - - // Act - testScope.cancel() - - // Assert - verify(mockFeatureFlags).removeListener(eq(flagListener.value)) - } - - @Test - fun initialize_enablesUnbundledChooser_whenFlagEnabled() { - // Arrange - setFlagMock(true) - - // Act - chooserSelector.start() - - // Assert - verify(mockPackageManager, times(2)).setComponentEnabledSetting( - eq(ComponentName("TestPackage", "TestClass")), - eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), - anyInt(), - ) - } - - @Test - fun initialize_disablesUnbundledChooser_whenFlagDisabled() { - // Arrange - setFlagMock(false) - - // Act - chooserSelector.start() - - // Assert - verify(mockPackageManager, times(2)).setComponentEnabledSetting( - eq(ComponentName("TestPackage", "TestClass")), - eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), - anyInt(), - ) - } - - @Test - fun enablesUnbundledChooser_whenFlagBecomesEnabled() { - // Arrange - setFlagMock(false) - chooserSelector.start() - verify(mockFeatureFlags).addListener( - eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), - flagListener.capture(), - ) - verify(mockPackageManager, never()).setComponentEnabledSetting( - any(), - eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), - anyInt(), - ) - - // Act - setFlagMock(true) - flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name)) - - // Assert - verify(mockPackageManager, times(2)).setComponentEnabledSetting( - eq(ComponentName("TestPackage", "TestClass")), - eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), - anyInt(), - ) - } - - @Test - fun disablesUnbundledChooser_whenFlagBecomesDisabled() { - // Arrange - setFlagMock(true) - chooserSelector.start() - verify(mockFeatureFlags).addListener( - eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), - flagListener.capture(), - ) - verify(mockPackageManager, never()).setComponentEnabledSetting( - any(), - eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), - anyInt(), - ) - - // Act - setFlagMock(false) - flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name)) - - // Assert - verify(mockPackageManager, times(2)).setComponentEnabledSetting( - eq(ComponentName("TestPackage", "TestClass")), - eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), - anyInt(), - ) - } - - @Test - fun doesNothing_whenAnotherFlagChanges() { - // Arrange - setFlagMock(false) - chooserSelector.start() - verify(mockFeatureFlags).addListener( - eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), - flagListener.capture(), - ) - clearInvocations(mockPackageManager) - - // Act - flagListener.value.onFlagChanged(TestFlagEvent("other flag")) - - // Assert - verifyZeroInteractions(mockPackageManager) - } - - private fun setFlagMock(enabled: Boolean) { - whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(enabled) - whenever(mockFeatureFlags.isEnabled(any<ReleasedFlag>())).thenReturn(enabled) - } - - private class TestFlagEvent(override val flagName: String) : FlagListenable.FlagEvent { - override fun requestNoRestart() {} - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java index eff8c019efb4..f64db78f4941 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java @@ -80,6 +80,8 @@ public class IWindowMagnificationConnectionTest extends SysuiTestCase { private OverviewProxyService mOverviewProxyService; @Mock private SecureSettings mSecureSettings; + @Mock + private AccessibilityLogger mA11yLogger; private IWindowMagnificationConnection mIWindowMagnificationConnection; private WindowMagnification mWindowMagnification; @@ -97,7 +99,7 @@ public class IWindowMagnificationConnectionTest extends SysuiTestCase { mWindowMagnification = new WindowMagnification(getContext(), getContext().getMainThreadHandler(), mCommandQueue, mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings, - mDisplayTracker, getContext().getSystemService(DisplayManager.class)); + mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger); mWindowMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier( mContext.getSystemService(DisplayManager.class)); mWindowMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier( diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java index 30cbc5242a81..665246bd7f7d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java @@ -17,7 +17,6 @@ package com.android.systemui.accessibility; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; -import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @@ -74,10 +73,9 @@ public class MagnificationSettingsControllerTest extends SysuiTestCase { @Test public void testShowSettingsPanel() { - final int mode = ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; - mMagnificationSettingsController.showMagnificationSettings(mode); + mMagnificationSettingsController.showMagnificationSettings(); - verify(mWindowMagnificationSettings).showSettingPanel(eq(mode)); + verify(mWindowMagnificationSettings).showSettingPanel(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java index c08b5b47cb06..ce96708039ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java @@ -16,7 +16,10 @@ package com.android.systemui.accessibility; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static com.google.common.truth.Truth.assertThat; @@ -24,12 +27,17 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.annotation.IdRes; import android.content.Context; import android.content.pm.ActivityInfo; +import android.database.ContentObserver; +import android.os.UserHandle; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -102,7 +110,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void showSettingPanel_hasAccessibilityWindowTitle() { - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); final WindowManager.LayoutParams layoutPrams = mWindowManager.getLayoutParamsFromAttachedView(); @@ -114,7 +125,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void showSettingPanel_windowMode_showEditButtonAndDiagonalView() { - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); final Button editButton = getInternalView(R.id.magnifier_edit_button); assertEquals(editButton.getVisibility(), View.VISIBLE); @@ -125,7 +139,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void showSettingPanel_fullScreenMode_hideEditButtonAndDiagonalView() { - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + mWindowMagnificationSettings.showSettingPanel(); final Button editButton = getInternalView(R.id.magnifier_edit_button); assertEquals(editButton.getVisibility(), View.INVISIBLE); @@ -135,9 +152,22 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { } @Test + public void showSettingPanel_windowOnlyCapability_hideFullscreenButton() { + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); + + final View fullscreenButton = getInternalView(R.id.magnifier_full_button); + assertThat(fullscreenButton.getVisibility()).isEqualTo(View.GONE); + } + + @Test public void performClick_smallSizeButton_changeMagnifierSizeSmallAndSwitchToWindowMode() { - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); verifyOnSetMagnifierSizeAndOnModeSwitch( R.id.magnifier_small_button, MAGNIFICATION_SIZE_SMALL); @@ -145,8 +175,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void performClick_mediumSizeButton_changeMagnifierSizeMediumAndSwitchToWindowMode() { - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); verifyOnSetMagnifierSizeAndOnModeSwitch( R.id.magnifier_medium_button, MAGNIFICATION_SIZE_MEDIUM); @@ -154,8 +186,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void performClick_largeSizeButton_changeMagnifierSizeLargeAndSwitchToWindowMode() { - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); verifyOnSetMagnifierSizeAndOnModeSwitch( R.id.magnifier_large_button, MAGNIFICATION_SIZE_LARGE); @@ -178,8 +212,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { View fullScreenModeButton = getInternalView(R.id.magnifier_full_button); getInternalView(R.id.magnifier_panel_view); - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); // Perform click fullScreenModeButton.performClick(); @@ -192,8 +228,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { public void performClick_editButton_setEditMagnifierSizeMode() { View editButton = getInternalView(R.id.magnifier_edit_button); - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); // Perform click editButton.performClick(); @@ -208,8 +246,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { getInternalView(R.id.magnifier_horizontal_lock_switch); final boolean currentCheckedState = diagonalScrollingSwitch.isChecked(); - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); // Perform click diagonalScrollingSwitch.performClick(); @@ -219,8 +259,10 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { @Test public void onConfigurationChanged_selectedButtonIsStillSelected() { - // Open view - mWindowMagnificationSettings.showSettingPanel(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + mWindowMagnificationSettings.showSettingPanel(); View magnifierMediumButton = getInternalView(R.id.magnifier_medium_button); magnifierMediumButton.performClick(); @@ -232,9 +274,46 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { assertThat(magnifierMediumButton.isSelected()).isTrue(); } + @Test + public void showSettingsPanel_observerRegistered() { + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + + mWindowMagnificationSettings.showSettingPanel(); + + verify(mSecureSettings).registerContentObserverForUser( + eq(ACCESSIBILITY_MAGNIFICATION_CAPABILITY), + any(ContentObserver.class), + eq(UserHandle.USER_CURRENT)); + } + + @Test + public void hideSettingsPanel_observerUnregistered() { + setupMagnificationCapabilityAndMode( + /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, + /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); + + mWindowMagnificationSettings.showSettingPanel(); + mWindowMagnificationSettings.hideSettingPanel(); + + verify(mSecureSettings).unregisterContentObserver(any(ContentObserver.class)); + } + private <T extends View> T getInternalView(@IdRes int idRes) { T view = mSettingView.findViewById(idRes); assertNotNull(view); return view; } + + private void setupMagnificationCapabilityAndMode(int capability, int mode) { + when(mSecureSettings.getIntForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, + ACCESSIBILITY_MAGNIFICATION_MODE_NONE, + UserHandle.USER_CURRENT)).thenReturn(capability); + when(mSecureSettings.getIntForUser( + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, + ACCESSIBILITY_MAGNIFICATION_MODE_NONE, + UserHandle.USER_CURRENT)).thenReturn(mode); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java index 239b5bd39430..38ecec0b4602 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java @@ -19,6 +19,7 @@ package com.android.systemui.accessibility; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; +import static com.android.systemui.accessibility.AccessibilityLogger.MagnificationSettingsEvent; import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; import static com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; @@ -89,6 +90,8 @@ public class WindowMagnificationTest extends SysuiTestCase { private WindowMagnificationController mWindowMagnificationController; @Mock private MagnificationSettingsController mMagnificationSettingsController; + @Mock + private AccessibilityLogger mA11yLogger; @Before public void setUp() throws Exception { @@ -103,11 +106,22 @@ public class WindowMagnificationTest extends SysuiTestCase { when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); + doAnswer(invocation -> { + mWindowMagnification.mMagnificationSettingsControllerCallback + .onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ true); + return null; + }).when(mMagnificationSettingsController).showMagnificationSettings(); + doAnswer(invocation -> { + mWindowMagnification.mMagnificationSettingsControllerCallback + .onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ false); + return null; + }).when(mMagnificationSettingsController).closeMagnificationSettings(); + mCommandQueue = new CommandQueue(getContext(), mDisplayTracker); mWindowMagnification = new WindowMagnification(getContext(), getContext().getMainThreadHandler(), mCommandQueue, mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings, mDisplayTracker, - getContext().getSystemService(DisplayManager.class)); + getContext().getSystemService(DisplayManager.class), mA11yLogger); mWindowMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier( mContext.getSystemService(DisplayManager.class), mWindowMagnificationController); mWindowMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier( @@ -184,8 +198,9 @@ public class WindowMagnificationTest extends SysuiTestCase { mWindowMagnification.mWindowMagnifierCallback.onClickSettingsButton(TEST_DISPLAY); waitForIdleSync(); - verify(mMagnificationSettingsController).showMagnificationSettings( - eq(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW)); + verify(mMagnificationSettingsController).showMagnificationSettings(); + verify(mA11yLogger).log( + eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_PANEL_OPENED)); } @Test @@ -196,6 +211,8 @@ public class WindowMagnificationTest extends SysuiTestCase { waitForIdleSync(); verify(mWindowMagnificationController).changeMagnificationSize(eq(index)); + verify(mA11yLogger).log( + eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_WINDOW_SIZE_SELECTED)); } @Test @@ -215,6 +232,16 @@ public class WindowMagnificationTest extends SysuiTestCase { waitForIdleSync(); verify(mWindowMagnificationController).setEditMagnifierSizeMode(eq(true)); + verify(mA11yLogger).log( + eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_SIZE_EDITING_ACTIVATED)); + + mWindowMagnification.mMagnificationSettingsControllerCallback.onEditMagnifierSizeMode( + TEST_DISPLAY, /* enable= */ false); + waitForIdleSync(); + verify(mA11yLogger).log( + eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_SIZE_EDITING_ACTIVATED)); + verify(mA11yLogger).log( + eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_SIZE_EDITING_DEACTIVATED)); } @Test @@ -240,6 +267,8 @@ public class WindowMagnificationTest extends SysuiTestCase { waitForIdleSync(); verify(mMagnificationSettingsController).closeMagnificationSettings(); + verify(mA11yLogger).log( + eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_PANEL_CLOSED)); verify(mConnectionCallback).onChangeMagnificationMode(eq(TEST_DISPLAY), eq(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN)); } @@ -269,6 +298,8 @@ public class WindowMagnificationTest extends SysuiTestCase { waitForIdleSync(); verify(mWindowMagnificationController).updateDragHandleResourcesIfNeeded(eq(shown)); + verify(mA11yLogger).log( + eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_PANEL_CLOSED)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt new file mode 100644 index 000000000000..44c99053eb47 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -0,0 +1,291 @@ +/* + * 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.authentication.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.AuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils +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.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class AuthenticationInteractorTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val repository: AuthenticationRepository = utils.authenticationRepository() + private val underTest = + utils.authenticationInteractor( + repository = repository, + ) + + @Test + fun authMethod() = + testScope.runTest { + val authMethod by collectLastValue(underTest.authenticationMethod) + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.PIN(1234)) + + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password("password")) + } + + @Test + fun isUnlocked_whenAuthMethodIsNone_isTrue() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + + underTest.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(isUnlocked).isTrue() + } + + @Test + fun unlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + + underTest.unlockDevice() + runCurrent() + + assertThat(isUnlocked).isTrue() + } + + @Test + fun biometricUnlock() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + + underTest.biometricUnlock() + runCurrent() + + assertThat(isUnlocked).isTrue() + } + + @Test + fun toggleBypassEnabled() = + testScope.runTest { + val isBypassEnabled by collectLastValue(underTest.isBypassEnabled) + assertThat(isBypassEnabled).isFalse() + + underTest.toggleBypassEnabled() + assertThat(isBypassEnabled).isTrue() + + underTest.toggleBypassEnabled() + assertThat(isBypassEnabled).isFalse() + } + + @Test + fun isAuthenticationRequired_lockedAndSecured_true() = + testScope.runTest { + underTest.lockDevice() + runCurrent() + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + + assertThat(underTest.isAuthenticationRequired()).isTrue() + } + + @Test + fun isAuthenticationRequired_lockedAndNotSecured_false() = + testScope.runTest { + underTest.lockDevice() + runCurrent() + underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + + assertThat(underTest.isAuthenticationRequired()).isFalse() + } + + @Test + fun isAuthenticationRequired_unlockedAndSecured_false() = + testScope.runTest { + underTest.unlockDevice() + runCurrent() + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + + assertThat(underTest.isAuthenticationRequired()).isFalse() + } + + @Test + fun isAuthenticationRequired_unlockedAndNotSecured_false() = + testScope.runTest { + underTest.unlockDevice() + runCurrent() + underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + + assertThat(underTest.isAuthenticationRequired()).isFalse() + } + + @Test + fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue() + assertThat(isUnlocked).isTrue() + } + + @Test + fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse() + assertThat(isUnlocked).isFalse() + } + + @Test + fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate("password".toList())).isTrue() + assertThat(isUnlocked).isTrue() + } + + @Test + fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password")) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate("alohomora".toList())).isFalse() + assertThat(isUnlocked).isFalse() + } + + @Test + fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod( + AuthenticationMethodModel.Pattern( + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 0, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 1, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 2, + ), + ) + ) + ) + assertThat(isUnlocked).isFalse() + + assertThat( + underTest.authenticate( + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 0, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 1, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 2, + ), + ) + ) + ) + .isTrue() + assertThat(isUnlocked).isTrue() + } + + @Test + fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod( + AuthenticationMethodModel.Pattern( + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 0, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 1, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 0, + y = 2, + ), + ) + ) + ) + assertThat(isUnlocked).isFalse() + + assertThat( + underTest.authenticate( + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 2, + y = 0, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 2, + y = 1, + ), + AuthenticationMethodModel.Pattern.PatternCoordinate( + x = 2, + y = 2, + ), + ) + ) + ) + .isFalse() + assertThat(isUnlocked).isFalse() + } + + @Test + fun unlocksDevice_whenAuthMethodBecomesNone() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + + repository.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(isUnlocked).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt index eb7d9c3900f1..c84efac86db5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt @@ -85,9 +85,9 @@ class BatteryMeterViewTest : SysuiTestCase() { } @Test - fun contentDescription_estimateAndOverheated() { + fun contentDescription_estimateAndBatteryDefender() { mBatteryMeterView.onBatteryLevelChanged(17, false) - mBatteryMeterView.onIsOverheatedChanged(true) + mBatteryMeterView.onIsBatteryDefenderChanged(true) mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE) mBatteryMeterView.setBatteryEstimateFetcher(Fetcher()) @@ -103,9 +103,9 @@ class BatteryMeterViewTest : SysuiTestCase() { } @Test - fun contentDescription_overheated() { + fun contentDescription_batteryDefender() { mBatteryMeterView.onBatteryLevelChanged(90, false) - mBatteryMeterView.onIsOverheatedChanged(true) + mBatteryMeterView.onIsBatteryDefenderChanged(true) assertThat(mBatteryMeterView.contentDescription).isEqualTo( context.getString(R.string.accessibility_battery_level_charging_paused, 90) @@ -155,14 +155,14 @@ class BatteryMeterViewTest : SysuiTestCase() { @Test fun contentDescription_manyUpdates_alwaysUpdated() { - // Overheated + // BatteryDefender mBatteryMeterView.onBatteryLevelChanged(90, false) - mBatteryMeterView.onIsOverheatedChanged(true) + mBatteryMeterView.onIsBatteryDefenderChanged(true) assertThat(mBatteryMeterView.contentDescription).isEqualTo( context.getString(R.string.accessibility_battery_level_charging_paused, 90) ) - // Overheated & estimate + // BatteryDefender & estimate mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE) mBatteryMeterView.setBatteryEstimateFetcher(Fetcher()) mBatteryMeterView.updatePercentText() @@ -175,7 +175,7 @@ class BatteryMeterViewTest : SysuiTestCase() { ) // Just estimate - mBatteryMeterView.onIsOverheatedChanged(false) + mBatteryMeterView.onIsBatteryDefenderChanged(false) assertThat(mBatteryMeterView.contentDescription).isEqualTo( context.getString( R.string.accessibility_battery_level_with_estimate, @@ -198,35 +198,35 @@ class BatteryMeterViewTest : SysuiTestCase() { } @Test - fun isOverheatedChanged_true_drawableGetsTrue() { + fun isBatteryDefenderChanged_true_drawableGetsTrue() { mBatteryMeterView.setDisplayShieldEnabled(true) val drawable = getBatteryDrawable() - mBatteryMeterView.onIsOverheatedChanged(true) + mBatteryMeterView.onIsBatteryDefenderChanged(true) assertThat(drawable.displayShield).isTrue() } @Test - fun isOverheatedChanged_false_drawableGetsFalse() { + fun isBatteryDefenderChanged_false_drawableGetsFalse() { mBatteryMeterView.setDisplayShieldEnabled(true) val drawable = getBatteryDrawable() // Start as true - mBatteryMeterView.onIsOverheatedChanged(true) + mBatteryMeterView.onIsBatteryDefenderChanged(true) // Update to false - mBatteryMeterView.onIsOverheatedChanged(false) + mBatteryMeterView.onIsBatteryDefenderChanged(false) assertThat(drawable.displayShield).isFalse() } @Test - fun isOverheatedChanged_true_featureflagOff_drawableGetsFalse() { + fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse() { mBatteryMeterView.setDisplayShieldEnabled(false) val drawable = getBatteryDrawable() - mBatteryMeterView.onIsOverheatedChanged(true) + mBatteryMeterView.onIsBatteryDefenderChanged(true) assertThat(drawable.displayShield).isFalse() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index a361bbc69ef3..9d68cf3aee01 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -504,6 +504,21 @@ class AuthContainerViewTest : SysuiTestCase() { waitForIdleSync() assertThat(isAttachedToWindow()).isTrue() } + + @Test + fun testLayoutParams_hasCutoutModeAlwaysFlag() { + val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") + val lpFlags = layoutParams.flags + + assertThat((lpFlags and WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) + != 0).isTrue() + } + + @Test + fun testLayoutParams_excludesSystemBarInsets() { + val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") + assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.systemBars()) == 0).isTrue() + } } private fun AuthContainerView.hasBiometricPrompt() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java index b8bca3a403e1..38c9caf085e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java @@ -29,7 +29,6 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationManager; import android.hardware.biometrics.BiometricFaceConstants; -import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricSourceType; import android.os.Handler; import android.testing.AndroidTestingRunner; @@ -51,6 +50,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Optional; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -64,11 +65,16 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { KeyguardStateController mKeyguardStateController; @Mock NotificationManager mNotificationManager; + @Mock + Optional<FingerprintReEnrollNotification> mFingerprintReEnrollNotificationOptional; + @Mock + FingerprintReEnrollNotification mFingerprintReEnrollNotification; private static final String TAG = "BiometricNotificationService"; private static final int FACE_NOTIFICATION_ID = 1; private static final int FINGERPRINT_NOTIFICATION_ID = 2; private static final long SHOW_NOTIFICATION_DELAY_MS = 5_000L; // 5 seconds + private static final int FINGERPRINT_ACQUIRED_RE_ENROLL = 0; private final ArgumentCaptor<Notification> mNotificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class); @@ -78,6 +84,11 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { @Before public void setUp() { + when(mFingerprintReEnrollNotificationOptional.orElse(any())) + .thenReturn(mFingerprintReEnrollNotification); + when(mFingerprintReEnrollNotification.isFingerprintReEnrollRequired( + FINGERPRINT_ACQUIRED_RE_ENROLL)).thenReturn(true); + mLooper = TestableLooper.get(this); Handler handler = new Handler(mLooper.getLooper()); BiometricNotificationDialogFactory dialogFactory = new BiometricNotificationDialogFactory(); @@ -87,7 +98,8 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { new BiometricNotificationService(mContext, mKeyguardUpdateMonitor, mKeyguardStateController, handler, mNotificationManager, - broadcastReceiver); + broadcastReceiver, + mFingerprintReEnrollNotificationOptional); biometricNotificationService.start(); ArgumentCaptor<KeyguardUpdateMonitorCallback> updateMonitorCallbackArgumentCaptor = @@ -108,8 +120,8 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { public void testShowFingerprintReEnrollNotification() { when(mKeyguardStateController.isShowing()).thenReturn(false); - mKeyguardUpdateMonitorCallback.onBiometricError( - BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL, + mKeyguardUpdateMonitorCallback.onBiometricHelp( + FINGERPRINT_ACQUIRED_RE_ENROLL, "Testing Fingerprint Re-enrollment" /* errString */, BiometricSourceType.FINGERPRINT ); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java index d3622c59b23c..0d3b394eabdb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java @@ -16,6 +16,7 @@ package com.android.systemui.biometrics; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -100,6 +101,8 @@ public class UdfpsKeyguardViewLegacyControllerBaseTest extends SysuiTestCase { when(mResourceContext.getString(anyInt())).thenReturn("test string"); when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(false); when(mView.getUnpausedAlpha()).thenReturn(255); + when(mShadeExpansionStateManager.addExpansionListener(any())).thenReturn( + new ShadeExpansionChangeEvent(0, false, false, 0)); mController = createUdfpsKeyguardViewController(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt index 7531cb4a91f7..ffad32626db3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt @@ -24,13 +24,14 @@ import com.android.keyguard.KeyguardSecurityModel import com.android.systemui.RoboPilotTest import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.BouncerView import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.FakeTrustRepository import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl -import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor @@ -95,8 +96,9 @@ class UdfpsKeyguardViewLegacyControllerWithCoroutinesTest : mock(DismissCallbackRegistry::class.java), context, mKeyguardUpdateMonitor, - mock(TrustRepository::class.java), - FakeFeatureFlags(), + FakeTrustRepository(), + FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) }, + testScope.backgroundScope, ) mAlternateBouncerInteractor = AlternateBouncerInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt index 4f89b69108f4..da55d5a099b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt @@ -33,7 +33,7 @@ class BoundingBoxOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { @Test fun isGoodOverlap() { val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat()) - val actual = underTest.isGoodOverlap(touchData, SENSOR) + val actual = underTest.isGoodOverlap(touchData, SENSOR, OVERLAY) assertThat(actual).isEqualTo(testCase.expected) } @@ -50,8 +50,10 @@ class BoundingBoxOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) ), genNegativeTestCases( - invalidXs = listOf(SENSOR.left - 1, SENSOR.right + 1), - invalidYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + invalidXs = + listOf(SENSOR.left - 1, SENSOR.right + 1, OVERLAY.left, OVERLAY.right), + invalidYs = + listOf(SENSOR.top - 1, SENSOR.bottom + 1, OVERLAY.top, OVERLAY.bottom), validXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), validYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()) ) @@ -82,6 +84,7 @@ private val TOUCH_DATA = ) private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 500 /* bottom */) +private val OVERLAY = Rect(0 /* left */, 100 /* top */, 400 /* right */, 600 /* bottom */) private fun genTestCases( xs: List<Int>, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt index fb3c1854e996..317141ba42dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt @@ -43,7 +43,7 @@ class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { minor = testCase.minor, major = testCase.major ) - val actual = underTest.isGoodOverlap(touchData, SENSOR) + val actual = underTest.isGoodOverlap(touchData, SENSOR, OVERLAY) assertThat(actual).isEqualTo(testCase.expected) } @@ -71,8 +71,10 @@ class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { expected = true ), genNegativeTestCase( - outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1), - outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + outerXs = + listOf(SENSOR.left - 1, SENSOR.right + 1, OVERLAY.left, OVERLAY.right), + outerYs = + listOf(SENSOR.top - 1, SENSOR.bottom + 1, OVERLAY.top, OVERLAY.bottom), minor = 100f, major = 100f, expected = false @@ -104,6 +106,7 @@ private val TOUCH_DATA = ) private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 400 /* bottom */) +private val OVERLAY = Rect(0 /* left */, 100 /* top */, 400 /* right */, 600 /* bottom */) private fun genPositiveTestCases( innerXs: List<Int>, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt index 834d0a69e427..3e5c43a33474 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt @@ -16,7 +16,7 @@ class NormalizedTouchDataTest(val testCase: TestCase) : SysuiTestCase() { @Test fun isWithinSensor() { val touchData = TOUCH_DATA.copy(x = testCase.x.toFloat(), y = testCase.y.toFloat()) - val actual = touchData.isWithinSensor(SENSOR) + val actual = touchData.isWithinBounds(SENSOR) assertThat(actual).isEqualTo(testCase.expected) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt new file mode 100644 index 000000000000..730f89dd76ba --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -0,0 +1,212 @@ +/* + * 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.bouncer.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +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 +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class BouncerInteractorTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), + ) + private val sceneInteractor = utils.sceneInteractor() + private val underTest = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ) + + @Before + fun setUp() { + overrideResource(R.string.keyguard_enter_your_pin, MESSAGE_ENTER_YOUR_PIN) + overrideResource(R.string.keyguard_enter_your_password, MESSAGE_ENTER_YOUR_PASSWORD) + overrideResource(R.string.keyguard_enter_your_pattern, MESSAGE_ENTER_YOUR_PATTERN) + overrideResource(R.string.kg_wrong_pin, MESSAGE_WRONG_PIN) + overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD) + overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN) + } + + @Test + fun pinAuthMethod() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val message by collectLastValue(underTest.message) + + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + underTest.showOrUnlockDevice("container1") + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) + + underTest.clearMessage() + assertThat(message).isNull() + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) + + // Wrong input. + underTest.authenticate(listOf(9, 8, 7)) + assertThat(message).isEqualTo(MESSAGE_WRONG_PIN) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) + + // Correct input. + underTest.authenticate(listOf(1, 2, 3, 4)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun passwordAuthMethod() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val message by collectLastValue(underTest.message) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + underTest.showOrUnlockDevice("container1") + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) + + underTest.clearMessage() + assertThat(message).isNull() + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) + + // Wrong input. + underTest.authenticate("alohamora".toList()) + assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) + + // Correct input. + underTest.authenticate("password".toList()) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun patternAuthMethod() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val message by collectLastValue(underTest.message) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(emptyList()) + ) + authenticationInteractor.lockDevice() + underTest.showOrUnlockDevice("container1") + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) + + underTest.clearMessage() + assertThat(message).isNull() + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) + + // Wrong input. + underTest.authenticate( + listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(3, 4)) + ) + assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + underTest.resetMessage() + assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) + + // Correct input. + underTest.authenticate(emptyList()) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun showOrUnlockDevice_notLocked_switchesToGoneScene() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() + runCurrent() + + underTest.showOrUnlockDevice("container1") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + authenticationInteractor.lockDevice() + + underTest.showOrUnlockDevice("container1") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun showOrUnlockDevice_customMessageShown() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val message by collectLastValue(underTest.message) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + + val customMessage = "Hello there!" + underTest.showOrUnlockDevice("container1", customMessage) + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + assertThat(message).isEqualTo(customMessage) + } + + companion object { + private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN" + private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password" + private const val MESSAGE_ENTER_YOUR_PATTERN = "Enter your pattern" + private const val MESSAGE_WRONG_PIN = "Wrong PIN" + private const val MESSAGE_WRONG_PASSWORD = "Wrong password" + private const val MESSAGE_WRONG_PATTERN = "Wrong pattern" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt new file mode 100644 index 000000000000..954e67d77181 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -0,0 +1,103 @@ +/* + * 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.bouncer.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class BouncerViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), + ) + private val underTest = + utils.bouncerViewModel( + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = utils.sceneInteractor(), + ) + ) + + @Test + fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() = + testScope.runTest { + val authMethodViewModel: AuthMethodBouncerViewModel? by + collectLastValue(underTest.authMethod) + authMethodsToTest().forEach { authMethod -> + authenticationInteractor.setAuthenticationMethod(authMethod) + + if (authMethod.isSecure) { + assertThat(authMethodViewModel).isNotNull() + } else { + assertThat(authMethodViewModel).isNull() + } + } + } + + @Test + fun authMethod_reusesInstances() = + testScope.runTest { + val seen = mutableMapOf<AuthenticationMethodModel, AuthMethodBouncerViewModel>() + val authMethodViewModel: AuthMethodBouncerViewModel? by + collectLastValue(underTest.authMethod) + // First pass, populate our "seen" map: + authMethodsToTest().forEach { authMethod -> + authenticationInteractor.setAuthenticationMethod(authMethod) + authMethodViewModel?.let { seen[authMethod] = it } + } + + // Second pass, assert same instances are reused: + authMethodsToTest().forEach { authMethod -> + authenticationInteractor.setAuthenticationMethod(authMethod) + authMethodViewModel?.let { assertThat(it).isSameInstanceAs(seen[authMethod]) } + } + } + + @Test + fun authMethodsToTest_returnsCompleteSampleOfAllAuthMethodTypes() { + assertThat(authMethodsToTest().map { it::class }.toSet()) + .isEqualTo(AuthenticationMethodModel::class.sealedSubclasses.toSet()) + } + + private fun authMethodsToTest(): List<AuthenticationMethodModel> { + return listOf( + AuthenticationMethodModel.None, + AuthenticationMethodModel.Swipe, + AuthenticationMethodModel.PIN(1234), + AuthenticationMethodModel.Password("password"), + AuthenticationMethodModel.Pattern( + listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1)) + ), + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt new file mode 100644 index 000000000000..e48b6386c739 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -0,0 +1,198 @@ +/* + * 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.bouncer.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class PasswordBouncerViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), + ) + private val sceneInteractor = utils.sceneInteractor() + private val bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ) + private val bouncerViewModel = + utils.bouncerViewModel( + bouncerInteractor = bouncerInteractor, + ) + private val underTest = + PasswordBouncerViewModel( + interactor = bouncerInteractor, + ) + + @Before + fun setUp() { + overrideResource(R.string.keyguard_enter_your_password, ENTER_YOUR_PASSWORD) + overrideResource(R.string.kg_wrong_password, WRONG_PASSWORD) + } + + @Test + fun onShown() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val password by collectLastValue(underTest.password) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + underTest.onShown() + + assertThat(message).isEqualTo(ENTER_YOUR_PASSWORD) + assertThat(password).isEqualTo("") + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onPasswordInputChanged() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val password by collectLastValue(underTest.password) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + + underTest.onPasswordInputChanged("password") + + assertThat(message).isEmpty() + assertThat(password).isEqualTo("password") + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onAuthenticateKeyPressed_whenCorrect() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onPasswordInputChanged("password") + + underTest.onAuthenticateKeyPressed() + + assertThat(isUnlocked).isTrue() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun onAuthenticateKeyPressed_whenWrong() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val password by collectLastValue(underTest.password) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onPasswordInputChanged("wrong") + + underTest.onAuthenticateKeyPressed() + + assertThat(password).isEqualTo("") + assertThat(message).isEqualTo(WRONG_PASSWORD) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onAuthenticateKeyPressed_correctAfterWrong() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val password by collectLastValue(underTest.password) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onPasswordInputChanged("wrong") + underTest.onAuthenticateKeyPressed() + assertThat(password).isEqualTo("") + assertThat(message).isEqualTo(WRONG_PASSWORD) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + // Enter the correct password: + underTest.onPasswordInputChanged("password") + assertThat(message).isEmpty() + + underTest.onAuthenticateKeyPressed() + + assertThat(isUnlocked).isTrue() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + companion object { + private const val CONTAINER_NAME = "container1" + private const val ENTER_YOUR_PASSWORD = "Enter your password" + private const val WRONG_PASSWORD = "Wrong password" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt new file mode 100644 index 000000000000..6ce29e67982c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -0,0 +1,272 @@ +/* + * 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.bouncer.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class PatternBouncerViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), + ) + private val sceneInteractor = utils.sceneInteractor() + private val bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ) + private val bouncerViewModel = + utils.bouncerViewModel( + bouncerInteractor = bouncerInteractor, + ) + private val underTest = + PatternBouncerViewModel( + applicationContext = context, + applicationScope = testScope.backgroundScope, + interactor = bouncerInteractor, + ) + + @Before + fun setUp() { + overrideResource(R.string.keyguard_enter_your_pattern, ENTER_YOUR_PATTERN) + overrideResource(R.string.kg_wrong_pattern, WRONG_PATTERN) + } + + @Test + fun onShown() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val selectedDots by collectLastValue(underTest.selectedDots) + val currentDot by collectLastValue(underTest.currentDot) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + underTest.onShown() + + assertThat(message).isEqualTo(ENTER_YOUR_PATTERN) + assertThat(selectedDots).isEmpty() + assertThat(currentDot).isNull() + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onDragStart() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val selectedDots by collectLastValue(underTest.selectedDots) + val currentDot by collectLastValue(underTest.currentDot) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + + underTest.onDragStart() + + assertThat(message).isEmpty() + assertThat(selectedDots).isEmpty() + assertThat(currentDot).isNull() + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onDragEnd_whenCorrect() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val selectedDots by collectLastValue(underTest.selectedDots) + val currentDot by collectLastValue(underTest.currentDot) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onDragStart() + assertThat(currentDot).isNull() + CORRECT_PATTERN.forEachIndexed { index, coordinate -> + underTest.onDrag( + xPx = 30f * coordinate.x + 15, + yPx = 30f * coordinate.y + 15, + containerSizePx = 90, + verticalOffsetPx = 0f, + ) + assertWithMessage("Wrong selected dots for index $index") + .that(selectedDots) + .isEqualTo( + CORRECT_PATTERN.subList(0, index + 1).map { + PatternDotViewModel( + x = it.x, + y = it.y, + ) + } + ) + assertWithMessage("Wrong current dot for index $index") + .that(currentDot) + .isEqualTo( + PatternDotViewModel( + x = CORRECT_PATTERN.subList(0, index + 1).last().x, + y = CORRECT_PATTERN.subList(0, index + 1).last().y, + ) + ) + } + + underTest.onDragEnd() + + assertThat(isUnlocked).isTrue() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun onDragEnd_whenWrong() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val selectedDots by collectLastValue(underTest.selectedDots) + val currentDot by collectLastValue(underTest.currentDot) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onDragStart() + CORRECT_PATTERN.subList(0, 3).forEach { coordinate -> + underTest.onDrag( + xPx = 30f * coordinate.x + 15, + yPx = 30f * coordinate.y + 15, + containerSizePx = 90, + verticalOffsetPx = 0f, + ) + } + + underTest.onDragEnd() + + assertThat(selectedDots).isEmpty() + assertThat(currentDot).isNull() + assertThat(message).isEqualTo(WRONG_PATTERN) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onDragEnd_correctAfterWrong() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val selectedDots by collectLastValue(underTest.selectedDots) + val currentDot by collectLastValue(underTest.currentDot) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onDragStart() + CORRECT_PATTERN.subList(2, 7).forEach { coordinate -> + underTest.onDrag( + xPx = 30f * coordinate.x + 15, + yPx = 30f * coordinate.y + 15, + containerSizePx = 90, + verticalOffsetPx = 0f, + ) + } + underTest.onDragEnd() + assertThat(selectedDots).isEmpty() + assertThat(currentDot).isNull() + assertThat(message).isEqualTo(WRONG_PATTERN) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + // Enter the correct pattern: + CORRECT_PATTERN.forEach { coordinate -> + underTest.onDrag( + xPx = 30f * coordinate.x + 15, + yPx = 30f * coordinate.y + 15, + containerSizePx = 90, + verticalOffsetPx = 0f, + ) + } + + underTest.onDragEnd() + + assertThat(isUnlocked).isTrue() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + companion object { + private const val CONTAINER_NAME = "container1" + private const val ENTER_YOUR_PATTERN = "Enter your pattern" + private const val WRONG_PATTERN = "Wrong pattern" + private val CORRECT_PATTERN = + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate(x = 1, y = 1), + AuthenticationMethodModel.Pattern.PatternCoordinate(x = 0, y = 1), + AuthenticationMethodModel.Pattern.PatternCoordinate(x = 0, y = 0), + AuthenticationMethodModel.Pattern.PatternCoordinate(x = 1, y = 0), + AuthenticationMethodModel.Pattern.PatternCoordinate(x = 2, y = 0), + AuthenticationMethodModel.Pattern.PatternCoordinate(x = 2, y = 1), + AuthenticationMethodModel.Pattern.PatternCoordinate(x = 2, y = 2), + AuthenticationMethodModel.Pattern.PatternCoordinate(x = 1, y = 2), + AuthenticationMethodModel.Pattern.PatternCoordinate(x = 0, y = 2), + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt new file mode 100644 index 000000000000..bb28520ad8a0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -0,0 +1,267 @@ +/* + * 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.bouncer.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class PinBouncerViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), + ) + private val bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ) + private val bouncerViewModel = + BouncerViewModel( + applicationContext = context, + applicationScope = testScope.backgroundScope, + interactorFactory = + object : BouncerInteractor.Factory { + override fun create(containerName: String): BouncerInteractor { + return bouncerInteractor + } + }, + containerName = CONTAINER_NAME, + ) + private val underTest = + PinBouncerViewModel( + applicationScope = testScope.backgroundScope, + interactor = bouncerInteractor, + ) + + @Before + fun setUp() { + overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN) + overrideResource(R.string.kg_wrong_pin, WRONG_PIN) + } + + @Test + fun onShown() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + underTest.onShown() + + assertThat(message).isEqualTo(ENTER_YOUR_PIN) + assertThat(pinLengths).isEqualTo(0 to 0) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onPinButtonClicked() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + + underTest.onPinButtonClicked(1) + + assertThat(message).isEmpty() + assertThat(pinLengths).isEqualTo(0 to 1) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onBackspaceButtonClicked() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onPinButtonClicked(1) + assertThat(pinLengths).isEqualTo(0 to 1) + + underTest.onBackspaceButtonClicked() + + assertThat(message).isEmpty() + assertThat(pinLengths).isEqualTo(1 to 0) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onBackspaceButtonLongPressed() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onPinButtonClicked(1) + underTest.onPinButtonClicked(2) + underTest.onPinButtonClicked(3) + underTest.onPinButtonClicked(4) + + underTest.onBackspaceButtonLongPressed() + repeat(4) { index -> + assertThat(pinLengths).isEqualTo(4 - index to 3 - index) + advanceTimeBy(PinBouncerViewModel.BACKSPACE_LONG_PRESS_DELAY_MS) + } + + assertThat(message).isEmpty() + assertThat(pinLengths).isEqualTo(1 to 0) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onAuthenticateButtonClicked_whenCorrect() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onPinButtonClicked(1) + underTest.onPinButtonClicked(2) + underTest.onPinButtonClicked(3) + underTest.onPinButtonClicked(4) + + underTest.onAuthenticateButtonClicked() + + assertThat(isUnlocked).isTrue() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun onAuthenticateButtonClicked_whenWrong() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onPinButtonClicked(1) + underTest.onPinButtonClicked(2) + underTest.onPinButtonClicked(3) + underTest.onPinButtonClicked(4) + underTest.onPinButtonClicked(5) // PIN is now wrong! + + underTest.onAuthenticateButtonClicked() + + assertThat(pinLengths).isEqualTo(0 to 0) + assertThat(message).isEqualTo(WRONG_PIN) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onAuthenticateButtonClicked_correctAfterWrong() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + underTest.onShown() + underTest.onPinButtonClicked(1) + underTest.onPinButtonClicked(2) + underTest.onPinButtonClicked(3) + underTest.onPinButtonClicked(4) + underTest.onPinButtonClicked(5) // PIN is now wrong! + underTest.onAuthenticateButtonClicked() + assertThat(message).isEqualTo(WRONG_PIN) + assertThat(pinLengths).isEqualTo(0 to 0) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + + // Enter the correct PIN: + underTest.onPinButtonClicked(1) + underTest.onPinButtonClicked(2) + underTest.onPinButtonClicked(3) + underTest.onPinButtonClicked(4) + assertThat(message).isEmpty() + + underTest.onAuthenticateButtonClicked() + + assertThat(isUnlocked).isTrue() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + companion object { + private const val CONTAINER_NAME = "container1" + private const val ENTER_YOUR_PIN = "Enter your pin" + private const val WRONG_PIN = "Wrong pin" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java index 3cbb24905222..63b0b2562a41 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; +import android.content.res.Resources; import android.testing.AndroidTestingRunner; import android.view.View; @@ -73,6 +74,9 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { private Context mContext; @Mock + private Resources mResources; + + @Mock private ControlsComponent mControlsComponent; @Mock @@ -118,7 +122,7 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { @Test public void complicationType() { final DreamHomeControlsComplication complication = - new DreamHomeControlsComplication(mComponentFactory); + new DreamHomeControlsComplication(mResources, mComponentFactory); assertThat(complication.getRequiredTypeAvailability()).isEqualTo( COMPLICATION_TYPE_HOME_CONTROLS); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 1a620d266e42..fcd6568de9f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -70,7 +70,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.doAnswer import org.mockito.Mockito.doReturn +import org.mockito.Mockito.isNull import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify @@ -104,6 +106,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { private lateinit var parent: FrameLayout private lateinit var underTest: ControlsUiControllerImpl + private var isKeyguardDismissed: Boolean = true + private var isRemoveAppDialogCreated: Boolean = false + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -140,11 +145,23 @@ class ControlsUiControllerImplTest : SysuiTestCase() { authorizedPanelsRepository, preferredPanelRepository, featureFlags, - ControlsDialogsFactory { fakeDialogController.dialog }, + ControlsDialogsFactory { + isRemoveAppDialogCreated = true + fakeDialogController.dialog + }, dumpManager, ) `when`(userTracker.userId).thenReturn(0) `when`(userTracker.userHandle).thenReturn(UserHandle.of(0)) + doAnswer { + if (isKeyguardDismissed) { + it.getArgument<ActivityStarter.OnDismissAction>(0).onDismiss() + } else { + it.getArgument<Runnable?>(1)?.run() + } + } + .whenever(activityStarter) + .dismissKeyguardThenExecute(any(), isNull(), any()) } @Test @@ -414,6 +431,26 @@ class ControlsUiControllerImplTest : SysuiTestCase() { } @Test + fun testKeyguardRemovingAppsNotShowingDialog() { + isKeyguardDismissed = false + val componentName = ComponentName(context, "cls") + whenever(controlsController.removeFavorites(eq(componentName))).thenReturn(true) + val panel = SelectedItem.PanelItem("App name", componentName) + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(panel) + ) + underTest.show(parent, {}, context) + underTest.startRemovingApp(componentName, "Test App") + + assertThat(isRemoveAppDialogCreated).isFalse() + verify(controlsController, never()).removeFavorites(eq(componentName)) + assertThat(underTest.getPreferredSelectedItem(emptyList())).isEqualTo(panel) + assertThat(preferredPanelRepository.shouldAddDefaultComponent()).isTrue() + assertThat(preferredPanelRepository.getSelectedComponent()) + .isEqualTo(SelectedComponentRepository.SelectedComponent(panel)) + } + + @Test fun testCancelRemovingAppsDoesntRemoveFavorite() { val componentName = ComponentName(context, "cls") whenever(controlsController.removeFavorites(eq(componentName))).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt index 9cf988e5b00a..8ee7d3e86265 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt @@ -30,6 +30,7 @@ import android.testing.TestableLooper import android.view.SurfaceControlViewHost import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils +import com.android.systemui.R import com.android.systemui.SystemUIAppComponentFactoryBase import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogLaunchAnimator @@ -67,6 +68,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -103,6 +105,7 @@ class CustomizationProviderTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + overrideResource(R.bool.custom_lockscreen_shortcuts_enabled, true) whenever(previewRenderer.surfacePackage).thenReturn(previewSurfacePackage) whenever(previewRendererFactory.create(any())).thenReturn(previewRenderer) whenever(backgroundHandler.looper).thenReturn(TestableLooper.get(this).looper) @@ -195,6 +198,7 @@ class CustomizationProviderTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, dockManager = dockManager, backgroundDispatcher = testDispatcher, + appContext = mContext, ) underTest.previewManager = KeyguardRemotePreviewManager( @@ -216,6 +220,13 @@ class CustomizationProviderTest : SysuiTestCase() { ) } + @After + fun tearDown() { + mContext + .getOrCreateTestableResources() + .removeOverride(R.bool.custom_lockscreen_shortcuts_enabled) + } + @Test fun onAttachInfo_reportsContext() { val callback: SystemUIAppComponentFactoryBase.ContextAvailableCallback = mock() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt index 47df64fc33e2..688c2db044e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt @@ -113,8 +113,7 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { @After fun tearDown() { - keyguardUnlockAnimationController.surfaceBehindEntryAnimator.cancel() - keyguardUnlockAnimationController.surfaceBehindAlphaAnimator.cancel() + keyguardUnlockAnimationController.notifyFinishedKeyguardExitAnimation(true) } /** 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 83c89f1e6855..f31ac00051f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -95,6 +95,7 @@ import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.DeviceConfigProxyFake; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; +import com.android.wm.shell.keyguard.KeyguardTransitions; import org.junit.Before; import org.junit.Test; @@ -135,6 +136,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock ScreenOffAnimationController mScreenOffAnimationController; private @Mock InteractionJankMonitor mInteractionJankMonitor; private @Mock ScreenOnCoordinator mScreenOnCoordinator; + private @Mock KeyguardTransitions mKeyguardTransitions; private @Mock ShadeController mShadeController; private NotificationShadeWindowController mNotificationShadeWindowController; private @Mock DreamOverlayStateController mDreamOverlayStateController; @@ -519,6 +521,12 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } @Test + public void testWakeAndUnlocking() { + mViewMediator.onWakeAndUnlocking(); + verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean()); + } + + @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testDoKeyguardWhileInteractive_resets() { mViewMediator.setShowingLocked(true); @@ -614,6 +622,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mScreenOffAnimationController, () -> mNotificationShadeDepthController, mScreenOnCoordinator, + mKeyguardTransitions, mInteractionJankMonitor, mDreamOverlayStateController, () -> mShadeController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt new file mode 100644 index 000000000000..931f82ce9f6b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt @@ -0,0 +1,190 @@ +package com.android.systemui.keyguard + +import android.content.ComponentCallbacks2 +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.data.repository.FakeCommandQueue +import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.WakeSleepReason +import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.utils.GlobalWindowManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ResourceTrimmerTest : SysuiTestCase() { + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val keyguardRepository = FakeKeyguardRepository() + + @Mock private lateinit var globalWindowManager: GlobalWindowManager + @Mock private lateinit var featureFlags: FeatureFlags + private lateinit var resourceTrimmer: ResourceTrimmer + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + keyguardRepository.setWakefulnessModel( + WakefulnessModel(WakefulnessState.AWAKE, WakeSleepReason.OTHER, WakeSleepReason.OTHER) + ) + keyguardRepository.setDozeAmount(0f) + + val interactor = + KeyguardInteractor( + keyguardRepository, + FakeCommandQueue(), + featureFlags, + FakeKeyguardBouncerRepository() + ) + resourceTrimmer = + ResourceTrimmer( + interactor, + globalWindowManager, + testScope.backgroundScope, + testDispatcher + ) + resourceTrimmer.start() + } + + @Test + fun noChange_noOutputChanges() = + testScope.runTest { + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + } + + @Test + fun dozeAodDisabled_sleep_trimsMemory() = + testScope.runTest { + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + testScope.runCurrent() + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + } + + @Test + fun dozeEnabled_sleepWithFullDozeAmount_trimsMemory() = + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(1f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + testScope.runCurrent() + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + } + + @Test + fun dozeEnabled_sleepWithoutFullDozeAmount_doesntTrimMemory() = + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(0f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + } + + @Test + fun aodEnabled_sleepWithFullDozeAmount_trimsMemoryOnce() { + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(0f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + + generateSequence(0f) { it + 0.1f } + .takeWhile { it < 1f } + .forEach { + keyguardRepository.setDozeAmount(it) + testScope.runCurrent() + } + verifyZeroInteractions(globalWindowManager) + + keyguardRepository.setDozeAmount(1f) + testScope.runCurrent() + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + } + } + + @Test + fun aodEnabled_deviceWakesHalfWayThrough_doesNotTrimMemory() { + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(0f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + + generateSequence(0f) { it + 0.1f } + .takeWhile { it < 0.8f } + .forEach { + keyguardRepository.setDozeAmount(it) + testScope.runCurrent() + } + verifyZeroInteractions(globalWindowManager) + + generateSequence(0.8f) { it - 0.1f } + .takeWhile { it >= 0f } + .forEach { + keyguardRepository.setDozeAmount(it) + testScope.runCurrent() + } + + keyguardRepository.setDozeAmount(0f) + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactoryTest.kt new file mode 100644 index 000000000000..9e798490eaff --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/factory/BouncerMessageFactoryTest.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.bouncer.data.factory + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardSecurityModel +import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN +import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Password +import com.android.keyguard.KeyguardSecurityModel.SecurityMode.Pattern +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT +import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class BouncerMessageFactoryTest : SysuiTestCase() { + private lateinit var underTest: BouncerMessageFactory + + @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor + + @Mock private lateinit var securityModel: KeyguardSecurityModel + + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testScope = TestScope() + underTest = BouncerMessageFactory(updateMonitor, securityModel) + } + + @Test + fun bouncerMessages_choosesTheRightMessage_basedOnSecurityModeAndFpAllowedInBouncer() = + testScope.runTest { + primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = false) + .isEqualTo("Enter PIN") + primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = true) + .isEqualTo("Unlock with PIN or fingerprint") + + primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = false) + .isEqualTo("Enter password") + primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = true) + .isEqualTo("Unlock with password or fingerprint") + + primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = false) + .isEqualTo("Draw pattern") + primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = true) + .isEqualTo("Unlock with pattern or fingerprint") + } + + @Test + fun bouncerMessages_setsPrimaryAndSecondaryMessage_basedOnSecurityModeAndFpAllowedInBouncer() = + testScope.runTest { + primaryMessage( + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT, + mode = PIN, + fpAllowedInBouncer = true + ) + .isEqualTo("Wrong PIN. Try again.") + secondaryMessage( + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT, + mode = PIN, + fpAllowedInBouncer = true + ) + .isEqualTo("Or unlock with fingerprint") + + primaryMessage( + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT, + mode = Password, + fpAllowedInBouncer = true + ) + .isEqualTo("Wrong password. Try again.") + secondaryMessage( + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT, + mode = Password, + fpAllowedInBouncer = true + ) + .isEqualTo("Or unlock with fingerprint") + + primaryMessage( + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT, + mode = Pattern, + fpAllowedInBouncer = true + ) + .isEqualTo("Wrong pattern. Try again.") + secondaryMessage( + PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT, + mode = Pattern, + fpAllowedInBouncer = true + ) + .isEqualTo("Or unlock with fingerprint") + } + + private fun primaryMessage( + reason: Int, + mode: KeyguardSecurityModel.SecurityMode, + fpAllowedInBouncer: Boolean + ): StringSubject { + return assertThat( + context.resources.getString( + bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!.message!!.messageResId!! + ) + )!! + } + + private fun secondaryMessage( + reason: Int, + mode: KeyguardSecurityModel.SecurityMode, + fpAllowedInBouncer: Boolean + ): StringSubject { + return assertThat( + context.resources.getString( + bouncerMessageModel(mode, fpAllowedInBouncer, reason)!! + .secondaryMessage!! + .messageResId!! + ) + )!! + } + + private fun bouncerMessageModel( + mode: KeyguardSecurityModel.SecurityMode, + fpAllowedInBouncer: Boolean, + reason: Int + ): BouncerMessageModel? { + whenever(securityModel.getSecurityMode(0)).thenReturn(mode) + whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(fpAllowedInBouncer) + + return underTest.createFromPromptReason(reason, 0) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt new file mode 100644 index 000000000000..1277fc0e1bdd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.bouncer.data.repository + +import android.content.pm.UserInfo +import android.hardware.biometrics.BiometricSourceType +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE +import com.android.keyguard.KeyguardSecurityModel +import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.R +import com.android.systemui.R.string.keyguard_enter_pin +import com.android.systemui.R.string.kg_prompt_after_dpm_lock +import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pin +import com.android.systemui.R.string.kg_prompt_auth_timeout +import com.android.systemui.R.string.kg_prompt_pin_auth_timeout +import com.android.systemui.R.string.kg_prompt_reason_restart_pin +import com.android.systemui.R.string.kg_prompt_unattended_update +import com.android.systemui.R.string.kg_trust_agent_disabled +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory +import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel +import com.android.systemui.keyguard.bouncer.shared.model.Message +import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.FakeTrustRepository +import com.android.systemui.keyguard.shared.model.AuthenticationFlags +import com.android.systemui.user.data.repository.FakeUserRepository +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 +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4::class) +class BouncerMessageRepositoryTest : SysuiTestCase() { + + @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var securityModel: KeyguardSecurityModel + @Captor + private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback> + + private lateinit var underTest: BouncerMessageRepository + private lateinit var trustRepository: FakeTrustRepository + private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository + private lateinit var userRepository: FakeUserRepository + private lateinit var fingerprintRepository: FakeDeviceEntryFingerprintAuthRepository + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + trustRepository = FakeTrustRepository() + biometricSettingsRepository = FakeBiometricSettingsRepository() + userRepository = FakeUserRepository() + userRepository.setUserInfos(listOf(PRIMARY_USER)) + fingerprintRepository = FakeDeviceEntryFingerprintAuthRepository() + testScope = TestScope() + + whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false) + whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN) + underTest = + BouncerMessageRepositoryImpl( + trustRepository = trustRepository, + biometricSettingsRepository = biometricSettingsRepository, + updateMonitor = updateMonitor, + bouncerMessageFactory = BouncerMessageFactory(updateMonitor, securityModel), + userRepository = userRepository, + fingerprintAuthRepository = fingerprintRepository + ) + } + + @Test + fun setCustomMessage_propagatesState() = + testScope.runTest { + underTest.setCustomMessage(message("not empty")) + + val customMessage = collectLastValue(underTest.customMessage) + + assertThat(customMessage()).isEqualTo(message("not empty")) + } + + @Test + fun setFaceMessage_propagatesState() = + testScope.runTest { + underTest.setFaceAcquisitionMessage(message("not empty")) + + val faceAcquisitionMessage = collectLastValue(underTest.faceAcquisitionMessage) + + assertThat(faceAcquisitionMessage()).isEqualTo(message("not empty")) + } + + @Test + fun setFpMessage_propagatesState() = + testScope.runTest { + underTest.setFingerprintAcquisitionMessage(message("not empty")) + + val fpAcquisitionMsg = collectLastValue(underTest.fingerprintAcquisitionMessage) + + assertThat(fpAcquisitionMsg()).isEqualTo(message("not empty")) + } + + @Test + fun setPrimaryAuthMessage_propagatesState() = + testScope.runTest { + underTest.setPrimaryAuthMessage(message("not empty")) + + val primaryAuthMessage = collectLastValue(underTest.primaryAuthMessage) + + assertThat(primaryAuthMessage()).isEqualTo(message("not empty")) + } + + @Test + fun biometricAuthMessage_propagatesBiometricAuthMessages() = + testScope.runTest { + userRepository.setSelectedUserInfo(PRIMARY_USER) + val biometricAuthMessage = collectLastValue(underTest.biometricAuthMessage) + runCurrent() + + verify(updateMonitor).registerCallback(updateMonitorCallback.capture()) + + updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT) + + assertThat(biometricAuthMessage()) + .isEqualTo(message(R.string.kg_fp_not_recognized, R.string.kg_bio_try_again_or_pin)) + + updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FACE) + + assertThat(biometricAuthMessage()) + .isEqualTo( + message(R.string.bouncer_face_not_recognized, R.string.kg_bio_try_again_or_pin) + ) + + updateMonitorCallback.value.onBiometricAcquired(BiometricSourceType.FACE, 0) + + assertThat(biometricAuthMessage()).isNull() + } + + @Test + fun onFaceLockout_propagatesState() = + testScope.runTest { + userRepository.setSelectedUserInfo(PRIMARY_USER) + val lockoutMessage = collectLastValue(underTest.biometricLockedOutMessage) + runCurrent() + verify(updateMonitor).registerCallback(updateMonitorCallback.capture()) + + whenever(updateMonitor.isFaceLockedOut).thenReturn(true) + updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE) + + assertThat(lockoutMessage()) + .isEqualTo(message(keyguard_enter_pin, R.string.kg_face_locked_out)) + + whenever(updateMonitor.isFaceLockedOut).thenReturn(false) + updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE) + assertThat(lockoutMessage()).isNull() + } + + @Test + fun onFingerprintLockout_propagatesState() = + testScope.runTest { + userRepository.setSelectedUserInfo(PRIMARY_USER) + val lockedOutMessage = collectLastValue(underTest.biometricLockedOutMessage) + runCurrent() + + fingerprintRepository.setLockedOut(true) + + assertThat(lockedOutMessage()) + .isEqualTo(message(keyguard_enter_pin, R.string.kg_fp_locked_out)) + + fingerprintRepository.setLockedOut(false) + assertThat(lockedOutMessage()).isNull() + } + + @Test + fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() = + testScope.runTest { + userRepository.setSelectedUserInfo(PRIMARY_USER) + trustRepository.setCurrentUserTrustManaged(false) + biometricSettingsRepository.setFaceEnrolled(false) + biometricSettingsRepository.setFingerprintEnrolled(false) + + verifyMessagesForAuthFlag( + STRONG_AUTH_NOT_REQUIRED to null, + STRONG_AUTH_REQUIRED_AFTER_BOOT to null, + SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, + STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null, + STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null, + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to null, + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null, + SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null, + STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to null, + STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock), + ) + } + + @Test + fun authFlagsChanges_withTrustManaged_providesDifferentMessages() = + testScope.runTest { + userRepository.setSelectedUserInfo(PRIMARY_USER) + biometricSettingsRepository.setFaceEnrolled(false) + biometricSettingsRepository.setFingerprintEnrolled(false) + + trustRepository.setCurrentUserTrustManaged(true) + + verifyMessagesForAuthFlag( + STRONG_AUTH_NOT_REQUIRED to null, + STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null, + STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin), + STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout), + STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock), + SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to + Pair(keyguard_enter_pin, kg_trust_agent_disabled), + SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + Pair(keyguard_enter_pin, kg_trust_agent_disabled), + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin), + STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + Pair(keyguard_enter_pin, kg_prompt_unattended_update), + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + Pair(keyguard_enter_pin, kg_prompt_auth_timeout), + ) + } + + @Test + fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() = + testScope.runTest { + userRepository.setSelectedUserInfo(PRIMARY_USER) + trustRepository.setCurrentUserTrustManaged(false) + biometricSettingsRepository.setFingerprintEnrolled(false) + + biometricSettingsRepository.setIsFaceAuthEnabled(true) + biometricSettingsRepository.setFaceEnrolled(true) + + verifyMessagesForAuthFlag( + STRONG_AUTH_NOT_REQUIRED to null, + STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null, + SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, + SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null, + STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin), + STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout), + STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock), + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin), + STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + Pair(keyguard_enter_pin, kg_prompt_unattended_update), + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + Pair(keyguard_enter_pin, kg_prompt_auth_timeout), + ) + } + + @Test + fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() = + testScope.runTest { + userRepository.setSelectedUserInfo(PRIMARY_USER) + trustRepository.setCurrentUserTrustManaged(false) + biometricSettingsRepository.setIsFaceAuthEnabled(false) + biometricSettingsRepository.setFaceEnrolled(false) + + biometricSettingsRepository.setFingerprintEnrolled(true) + biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true) + + verifyMessagesForAuthFlag( + STRONG_AUTH_NOT_REQUIRED to null, + STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null, + SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null, + SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null, + STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin), + STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout), + STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock), + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin), + STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + Pair(keyguard_enter_pin, kg_prompt_unattended_update), + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + Pair(keyguard_enter_pin, kg_prompt_auth_timeout), + ) + } + + private fun TestScope.verifyMessagesForAuthFlag( + vararg authFlagToExpectedMessages: Pair<Int, Pair<Int, Int>?> + ) { + val authFlagsMessage = collectLastValue(underTest.authFlagsMessage) + + authFlagToExpectedMessages.forEach { (flag, messagePair) -> + biometricSettingsRepository.setAuthenticationFlags( + AuthenticationFlags(PRIMARY_USER_ID, flag) + ) + + assertThat(authFlagsMessage()) + .isEqualTo(messagePair?.let { message(it.first, it.second) }) + } + } + + private fun message(primaryResId: Int, secondaryResId: Int): BouncerMessageModel { + return BouncerMessageModel( + message = Message(messageResId = primaryResId), + secondaryMessage = Message(messageResId = secondaryResId) + ) + } + private fun message(value: String): BouncerMessageModel { + return BouncerMessageModel(message = Message(message = value)) + } + + companion object { + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt new file mode 100644 index 000000000000..b0af3104a3ae --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.bouncer.domain.interactor + +import android.content.pm.UserInfo +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardSecurityModel +import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.R.string.keyguard_enter_pin +import com.android.systemui.R.string.kg_too_many_failed_attempts_countdown +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.FlowValue +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory +import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel +import com.android.systemui.keyguard.bouncer.shared.model.Message +import com.android.systemui.keyguard.data.repository.FakeBouncerMessageRepository +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.mockito.KotlinArgumentCaptor +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.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4::class) +class BouncerMessageInteractorTest : SysuiTestCase() { + + @Mock private lateinit var securityModel: KeyguardSecurityModel + @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil + private lateinit var countDownTimerCallback: KotlinArgumentCaptor<CountDownTimerCallback> + private lateinit var underTest: BouncerMessageInteractor + private lateinit var repository: FakeBouncerMessageRepository + private lateinit var userRepository: FakeUserRepository + private lateinit var testScope: TestScope + private lateinit var bouncerMessage: FlowValue<BouncerMessageModel?> + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + repository = FakeBouncerMessageRepository() + userRepository = FakeUserRepository() + userRepository.setUserInfos(listOf(PRIMARY_USER)) + testScope = TestScope() + countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java) + + allowTestableLooperAsMainThread() + whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN) + whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false) + } + + suspend fun TestScope.init() { + userRepository.setSelectedUserInfo(PRIMARY_USER) + val featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + underTest = + BouncerMessageInteractor( + repository = repository, + factory = BouncerMessageFactory(updateMonitor, securityModel), + userRepository = userRepository, + countDownTimerUtil = countDownTimerUtil, + featureFlags = featureFlags + ) + bouncerMessage = collectLastValue(underTest.bouncerMessage) + } + + @Test + fun onIncorrectSecurityInput_setsTheBouncerModelInTheRepository() = + testScope.runTest { + init() + underTest.onPrimaryAuthIncorrectAttempt() + + assertThat(repository.primaryAuthMessage).isNotNull() + assertThat( + context.resources.getString( + repository.primaryAuthMessage.value!!.message!!.messageResId!! + ) + ) + .isEqualTo("Wrong PIN. Try again.") + } + + @Test + fun onUserStartsPrimaryAuthInput_clearsAllSetBouncerMessages() = + testScope.runTest { + init() + repository.setCustomMessage(message("not empty")) + repository.setFaceAcquisitionMessage(message("not empty")) + repository.setFingerprintAcquisitionMessage(message("not empty")) + repository.setPrimaryAuthMessage(message("not empty")) + + underTest.onPrimaryBouncerUserInput() + + assertThat(repository.customMessage.value).isNull() + assertThat(repository.faceAcquisitionMessage.value).isNull() + assertThat(repository.fingerprintAcquisitionMessage.value).isNull() + assertThat(repository.primaryAuthMessage.value).isNull() + } + + @Test + fun onBouncerBeingHidden_clearsAllSetBouncerMessages() = + testScope.runTest { + init() + repository.setCustomMessage(message("not empty")) + repository.setFaceAcquisitionMessage(message("not empty")) + repository.setFingerprintAcquisitionMessage(message("not empty")) + repository.setPrimaryAuthMessage(message("not empty")) + + underTest.onBouncerBeingHidden() + + assertThat(repository.customMessage.value).isNull() + assertThat(repository.faceAcquisitionMessage.value).isNull() + assertThat(repository.fingerprintAcquisitionMessage.value).isNull() + assertThat(repository.primaryAuthMessage.value).isNull() + } + + @Test + fun setCustomMessage_setsRepositoryValue() = + testScope.runTest { + init() + + underTest.setCustomMessage("not empty") + + assertThat(repository.customMessage.value) + .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty"))) + + underTest.setCustomMessage(null) + assertThat(repository.customMessage.value).isNull() + } + + @Test + fun setFaceMessage_setsRepositoryValue() = + testScope.runTest { + init() + + underTest.setFaceAcquisitionMessage("not empty") + + assertThat(repository.faceAcquisitionMessage.value) + .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty"))) + + underTest.setFaceAcquisitionMessage(null) + assertThat(repository.faceAcquisitionMessage.value).isNull() + } + + @Test + fun setFingerprintMessage_setsRepositoryValue() = + testScope.runTest { + init() + + underTest.setFingerprintAcquisitionMessage("not empty") + + assertThat(repository.fingerprintAcquisitionMessage.value) + .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty"))) + + underTest.setFingerprintAcquisitionMessage(null) + assertThat(repository.fingerprintAcquisitionMessage.value).isNull() + } + + @Test + fun onPrimaryAuthLockout_startsTimerForSpecifiedNumberOfSeconds() = + testScope.runTest { + init() + + underTest.onPrimaryAuthLockedOut(3) + + verify(countDownTimerUtil) + .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture()) + + countDownTimerCallback.value.onTick(2000L) + + val primaryMessage = repository.primaryAuthMessage.value!!.message!! + assertThat(primaryMessage.messageResId!!) + .isEqualTo(kg_too_many_failed_attempts_countdown) + assertThat(primaryMessage.formatterArgs).isEqualTo(mapOf(Pair("count", 2))) + } + + @Test + fun onPrimaryAuthLockout_timerComplete_resetsRepositoryMessages() = + testScope.runTest { + init() + repository.setCustomMessage(message("not empty")) + repository.setFaceAcquisitionMessage(message("not empty")) + repository.setFingerprintAcquisitionMessage(message("not empty")) + repository.setPrimaryAuthMessage(message("not empty")) + + underTest.onPrimaryAuthLockedOut(3) + + verify(countDownTimerUtil) + .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture()) + + countDownTimerCallback.value.onFinish() + + assertThat(repository.customMessage.value).isNull() + assertThat(repository.faceAcquisitionMessage.value).isNull() + assertThat(repository.fingerprintAcquisitionMessage.value).isNull() + assertThat(repository.primaryAuthMessage.value).isNull() + } + + @Test + fun bouncerMessage_hasPriorityOrderOfMessages() = + testScope.runTest { + init() + repository.setBiometricAuthMessage(message("biometric message")) + repository.setFaceAcquisitionMessage(message("face acquisition message")) + repository.setFingerprintAcquisitionMessage(message("fingerprint acquisition message")) + repository.setPrimaryAuthMessage(message("primary auth message")) + repository.setAuthFlagsMessage(message("auth flags message")) + repository.setBiometricLockedOutMessage(message("biometrics locked out")) + repository.setCustomMessage(message("custom message")) + + assertThat(bouncerMessage()).isEqualTo(message("primary auth message")) + + repository.setPrimaryAuthMessage(null) + + assertThat(bouncerMessage()).isEqualTo(message("biometric message")) + + repository.setBiometricAuthMessage(null) + + assertThat(bouncerMessage()).isEqualTo(message("fingerprint acquisition message")) + + repository.setFingerprintAcquisitionMessage(null) + + assertThat(bouncerMessage()).isEqualTo(message("face acquisition message")) + + repository.setFaceAcquisitionMessage(null) + + assertThat(bouncerMessage()).isEqualTo(message("custom message")) + + repository.setCustomMessage(null) + + assertThat(bouncerMessage()).isEqualTo(message("auth flags message")) + + repository.setAuthFlagsMessage(null) + + assertThat(bouncerMessage()).isEqualTo(message("biometrics locked out")) + + repository.setBiometricLockedOutMessage(null) + + // sets the default message if everything else is null + assertThat(bouncerMessage()!!.message!!.messageResId).isEqualTo(keyguard_enter_pin) + } + + private fun message(value: String): BouncerMessageModel { + return BouncerMessageModel(message = Message(message = value)) + } + + companion object { + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt index 1bab8170ccde..b925aeb10262 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt @@ -310,19 +310,38 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { createBiometricSettingsRepository() verify(biometricManager) .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture()) - whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID))) .thenReturn(0) broadcastDPMStateChange() + val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled) + + assertThat(isFaceAuthEnabled()).isFalse() + + // Value changes for another user + biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID) + assertThat(isFaceAuthEnabled()).isFalse() + + // Value changes for current user. biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID) - val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled) assertThat(isFaceAuthEnabled()).isTrue() + } + + @Test + fun userChange_biometricEnabledChange_handlesRaceCondition() = + testScope.runTest { + createBiometricSettingsRepository() + verify(biometricManager) + .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture()) + val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled) + biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID) + runCurrent() - biometricManagerCallback.value.onChanged(false, PRIMARY_USER_ID) + userRepository.setSelectedUserInfo(ANOTHER_USER) + runCurrent() - assertThat(isFaceAuthEnabled()).isFalse() + assertThat(isFaceAuthEnabled()).isTrue() } @Test @@ -382,7 +401,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { } @Test - fun userInLockdownUsesStrongAuthFlagsToDetermineValue() = + fun userInLockdownUsesAuthFlagsToDetermineValue() = testScope.runTest { createBiometricSettingsRepository() @@ -405,6 +424,38 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { assertThat(isUserInLockdown()).isTrue() } + @Test + fun authFlagChangesForCurrentUserArePropagated() = + testScope.runTest { + createBiometricSettingsRepository() + + val authFlags = collectLastValue(underTest.authenticationFlags) + // has default value. + val defaultStrongAuthValue = STRONG_AUTH_REQUIRED_AFTER_BOOT + assertThat(authFlags()!!.flag).isEqualTo(defaultStrongAuthValue) + + // change strong auth flags for another user. + // Combine with one more flag to check if we do the bitwise and + val inLockdown = + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN or STRONG_AUTH_REQUIRED_AFTER_TIMEOUT + onStrongAuthChanged(inLockdown, ANOTHER_USER_ID) + + // Still false. + assertThat(authFlags()!!.flag).isEqualTo(defaultStrongAuthValue) + + // change strong auth flags for current user. + onStrongAuthChanged(inLockdown, PRIMARY_USER_ID) + + assertThat(authFlags()!!.flag).isEqualTo(inLockdown) + + onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, ANOTHER_USER_ID) + + assertThat(authFlags()!!.flag).isEqualTo(inLockdown) + + userRepository.setSelectedUserInfo(ANOTHER_USER) + assertThat(authFlags()!!.flag).isEqualTo(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT) + } + private fun enrollmentChange(biometricType: BiometricType, userId: Int, enabled: Boolean) { authControllerCallback.value.onEnrollmentsChanged(biometricType, userId, enabled) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt index 8611359adc71..29d75001148a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt @@ -64,7 +64,6 @@ class TrustRepositoryTest : SysuiTestCase() { testScope = TestScope() userRepository = FakeUserRepository() userRepository.setUserInfos(users) - val logger = TrustRepositoryLogger( LogBuffer("TestBuffer", 1, mock(LogcatEchoTracker::class.java), false) @@ -224,4 +223,41 @@ class TrustRepositoryTest : SysuiTestCase() { assertThat(isCurrentUserTrusted()).isTrue() } + + @Test + fun isCurrentUserActiveUnlockRunning_runningFirstBeforeUserInfoChanges_emitsCorrectValue() = + testScope.runTest { + runCurrent() + verify(trustManager).registerTrustListener(listener.capture()) + val isCurrentUserActiveUnlockRunning by + collectLastValue(underTest.isCurrentUserActiveUnlockRunning) + userRepository.setSelectedUserInfo(users[1]) + + // active unlock running = true for users[0].id, but not the current user + listener.value.onIsActiveUnlockRunningChanged(true, users[0].id) + assertThat(isCurrentUserActiveUnlockRunning).isFalse() + + // current user is now users[0].id + userRepository.setSelectedUserInfo(users[0]) + assertThat(isCurrentUserActiveUnlockRunning).isTrue() + } + + @Test + fun isCurrentUserActiveUnlockRunning_whenActiveUnlockRunningForCurrentUser_emitsNewValue() = + testScope.runTest { + runCurrent() + verify(trustManager).registerTrustListener(listener.capture()) + val isCurrentUserActiveUnlockRunning by + collectLastValue(underTest.isCurrentUserActiveUnlockRunning) + userRepository.setSelectedUserInfo(users[0]) + + listener.value.onIsActiveUnlockRunningChanged(true, users[0].id) + assertThat(isCurrentUserActiveUnlockRunning).isTrue() + + listener.value.onIsActiveUnlockRunningChanged(false, users[0].id) + assertThat(isCurrentUserActiveUnlockRunning).isFalse() + + listener.value.onIsActiveUnlockRunningChanged(true, users[0].id) + assertThat(isCurrentUserActiveUnlockRunning).isTrue() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt index e261982dfd18..6af122051afc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt @@ -35,7 +35,7 @@ import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepo import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.data.repository.TrustRepository +import com.android.systemui.keyguard.data.repository.FakeTrustRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -100,8 +100,9 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { mock(DismissCallbackRegistry::class.java), context, keyguardUpdateMonitor, - mock(TrustRepository::class.java), - FakeFeatureFlags(), + FakeTrustRepository(), + FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) }, + testScope.backgroundScope, ), AlternateBouncerInteractor( mock(StatusBarStateController::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt index dfef94777039..5de24e4fc322 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt @@ -21,6 +21,7 @@ import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.systemui.R import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -39,6 +40,7 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -65,6 +67,7 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, true) whenever(accessibilityManager.getRecommendedTimeoutMillis(anyInt(), anyInt())).thenAnswer { it.arguments[0] } @@ -76,6 +79,13 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { runBlocking { createUnderTest() } } + @After + fun tearDown() { + mContext + .getOrCreateTestableResources() + .removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled) + } + @Test fun isEnabled() = testScope.runTest { @@ -108,6 +118,17 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { } @Test + fun isEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() = + testScope.runTest { + overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, false) + createUnderTest() + val isEnabled by collectLastValue(underTest.isLongPressHandlingEnabled) + runCurrent() + + assertThat(isEnabled).isFalse() + } + + @Test fun longPressed_menuClicked_showsSettings() = testScope.runTest { val isMenuVisible by collectLastValue(underTest.isMenuVisible) @@ -267,6 +288,7 @@ class KeyguardLongPressInteractorTest : SysuiTestCase() { ) { underTest = KeyguardLongPressInteractor( + appContext = mContext, scope = testScope.backgroundScope, transitionInteractor = KeyguardTransitionInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index a75e11a23deb..fb21847cd4d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -340,6 +340,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, dockManager = dockManager, backgroundDispatcher = testDispatcher, + appContext = mContext, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 3336e3b21180..5d2c3edd40af 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -200,6 +200,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, dockManager = dockManager, backgroundDispatcher = testDispatcher, + appContext = mContext, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt new file mode 100644 index 000000000000..d622f1c30816 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1 +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +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.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class LockscreenSceneInteractorTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), + ) + private val underTest = + utils.lockScreenSceneInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ), + ) + + @Test + fun isDeviceLocked() = + testScope.runTest { + val isDeviceLocked by collectLastValue(underTest.isDeviceLocked) + + authenticationInteractor.lockDevice() + assertThat(isDeviceLocked).isTrue() + + authenticationInteractor.unlockDevice() + assertThat(isDeviceLocked).isFalse() + } + + @Test + fun isSwipeToDismissEnabled_deviceLockedAndAuthMethodSwipe_true() = + testScope.runTest { + val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled) + + authenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + + assertThat(isSwipeToDismissEnabled).isTrue() + } + + @Test + fun isSwipeToDismissEnabled_deviceUnlockedAndAuthMethodSwipe_false() = + testScope.runTest { + val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled) + + authenticationInteractor.unlockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + + assertThat(isSwipeToDismissEnabled).isFalse() + } + + @Test + fun dismissLockScreen_deviceLockedWithSecureAuthMethod_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + + underTest.dismissLockscreen() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun dismissLockScreen_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.unlockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + + underTest.dismissLockscreen() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun dismissLockScreen_deviceLockedWithInsecureAuthMethod_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + + underTest.dismissLockscreen() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun deviceLockedInNonLockScreenScene_switchesToLockScreenScene() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + runCurrent() + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) + runCurrent() + authenticationInteractor.unlockDevice() + runCurrent() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + + authenticationInteractor.lockDevice() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + } + + @Test + fun deviceBiometricUnlockedInLockScreen_bypassEnabled_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen)) + if (!authenticationInteractor.isBypassEnabled.value) { + authenticationInteractor.toggleBypassEnabled() + } + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + + authenticationInteractor.biometricUnlock() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun deviceBiometricUnlockedInLockScreen_bypassNotEnabled_doesNotSwitch() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen)) + if (authenticationInteractor.isBypassEnabled.value) { + authenticationInteractor.toggleBypassEnabled() + } + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + + authenticationInteractor.biometricUnlock() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + } + + @Test + fun switchFromLockScreenToGone_authMethodSwipe_unlocksDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + assertThat(isUnlocked).isFalse() + + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) + + assertThat(isUnlocked).isTrue() + } + + @Test + fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + assertThat(isUnlocked).isFalse() + + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) + + assertThat(isUnlocked).isFalse() + } + + @Test + fun switchFromNonLockScreenToGone_authMethodSwipe_doesNotUnlockDevice() = + testScope.runTest { + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + runCurrent() + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Shade)) + runCurrent() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + runCurrent() + assertThat(isUnlocked).isFalse() + + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) + + assertThat(isUnlocked).isFalse() + } + + @Test + fun authMethodChangedToNone_onLockScreenScene_dismissesLockScreen() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun authMethodChangedToNone_notOnLockScreenScene_doesNotDismissLockScreen() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + runCurrent() + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.QuickSettings)) + runCurrent() + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings)) + + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None) + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt index eed1e739b909..b5cb44aafd29 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt @@ -44,6 +44,8 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.android.systemui.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -75,6 +77,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { private lateinit var resources: TestableResources private lateinit var trustRepository: FakeTrustRepository private lateinit var featureFlags: FakeFeatureFlags + private lateinit var testScope: TestScope @Before fun setUp() { @@ -83,9 +86,10 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { .thenReturn(KeyguardSecurityModel.SecurityMode.PIN) DejankUtils.setImmediate(true) + testScope = TestScope() mainHandler = FakeHandler(android.os.Looper.getMainLooper()) trustRepository = FakeTrustRepository() - featureFlags = FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, false) } + featureFlags = FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) } underTest = PrimaryBouncerInteractor( repository, @@ -100,6 +104,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { keyguardUpdateMonitor, trustRepository, featureFlags, + testScope.backgroundScope, ) whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null) whenever(repository.primaryBouncerShow.value).thenReturn(false) @@ -154,6 +159,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { verify(repository).setPrimaryShow(false) verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.INVISIBLE) verify(repository).setPrimaryStartDisappearAnimation(null) + verify(repository).setPanelExpansion(EXPANSION_HIDDEN) } @Test @@ -398,7 +404,6 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { mainHandler.setMode(FakeHandler.Mode.QUEUEING) // GIVEN bouncer should be delayed due to face auth - featureFlags.apply { set(Flags.DELAY_BOUNCER, true) } whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true) whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE)) .thenReturn(true) @@ -420,26 +425,29 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { @Test fun delayBouncerWhenActiveUnlockPossible() { - mainHandler.setMode(FakeHandler.Mode.QUEUEING) + testScope.run { + mainHandler.setMode(FakeHandler.Mode.QUEUEING) - // GIVEN bouncer should be delayed due to active unlock - featureFlags.apply { set(Flags.DELAY_BOUNCER, true) } - trustRepository.setCurrentUserActiveUnlockAvailable(true) - whenever(keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState()).thenReturn(true) + // GIVEN bouncer should be delayed due to active unlock + trustRepository.setCurrentUserActiveUnlockAvailable(true) + whenever(keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState()) + .thenReturn(true) + runCurrent() - // WHEN bouncer show is requested - underTest.show(true) + // WHEN bouncer show is requested + underTest.show(true) - // THEN primary show & primary showing soon were scheduled to update - verify(repository, never()).setPrimaryShow(true) - verify(repository, never()).setPrimaryShowingSoon(false) + // THEN primary show & primary showing soon were scheduled to update + verify(repository, never()).setPrimaryShow(true) + verify(repository, never()).setPrimaryShowingSoon(false) - // WHEN all queued messages are dispatched - mainHandler.dispatchQueuedMessages() + // WHEN all queued messages are dispatched + mainHandler.dispatchQueuedMessages() - // THEN primary show & primary showing soon are updated - verify(repository).setPrimaryShow(true) - verify(repository).setPrimaryShowingSoon(false) + // THEN primary show & primary showing soon are updated + verify(repository).setPrimaryShow(true) + verify(repository).setPrimaryShowingSoon(false) + } } private fun updateSideFpsVisibilityParameters( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt index 5056b43798d2..b288fbcc0678 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.BouncerView import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository @@ -34,6 +35,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -75,7 +77,8 @@ class PrimaryBouncerInteractorWithCoroutinesTest : SysuiTestCase() { context, keyguardUpdateMonitor, Mockito.mock(TrustRepository::class.java), - FakeFeatureFlags(), + FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) }, + TestScope().backgroundScope, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 69d43af60321..8a36dbc86e81 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -211,6 +211,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { ) val keyguardLongPressInteractor = KeyguardLongPressInteractor( + appContext = mContext, scope = testScope.backgroundScope, transitionInteractor = KeyguardTransitionInteractor( @@ -240,6 +241,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, dockManager = dockManager, backgroundDispatcher = testDispatcher, + appContext = mContext, ), bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository), burnInHelperWrapper = burnInHelperWrapper, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt index e9f1ac103f87..15707c93146c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.BouncerView import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository @@ -38,6 +39,7 @@ import com.android.systemui.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -81,7 +83,8 @@ class KeyguardBouncerViewModelTest : SysuiTestCase() { context, keyguardUpdateMonitor, Mockito.mock(TrustRepository::class.java), - FakeFeatureFlags(), + FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) }, + TestScope().backgroundScope, ) underTest = KeyguardBouncerViewModel(bouncerView, bouncerInteractor) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt new file mode 100644 index 000000000000..8ba3f0f57eed --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1 +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +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.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class LockscreenSceneViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), + ) + + private val underTest = + LockscreenSceneViewModel( + applicationScope = testScope.backgroundScope, + interactorFactory = + object : LockscreenSceneInteractor.Factory { + override fun create(containerName: String): LockscreenSceneInteractor { + return utils.lockScreenSceneInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ), + ) + } + }, + containerName = CONTAINER_1 + ) + + @Test + fun lockButtonIcon_whenLocked() = + testScope.runTest { + val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.lockDevice() + + assertThat((lockButtonIcon as? Icon.Resource)?.res) + .isEqualTo(R.drawable.ic_device_lock_on) + } + + @Test + fun lockButtonIcon_whenUnlocked() = + testScope.runTest { + val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) + authenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + authenticationInteractor.unlockDevice() + + assertThat((lockButtonIcon as? Icon.Resource)?.res) + .isEqualTo(R.drawable.ic_device_lock_off) + } + + @Test + fun upTransitionSceneKey_swipeToUnlockedEnabled_gone() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + authenticationInteractor.lockDevice() + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) + } + + @Test + fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) + } + + @Test + fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + runCurrent() + + underTest.onLockButtonClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onContentClicked_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } + + @Test + fun onLockButtonClicked_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() + runCurrent() + + underTest.onLockButtonClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index d428db7b9dda..fd6e4577d323 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -25,6 +25,7 @@ import android.app.smartspace.SmartspaceConfig import android.app.smartspace.SmartspaceManager import android.app.smartspace.SmartspaceTarget import android.content.Intent +import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.drawable.Icon import android.media.MediaDescription @@ -40,6 +41,7 @@ import android.testing.TestableLooper.RunWithLooper import androidx.media.utils.MediaConstants import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId +import com.android.internal.statusbar.IStatusBarService import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.R @@ -76,6 +78,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify @@ -130,6 +133,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var activityStarter: ActivityStarter @Mock lateinit var smartspaceManager: SmartspaceManager @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock lateinit var statusBarService: IStatusBarService lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget @Mock private lateinit var mediaRecommendationItem: SmartspaceAction @@ -192,7 +196,8 @@ class MediaDataManagerTest : SysuiTestCase() { mediaFlags = mediaFlags, logger = logger, smartspaceManager = smartspaceManager, - keyguardUpdateMonitor = keyguardUpdateMonitor + keyguardUpdateMonitor = keyguardUpdateMonitor, + statusBarService = statusBarService, ) verify(tunerService) .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) @@ -517,19 +522,211 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test - fun testOnNotificationRemoved_emptyTitle_notConverted() { - // GIVEN that the manager has a notification with a resume action and empty title. + fun testOnNotificationAdded_emptyTitle_isRequired_notLoaded() { + // When the manager has a notification with an empty title, and the app is required + // to include a non-empty title + whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true) whenever(controller.metadata) .thenReturn( metadataBuilder .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE) .build() ) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + // Then the media control is not added and we report a notification error + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(statusBarService) + .onNotificationError( + eq(PACKAGE_NAME), + eq(mediaNotification.tag), + eq(mediaNotification.id), + eq(mediaNotification.uid), + eq(mediaNotification.initialPid), + eq(MEDIA_TITLE_ERROR_MESSAGE), + eq(mediaNotification.user.identifier) + ) + verify(listener, never()) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any()) + } + + @Test + fun testOnNotificationAdded_blankTitle_isRequired_notLoaded() { + // When the manager has a notification with a blank title, and the app is required + // to include a non-empty title + whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE) + .build() + ) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + // Then the media control is not added and we report a notification error + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(statusBarService) + .onNotificationError( + eq(PACKAGE_NAME), + eq(mediaNotification.tag), + eq(mediaNotification.id), + eq(mediaNotification.uid), + eq(mediaNotification.initialPid), + eq(MEDIA_TITLE_ERROR_MESSAGE), + eq(mediaNotification.user.identifier) + ) + verify(listener, never()) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any()) + } + + @Test + fun testOnNotificationUpdated_invalidTitle_isRequired_logMediaRemoved() { + // When the app is required to provide a non-blank title, and updates a previously valid + // title to an empty one + whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true) + addNotificationAndLoad() + val data = mediaDataCaptor.value + + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + + reset(listener) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE) + .build() + ) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + // Then the media control is removed + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(statusBarService) + .onNotificationError( + eq(PACKAGE_NAME), + eq(mediaNotification.tag), + eq(mediaNotification.id), + eq(mediaNotification.uid), + eq(mediaNotification.initialPid), + eq(MEDIA_TITLE_ERROR_MESSAGE), + eq(mediaNotification.user.identifier) + ) + verify(listener, never()) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testOnNotificationAdded_emptyTitle_notRequired_hasPlaceholder() { + // When the manager has a notification with an empty title, and the app is not + // required to include a non-empty title + val mockPackageManager = mock(PackageManager::class.java) + context.setMockPackageManager(mockPackageManager) + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(false) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE) + .build() + ) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + // Then a media control is created with a placeholder title string + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val placeholderTitle = context.getString(R.string.controls_media_empty_title, APP_NAME) + assertThat(mediaDataCaptor.value.song).isEqualTo(placeholderTitle) + } + + @Test + fun testOnNotificationAdded_blankTitle_notRequired_hasPlaceholder() { + // GIVEN that the manager has a notification with a blank title, and the app is not + // required to include a non-empty title + val mockPackageManager = mock(PackageManager::class.java) + context.setMockPackageManager(mockPackageManager) + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(false) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE) + .build() + ) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + // Then a media control is created with a placeholder title string + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val placeholderTitle = context.getString(R.string.controls_media_empty_title, APP_NAME) + assertThat(mediaDataCaptor.value.song).isEqualTo(placeholderTitle) + } + + @Test + fun testOnNotificationRemoved_emptyTitle_notConverted() { + // GIVEN that the manager has a notification with a resume action and empty title. addNotificationAndLoad() val data = mediaDataCaptor.value val instanceId = data.instanceId assertThat(data.resumption).isFalse() - mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + mediaDataManager.onMediaDataLoaded( + KEY, + null, + data.copy(song = SESSION_EMPTY_TITLE, resumeAction = Runnable {}) + ) // WHEN the notification is removed reset(listener) @@ -554,17 +751,15 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testOnNotificationRemoved_blankTitle_notConverted() { // GIVEN that the manager has a notification with a resume action and blank title. - whenever(controller.metadata) - .thenReturn( - metadataBuilder - .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE) - .build() - ) addNotificationAndLoad() val data = mediaDataCaptor.value val instanceId = data.instanceId assertThat(data.resumption).isFalse() - mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + mediaDataManager.onMediaDataLoaded( + KEY, + null, + data.copy(song = SESSION_BLANK_TITLE, resumeAction = Runnable {}) + ) // WHEN the notification is removed reset(listener) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index 1e465c791795..68c33fb93fe6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -530,6 +530,8 @@ public class MediaControlPanelTest : SysuiTestCase() { verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.VISIBLE) assertThat(actionNext.isEnabled()).isTrue() + assertThat(actionNext.isFocusable()).isTrue() + assertThat(actionNext.isClickable()).isTrue() assertThat(actionNext.contentDescription).isEqualTo("next") verify(collapsedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE) @@ -576,6 +578,8 @@ public class MediaControlPanelTest : SysuiTestCase() { assertThat(actionPrev.isEnabled()).isFalse() assertThat(actionPrev.drawable).isNull() + assertThat(actionPrev.isFocusable()).isFalse() + assertThat(actionPrev.isClickable()).isFalse() verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.INVISIBLE) assertThat(actionNext.isEnabled()).isFalse() @@ -610,6 +614,8 @@ public class MediaControlPanelTest : SysuiTestCase() { assertThat(actionNext.isEnabled()).isFalse() assertThat(actionNext.drawable).isNull() + assertThat(actionNext.isFocusable()).isFalse() + assertThat(actionNext.isClickable()).isFalse() verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.INVISIBLE) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index faca8a91d6b3..c89897c0233c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -22,6 +22,7 @@ import static android.media.RouteListingPreference.Item.SUBTEXT_SUBSCRIPTION_REQ import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE; +import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; import static com.google.common.truth.Truth.assertThat; @@ -94,10 +95,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Before public void setUp() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(false); - when(mMediaOutputController.isSubStatusSupported()).thenReturn(false); when(mMediaOutputController.getMediaItemList()).thenReturn(mMediaItems); - when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices); when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false); when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false); when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat); @@ -126,85 +124,24 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void getItemCount_containExtraOneForPairNew() { - assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size() + 1); - } - - @Test - public void getItemId_validPosition_returnCorrespondingId() { - assertThat(mMediaOutputAdapter.getItemId(0)).isEqualTo(mMediaDevices.get( - 0).getId().hashCode()); - } - - @Test - public void getItemId_invalidPosition_returnPosition() { - int invalidPosition = mMediaDevices.size() + 1; - assertThat(mMediaOutputAdapter.getItemId(invalidPosition)).isEqualTo(invalidPosition); - } - - @Test - public void advanced_getItemCount_returnsMediaItemSize() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void getItemCount_returnsMediaItemSize() { assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaItems.size()); } @Test - public void advanced_getItemId_validPosition_returnCorrespondingId() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void getItemId_validPosition_returnCorrespondingId() { assertThat(mMediaOutputAdapter.getItemId(0)).isEqualTo(mMediaItems.get( 0).getMediaDevice().get().getId().hashCode()); } @Test - public void advanced_getItemId_invalidPosition_returnPosition() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void getItemId_invalidPosition_returnPosition() { int invalidPosition = mMediaItems.size() + 1; assertThat(mMediaOutputAdapter.getItemId(invalidPosition)).isEqualTo(invalidPosition); } @Test public void onBindViewHolder_bindPairNew_verifyView() { - mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2); - - assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); - assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); - assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE); - assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE); - assertThat(mViewHolder.mTitleText.getText()).isEqualTo(mContext.getText( - R.string.media_output_dialog_pairing_new)); - } - - @Test - public void onBindViewHolder_bindGroup_withSessionName_verifyView() { - when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices); - when(mMediaOutputController.getSessionName()).thenReturn(TEST_SESSION_NAME); - mMediaOutputAdapter.getItemCount(); - mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); - - assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); - assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); - assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); - assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE); - assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); - } - - @Test - public void onBindViewHolder_bindGroup_noSessionName_verifyView() { - when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices); - when(mMediaOutputController.getSessionName()).thenReturn(null); - mMediaOutputAdapter.getItemCount(); - mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); - - assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); - assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); - assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); - assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE); - assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); - } - - @Test - public void advanced_onBindViewHolder_bindPairNew_verifyView() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter @@ -222,8 +159,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void advanced_onBindViewHolder_bindGroup_withSessionName_verifyView() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void onBindViewHolder_bindGroup_withSessionName_verifyView() { when(mMediaOutputController.getSelectedMediaDevice()).thenReturn( mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect( Collectors.toList())); @@ -243,8 +179,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void advanced_onBindViewHolder_bindGroup_noSessionName_verifyView() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void onBindViewHolder_bindGroup_noSessionName_verifyView() { when(mMediaOutputController.getSelectedMediaDevice()).thenReturn( mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect( Collectors.toList())); @@ -277,8 +212,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void advanced_onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() { when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -294,8 +228,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void advanced_onBindViewHolder_bindConnectedRemoteDevice_verifyView() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void onBindViewHolder_bindConnectedRemoteDevice_verifyView() { when(mMediaOutputController.getSelectableMediaDevice()).thenReturn( ImmutableList.of(mMediaDevice2)); when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true); @@ -314,8 +247,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void advanced_onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() { when(mMediaOutputController.getSelectableMediaDevice()).thenReturn( ImmutableList.of()); when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(true); @@ -351,6 +283,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true); when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false); when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); @@ -446,8 +380,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void subStatusSupported_onBindViewHolder_bindHostDeviceWithOngoingSession_verifyView() { - when(mMediaOutputController.isSubStatusSupported()).thenReturn(true); - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); when(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).thenReturn(true); when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true); when(mMediaDevice1.hasSubtext()).thenReturn(true); @@ -477,8 +409,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { public void subStatusSupported_onBindViewHolder_bindDeviceRequirePremium_verifyView() { String deviceStatus = (String) mContext.getText( R.string.media_output_status_require_premium); - when(mMediaOutputController.isSubStatusSupported()).thenReturn(true); - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); when(mMediaDevice2.hasSubtext()).thenReturn(true); when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_SUBSCRIPTION_REQUIRED); when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus); @@ -503,8 +433,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { public void subStatusSupported_onBindViewHolder_bindDeviceWithAdPlaying_verifyView() { String deviceStatus = (String) mContext.getText( R.string.media_output_status_try_after_ad); - when(mMediaOutputController.isSubStatusSupported()).thenReturn(true); - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); when(mMediaDevice2.hasSubtext()).thenReturn(true); when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_AD_ROUTING_DISALLOWED); when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus); @@ -528,8 +456,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void subStatusSupported_onBindViewHolder_bindDeviceWithOngoingSession_verifyView() { - when(mMediaOutputController.isSubStatusSupported()).thenReturn(true); - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); when(mMediaDevice1.hasSubtext()).thenReturn(true); when(mMediaDevice1.getSubtext()).thenReturn(SUBTEXT_CUSTOM); when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT); @@ -600,15 +526,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() { - mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2); - mViewHolder.mContainerLayout.performClick(); - - verify(mMediaOutputController).launchBluetoothPairing(mViewHolder.mContainerLayout); - } - - @Test - public void advanced_onItemClick_clickPairNew_verifyLaunchBluetoothPairing() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter @@ -626,6 +543,12 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaOutputController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false); assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); + when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + mMediaOutputAdapter.updateItems(); + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder(new LinearLayout(mContext), 0); + mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); mViewHolder.mContainerLayout.performClick(); @@ -638,7 +561,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaOutputController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(true); assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); + when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); + mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + mMediaOutputAdapter.updateItems(); + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder(new LinearLayout(mContext), 0); MediaOutputAdapter.MediaDeviceViewHolder spyMediaDeviceViewHolder = spy(mViewHolder); + mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 0); mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 1); @@ -662,20 +591,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void onItemClick_clicksSelectableDevice_triggerGrouping() { - List<MediaDevice> selectableDevices = new ArrayList<>(); - selectableDevices.add(mMediaDevice2); - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices); - mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); - - mViewHolder.mContainerLayout.performClick(); - - verify(mMediaOutputController).addDeviceToPlayMedia(mMediaDevice2); - } - - @Test - public void advanced_onGroupActionTriggered_clicksEndAreaOfSelectableDevice_triggerGrouping() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void onGroupActionTriggered_clicksEndAreaOfSelectableDevice_triggerGrouping() { List<MediaDevice> selectableDevices = new ArrayList<>(); selectableDevices.add(mMediaDevice2); when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices); @@ -689,8 +605,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void advanced_onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); + public void onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() { when(mMediaOutputController.getSelectableMediaDevice()).thenReturn( ImmutableList.of(mMediaDevice2)); when(mMediaOutputController.getDeselectableMediaDevice()).thenReturn( @@ -707,21 +622,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onItemClick_onGroupActionTriggered_verifySeekbarDisabled() { - when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices); - List<MediaDevice> selectableDevices = new ArrayList<>(); - selectableDevices.add(mMediaDevice1); - when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices); - when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(true); - mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); - - mViewHolder.mContainerLayout.performClick(); - - assertThat(mViewHolder.mSeekBar.isEnabled()).isFalse(); - } - - @Test - public void advanced_onItemClick_onGroupActionTriggered_verifySeekbarDisabled() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); when(mMediaOutputController.getSelectedMediaDevice()).thenReturn( mMediaItems.stream().map((item) -> item.getMediaDevice().get()).collect( Collectors.toList())); @@ -764,7 +664,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void updateItems_controllerItemsUpdated_notUpdatesInAdapterUntilUpdateItems() { - when(mMediaOutputController.isAdvancedLayoutSupported()).thenReturn(true); mMediaOutputAdapter.updateItems(); List<MediaItem> updatedList = new ArrayList<>(); updatedList.add(new MediaItem()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index 480d59c5e8bf..f79c53d10b52 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -54,6 +54,7 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import org.junit.Before; @@ -91,6 +92,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class); private KeyguardManager mKeyguardManager = mock(KeyguardManager.class); private FeatureFlags mFlags = mock(FeatureFlags.class); + private UserTracker mUserTracker = mock(UserTracker.class); private List<MediaController> mMediaControllers = new ArrayList<>(); private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl; @@ -123,7 +125,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender, mMediaOutputController); mMediaOutputBaseDialogImpl.onCreate(new Bundle()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java index 891a6f8a102c..705b485ce1b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java @@ -50,6 +50,7 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import org.junit.After; @@ -95,6 +96,7 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase { private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class); private KeyguardManager mKeyguardManager = mock(KeyguardManager.class); private FeatureFlags mFlags = mock(FeatureFlags.class); + private UserTracker mUserTracker = mock(UserTracker.class); private MediaOutputBroadcastDialog mMediaOutputBroadcastDialog; private MediaOutputController mMediaOutputController; @@ -109,7 +111,7 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false, mBroadcastSender, mMediaOutputController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index 299303d38f29..9f06b5fcc903 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.app.KeyguardManager; @@ -48,13 +49,18 @@ import android.media.MediaMetadata; import android.media.MediaRoute2Info; import android.media.NearbyDevice; import android.media.RoutingSessionInfo; +import android.media.session.ISessionController; import android.media.session.MediaController; +import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; +import android.os.Bundle; import android.os.PowerExemptionManager; import android.os.RemoteException; +import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; import android.text.TextUtils; import android.view.View; @@ -70,9 +76,9 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; @@ -91,6 +97,7 @@ import java.util.Optional; @SmallTest @RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) public class MediaOutputControllerTest extends SysuiTestCase { private static final String TEST_PACKAGE_NAME = "com.test.package.name"; @@ -111,7 +118,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { private NearbyMediaDevicesManager mNearbyMediaDevicesManager; // Mock @Mock - private MediaController mMediaController; + private MediaController mSessionMediaController; @Mock private MediaSessionManager mMediaSessionManager; @Mock @@ -151,10 +158,15 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Mock private PlaybackState mPlaybackState; + @Mock + private UserTracker mUserTracker; + private FeatureFlags mFlags = mock(FeatureFlags.class); private View mDialogLaunchView = mock(View.class); private MediaOutputController.Callback mCallback = mock(MediaOutputController.Callback.class); + final Notification mNotification = mock(Notification.class); + private Context mSpyContext; private MediaOutputController mMediaOutputController; private LocalMediaManager mLocalMediaManager; @@ -169,10 +181,14 @@ public class MediaOutputControllerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mContext.setMockPackageManager(mPackageManager); mSpyContext = spy(mContext); - when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE_NAME); - when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState); - mMediaControllers.add(mMediaController); - when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers); + final UserHandle userHandle = mock(UserHandle.class); + when(mUserTracker.getUserHandle()).thenReturn(userHandle); + when(mSessionMediaController.getPackageName()).thenReturn(TEST_PACKAGE_NAME); + when(mSessionMediaController.getPlaybackState()).thenReturn(mPlaybackState); + mMediaControllers.add(mSessionMediaController); + when(mMediaSessionManager.getActiveSessionsForUser(any(), + Mockito.eq(userHandle))).thenReturn( + mMediaControllers); doReturn(mMediaSessionManager).when(mSpyContext).getSystemService( MediaSessionManager.class); when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn( @@ -182,9 +198,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); - when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(false); - when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(false); + mKeyguardManager, mFlags, mUserTracker); mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; @@ -205,6 +219,24 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE); mNearbyDevices.add(mNearbyDevice1); mNearbyDevices.add(mNearbyDevice2); + + final List<NotificationEntry> entryList = new ArrayList<>(); + final NotificationEntry entry = mock(NotificationEntry.class); + final StatusBarNotification sbn = mock(StatusBarNotification.class); + final Bundle bundle = mock(Bundle.class); + final MediaSession.Token token = mock(MediaSession.Token.class); + final ISessionController binder = mock(ISessionController.class); + entryList.add(entry); + + when(mNotification.isMediaNotification()).thenReturn(false); + when(mNotifCollection.getAllNotifs()).thenReturn(entryList); + when(entry.getSbn()).thenReturn(sbn); + when(sbn.getNotification()).thenReturn(mNotification); + when(sbn.getPackageName()).thenReturn(TEST_PACKAGE_NAME); + mNotification.extras = bundle; + when(bundle.getParcelable(Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token.class)).thenReturn(token); + when(token.getBinder()).thenReturn(binder); } @Test @@ -227,10 +259,19 @@ public class MediaOutputControllerTest extends SysuiTestCase { } @Test - public void start_withPackageName_verifyMediaControllerInit() { + public void start_notificationNotFound_mediaControllerInitFromSession() { + mMediaOutputController.start(mCb); + + verify(mSessionMediaController).registerCallback(any()); + } + + @Test + public void start_MediaNotificationFound_mediaControllerNotInitFromSession() { + when(mNotification.isMediaNotification()).thenReturn(true); mMediaOutputController.start(mCb); - verify(mMediaController).registerCallback(any()); + verify(mSessionMediaController, never()).registerCallback(any()); + verifyZeroInteractions(mMediaSessionManager); } @Test @@ -239,11 +280,11 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); mMediaOutputController.start(mCb); - verify(mMediaController, never()).registerCallback(any()); + verify(mSessionMediaController, never()).registerCallback(any()); } @Test @@ -256,11 +297,11 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void stop_withPackageName_verifyMediaControllerDeinit() { mMediaOutputController.start(mCb); - reset(mMediaController); + reset(mSessionMediaController); mMediaOutputController.stop(); - verify(mMediaController).unregisterCallback(any()); + verify(mSessionMediaController).unregisterCallback(any()); } @Test @@ -269,19 +310,19 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); mMediaOutputController.start(mCb); mMediaOutputController.stop(); - verify(mMediaController, never()).unregisterCallback(any()); + verify(mSessionMediaController, never()).unregisterCallback(any()); } @Test public void stop_nearbyMediaDevicesManagerNotNull_unregistersNearbyDevicesCallback() { mMediaOutputController.start(mCb); - reset(mMediaController); + reset(mSessionMediaController); mMediaOutputController.stop(); @@ -358,24 +399,9 @@ public class MediaOutputControllerTest extends SysuiTestCase { } @Test - public void onDeviceListUpdate_verifyDeviceListCallback() { - mMediaOutputController.start(mCb); - reset(mCb); - - mMediaOutputController.onDeviceListUpdate(mMediaDevices); - final List<MediaDevice> devices = new ArrayList<>(mMediaOutputController.getMediaDevices()); - - assertThat(devices.containsAll(mMediaDevices)).isTrue(); - assertThat(devices.size()).isEqualTo(mMediaDevices.size()); - verify(mCb).onDeviceListChanged(); - } - - @Test public void routeProcessSupport_onDeviceListUpdate_preferenceExist_NotUpdatesRangeInformation() throws RemoteException { when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); - when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(true); - when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); mMediaOutputController.start(mCb); reset(mCb); @@ -387,8 +413,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { } @Test - public void advanced_onDeviceListUpdate_verifyDeviceListCallback() { - when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); + public void onDeviceListUpdate_verifyDeviceListCallback() { mMediaOutputController.start(mCb); reset(mCb); @@ -409,7 +434,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void advanced_onDeviceListUpdateWithConnectedDeviceRemote_verifyItemSize() { - when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); when(mMediaDevice1.getFeatures()).thenReturn( ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)); when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); @@ -433,7 +457,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void advanced_categorizeMediaItems_withSuggestedDevice_verifyDeviceListSize() { - when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); when(mMediaDevice1.isSuggestedDevice()).thenReturn(true); when(mMediaDevice2.isSuggestedDevice()).thenReturn(false); @@ -471,7 +494,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void advanced_onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() { - when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); mMediaOutputController.start(mCb); reset(mCb); mMediaOutputController.mIsRefreshing = true; @@ -509,7 +531,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); testMediaOutputController.start(mCb); reset(mCb); @@ -532,7 +554,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); testMediaOutputController.start(mCb); reset(mCb); @@ -568,7 +590,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager); testMediaOutputController.mLocalMediaManager = testLocalMediaManager; @@ -585,7 +607,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager); testMediaOutputController.mLocalMediaManager = testLocalMediaManager; @@ -673,7 +695,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void isAnyDeviceTransferring_advancedLayoutSupport() { - when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); when(mMediaDevice1.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_CONNECTING); mMediaOutputController.start(mCb); @@ -684,7 +705,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void isPlaying_stateIsNull() { - when(mMediaController.getPlaybackState()).thenReturn(null); + when(mSessionMediaController.getPlaybackState()).thenReturn(null); assertThat(mMediaOutputController.isPlaying()).isFalse(); } @@ -726,7 +747,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void getHeaderTitle_withoutMetadata_returnDefaultString() { - when(mMediaController.getMetadata()).thenReturn(null); + when(mSessionMediaController.getMetadata()).thenReturn(null); mMediaOutputController.start(mCb); @@ -736,7 +757,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void getHeaderTitle_withMetadata_returnSongName() { - when(mMediaController.getMetadata()).thenReturn(mMediaMetadata); + when(mSessionMediaController.getMetadata()).thenReturn(mMediaMetadata); mMediaOutputController.start(mCb); @@ -745,7 +766,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void getHeaderSubTitle_withoutMetadata_returnNull() { - when(mMediaController.getMetadata()).thenReturn(null); + when(mSessionMediaController.getMetadata()).thenReturn(null); mMediaOutputController.start(mCb); @@ -754,7 +775,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void getHeaderSubTitle_withMetadata_returnArtistName() { - when(mMediaController.getMetadata()).thenReturn(mMediaMetadata); + when(mSessionMediaController.getMetadata()).thenReturn(mMediaMetadata); mMediaOutputController.start(mCb); @@ -868,7 +889,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); assertThat(mMediaOutputController.getNotificationIcon()).isNull(); } @@ -1060,7 +1081,7 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); testMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java index 425d0bc47a4e..f3aee48cfb56 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java @@ -34,6 +34,7 @@ import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.PowerExemptionManager; +import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.FeatureFlagUtils; @@ -55,12 +56,14 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import java.util.ArrayList; import java.util.List; @@ -98,6 +101,7 @@ public class MediaOutputDialogTest extends SysuiTestCase { private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class); private KeyguardManager mKeyguardManager = mock(KeyguardManager.class); private FeatureFlags mFlags = mock(FeatureFlags.class); + private UserTracker mUserTracker = mock(UserTracker.class); private List<MediaController> mMediaControllers = new ArrayList<>(); private MediaOutputDialog mMediaOutputDialog; @@ -119,13 +123,17 @@ public class MediaOutputDialogTest extends SysuiTestCase { when(mMediaController.getMetadata()).thenReturn(mMediaMetadata); when(mMediaMetadata.getDescription()).thenReturn(mMediaDescription); mMediaControllers.add(mMediaController); - when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers); + final UserHandle userHandle = mock(UserHandle.class); + when(mUserTracker.getUserHandle()).thenReturn(userHandle); + when(mMediaSessionManager.getActiveSessionsForUser(any(), + Mockito.eq(userHandle))).thenReturn( + mMediaControllers); mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, mMediaSessionManager, mLocalBluetoothManager, mStarter, mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, - mKeyguardManager, mFlags); + mKeyguardManager, mFlags, mUserTracker); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputDialog = makeTestDialog(mMediaOutputController); mMediaOutputDialog.show(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index ca2b1da34766..349fac0bf6ea 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -22,6 +22,7 @@ import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.media.MediaRoute2Info import android.os.PowerManager +import android.os.VibrationAttributes import android.os.VibrationEffect import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -210,7 +211,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id) - verify(vibratorHelper).vibrate(any<VibrationEffect>()) + verify(vibratorHelper) + .vibrate( + any(), + any(), + any<VibrationEffect>(), + any(), + any<VibrationAttributes>(), + ) } @Test @@ -246,7 +254,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id) - verify(vibratorHelper).vibrate(any<VibrationEffect>()) + verify(vibratorHelper) + .vibrate( + any(), + any(), + any<VibrationEffect>(), + any(), + any<VibrationAttributes>(), + ) } @Test @@ -267,7 +282,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id) - verify(vibratorHelper).vibrate(any<VibrationEffect>()) + verify(vibratorHelper) + .vibrate( + any(), + any(), + any<VibrationEffect>(), + any(), + any<VibrationAttributes>(), + ) } @Test @@ -303,7 +325,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) assertThat(uiEventLoggerFake.eventId(0)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id) - verify(vibratorHelper).vibrate(any<VibrationEffect>()) + verify(vibratorHelper) + .vibrate( + any(), + any(), + any<VibrationEffect>(), + any(), + any<VibrationAttributes>(), + ) } @Test @@ -326,7 +355,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { // Event index 1 since initially displaying the triggered chip would also log an event. assertThat(uiEventLoggerFake.eventId(1)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id) - verify(vibratorHelper, never()).vibrate(any<VibrationEffect>()) + verify(vibratorHelper, never()) + .vibrate( + any(), + any(), + any<VibrationEffect>(), + any(), + any<VibrationAttributes>(), + ) } @Test @@ -403,7 +439,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { // Event index 1 since initially displaying the triggered chip would also log an event. assertThat(uiEventLoggerFake.eventId(1)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id) - verify(vibratorHelper, never()).vibrate(any<VibrationEffect>()) + verify(vibratorHelper, never()) + .vibrate( + any(), + any(), + any<VibrationEffect>(), + any(), + any<VibrationAttributes>(), + ) } @Test @@ -483,7 +526,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { // Event index 1 since initially displaying the triggered chip would also log an event. assertThat(uiEventLoggerFake.eventId(1)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id) - verify(vibratorHelper).vibrate(any<VibrationEffect>()) + verify(vibratorHelper) + .vibrate( + any(), + any(), + any<VibrationEffect>(), + any(), + any<VibrationAttributes>(), + ) } @Test @@ -511,7 +561,14 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { // Event index 1 since initially displaying the triggered chip would also log an event. assertThat(uiEventLoggerFake.eventId(1)) .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id) - verify(vibratorHelper).vibrate(any<VibrationEffect>()) + verify(vibratorHelper) + .vibrate( + any(), + any(), + any<VibrationEffect>(), + any(), + any<VibrationAttributes>(), + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index bd7898a5d986..e99f8b6aa47b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -25,8 +25,11 @@ import android.app.role.RoleManager.ROLE_NOTES import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.Intent.ACTION_CREATE_NOTE import android.content.Intent.ACTION_MAIN +import android.content.Intent.ACTION_MANAGE_DEFAULT_APP import android.content.Intent.CATEGORY_HOME +import android.content.Intent.EXTRA_USE_STYLUS_MODE import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT import android.content.Intent.FLAG_ACTIVITY_NEW_TASK @@ -47,8 +50,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.NoteTaskController.Companion.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE import com.android.systemui.notetask.NoteTaskController.Companion.SHORTCUT_ID import com.android.systemui.notetask.NoteTaskEntryPoint.APP_CLIPS +import com.android.systemui.notetask.NoteTaskEntryPoint.KEYBOARD_SHORTCUT import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON +import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity @@ -103,6 +108,8 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(context.packageManager).thenReturn(packageManager) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(NOTE_TASK_INFO) whenever(userManager.isUserUnlocked).thenReturn(true) + whenever(userManager.isUserUnlocked(any<Int>())).thenReturn(true) + whenever(userManager.isUserUnlocked(any<UserHandle>())).thenReturn(true) whenever( devicePolicyManager.getKeyguardDisabledFeatures( /* admin= */ eq(null), @@ -221,31 +228,22 @@ internal class NoteTaskControllerTest : SysuiTestCase() { // region showNoteTask @Test fun showNoteTask_keyguardIsLocked_shouldStartActivityAndLogUiEvent() { - val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isKeyguardLocked = true, - ) + val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) - createNoteTaskController() - .showNoteTask( - entryPoint = expectedInfo.entryPoint!!, - ) + createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) - assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) - .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) - .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) - assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() + assertThat(intentCaptor.value).run { + hasAction(ACTION_CREATE_NOTE) + hasPackage(NOTE_TASK_PACKAGE_NAME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) + hasFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() } assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) verify(eventLogger).logNoteTaskOpened(expectedInfo) @@ -256,32 +254,23 @@ internal class NoteTaskControllerTest : SysuiTestCase() { fun showNoteTaskWithUser_keyguardIsLocked_shouldStartActivityWithExpectedUserAndLogUiEvent() { val user10 = UserHandle.of(/* userId= */ 10) val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isKeyguardLocked = true, - user = user10, - ) + NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true, user = user10) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) createNoteTaskController() - .showNoteTaskAsUser( - entryPoint = expectedInfo.entryPoint!!, - user = user10, - ) + .showNoteTaskAsUser(entryPoint = expectedInfo.entryPoint!!, user = user10) val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) - assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) - .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) - .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) - assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() + assertThat(intentCaptor.value).run { + hasAction(ACTION_CREATE_NOTE) + hasPackage(NOTE_TASK_PACKAGE_NAME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) + hasFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() } assertThat(userCaptor.value).isEqualTo(user10) verify(eventLogger).logNoteTaskOpened(expectedInfo) @@ -290,11 +279,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_keyguardIsLocked_noteIsOpen_shouldCloseActivityAndLogUiEvent() { - val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isKeyguardLocked = true, - ) + val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = true) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) whenever(activityManager.getRunningTasks(anyInt())) @@ -305,10 +290,10 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(ACTION_MAIN) - assertThat(intent.categories).contains(CATEGORY_HOME) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) + assertThat(intentCaptor.value).run { + hasAction(ACTION_MAIN) + categories().contains(CATEGORY_HOME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) } assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) verify(eventLogger).logNoteTaskClosed(expectedInfo) @@ -317,18 +302,11 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_keyguardIsUnlocked_shouldStartBubblesWithoutLoggingUiEvent() { - val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - isKeyguardLocked = false, - ) + val expectedInfo = NOTE_TASK_INFO.copy(entryPoint = TAIL_BUTTON, isKeyguardLocked = false) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) - createNoteTaskController() - .showNoteTask( - entryPoint = expectedInfo.entryPoint!!, - ) + createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) // Context package name used to create bubble icon from drawable resource id verify(context).packageName @@ -338,10 +316,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_bubblesIsNull_shouldDoNothing() { - createNoteTaskController(bubbles = null) - .showNoteTask( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - ) + createNoteTaskController(bubbles = null).showNoteTask(entryPoint = TAIL_BUTTON) verifyZeroInteractions(context, bubbles, eventLogger) } @@ -352,7 +327,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val noteTaskController = spy(createNoteTaskController()) doNothing().whenever(noteTaskController).showNoDefaultNotesAppToast() - noteTaskController.showNoteTask(entryPoint = NoteTaskEntryPoint.TAIL_BUTTON) + noteTaskController.showNoteTask(entryPoint = TAIL_BUTTON) verify(noteTaskController).showNoDefaultNotesAppToast() verifyZeroInteractions(context, bubbles, eventLogger) @@ -360,10 +335,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_flagDisabled_shouldDoNothing() { - createNoteTaskController(isEnabled = false) - .showNoteTask( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - ) + createNoteTaskController(isEnabled = false).showNoteTask(entryPoint = TAIL_BUTTON) verifyZeroInteractions(context, bubbles, eventLogger) } @@ -372,10 +344,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { fun showNoteTask_userIsLocked_shouldDoNothing() { whenever(userManager.isUserUnlocked).thenReturn(false) - createNoteTaskController() - .showNoteTask( - entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, - ) + createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON) verifyZeroInteractions(context, bubbles, eventLogger) } @@ -383,30 +352,22 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_keyboardShortcut_shouldStartActivity() { val expectedInfo = - NOTE_TASK_INFO.copy( - entryPoint = NoteTaskEntryPoint.KEYBOARD_SHORTCUT, - isKeyguardLocked = true, - ) + NOTE_TASK_INFO.copy(entryPoint = KEYBOARD_SHORTCUT, isKeyguardLocked = true) whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo) - createNoteTaskController() - .showNoteTask( - entryPoint = expectedInfo.entryPoint!!, - ) + createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) - assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK) - .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK) - assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT) - .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT) - assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, true)).isFalse() + assertThat(intentCaptor.value).run { + hasAction(ACTION_CREATE_NOTE) + hasPackage(NOTE_TASK_PACKAGE_NAME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) + hasFlags(FLAG_ACTIVITY_MULTIPLE_TASK) + hasFlags(FLAG_ACTIVITY_NEW_DOCUMENT) + extras().bool(EXTRA_USE_STYLUS_MODE).isFalse() } assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) verify(eventLogger).logNoteTaskOpened(expectedInfo) @@ -583,7 +544,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.TAIL_BUTTON) + createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON) verifyNoteTaskOpenInBubbleInUser(workUserInfo.userHandle) } @@ -593,8 +554,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) - createNoteTaskController() - .showNoteTask(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT) + createNoteTaskController().showNoteTask(entryPoint = WIDGET_PICKER_SHORTCUT) verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) } @@ -604,7 +564,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) - createNoteTaskController().showNoteTask(entryPoint = NoteTaskEntryPoint.APP_CLIPS) + createNoteTaskController().showNoteTask(entryPoint = APP_CLIPS) verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) } @@ -615,13 +575,13 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val iconCaptor = argumentCaptor<Icon>() verify(bubbles) .showOrHideAppBubble(capture(intentCaptor), eq(userHandle), capture(iconCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE) - assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME) - assertThat(intent.flags).isEqualTo(FLAG_ACTIVITY_NEW_TASK) - assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue() + assertThat(intentCaptor.value).run { + hasAction(ACTION_CREATE_NOTE) + hasPackage(NOTE_TASK_PACKAGE_NAME) + hasFlags(FLAG_ACTIVITY_NEW_TASK) + extras().bool(EXTRA_USE_STYLUS_MODE).isTrue() } - iconCaptor.value.let { icon -> + iconCaptor.value?.let { icon -> assertThat(icon).isNotNull() assertThat(icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) } @@ -679,9 +639,10 @@ internal class NoteTaskControllerTest : SysuiTestCase() { verify(shortcutManager).updateShortcuts(actualShortcuts.capture()) val actualShortcut = actualShortcuts.value.first() assertThat(actualShortcut.id).isEqualTo(SHORTCUT_ID) - assertThat(actualShortcut.intent?.component?.className) - .isEqualTo(LaunchNoteTaskActivity::class.java.name) - assertThat(actualShortcut.intent?.action).isEqualTo(Intent.ACTION_CREATE_NOTE) + assertThat(actualShortcut.intent).run { + hasComponentClass(LaunchNoteTaskActivity::class.java) + hasAction(ACTION_CREATE_NOTE) + } assertThat(actualShortcut.shortLabel).isEqualTo(NOTE_TASK_SHORT_LABEL) assertThat(actualShortcut.isLongLived).isEqualTo(true) assertThat(actualShortcut.icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget) @@ -737,12 +698,9 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() verify(context).startActivityAsUser(intentCaptor.capture(), eq(user0)) - intentCaptor.value.let { intent -> - assertThat(intent) - .hasComponent( - ComponentName(context, LaunchNoteTaskManagedProfileProxyActivity::class.java) - ) - assertThat(intent).hasFlags(FLAG_ACTIVITY_NEW_TASK) + assertThat(intentCaptor.value).run { + hasComponentClass(LaunchNoteTaskManagedProfileProxyActivity::class.java) + hasFlags(FLAG_ACTIVITY_NEW_TASK) } } // endregion @@ -817,9 +775,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent).hasAction(Intent.ACTION_MANAGE_DEFAULT_APP) - } + assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) assertThat(userCaptor.value).isEqualTo(UserHandle.of(workUserInfo.id)) } @@ -833,9 +789,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent).hasAction(Intent.ACTION_MANAGE_DEFAULT_APP) - } + assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) assertThat(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) } @@ -848,9 +802,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent).hasAction(Intent.ACTION_MANAGE_DEFAULT_APP) - } + assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) assertThat(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) } @@ -863,9 +815,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { val intentCaptor = argumentCaptor<Intent>() val userCaptor = argumentCaptor<UserHandle>() verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) - intentCaptor.value.let { intent -> - assertThat(intent).hasAction(Intent.ACTION_MANAGE_DEFAULT_APP) - } + assertThat(intentCaptor.value).hasAction(ACTION_MANAGE_DEFAULT_APP) assertThat(userCaptor.value).isEqualTo(UserHandle.of(mainUserInfo.id)) } // endregion diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt index 28ed9d22a41b..4e85b6c555ef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -16,124 +16,169 @@ package com.android.systemui.notetask import android.app.role.RoleManager -import android.test.suitebuilder.annotation.SmallTest +import android.app.role.RoleManager.ROLE_NOTES +import android.os.UserHandle +import android.os.UserManager +import android.testing.AndroidTestingRunner import android.view.KeyEvent -import androidx.test.runner.AndroidJUnit4 +import android.view.KeyEvent.ACTION_DOWN +import android.view.KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.SysuiTestCase import com.android.systemui.settings.FakeUserTracker import com.android.systemui.statusbar.CommandQueue import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.android.wm.shell.bubbles.Bubbles import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.never +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.MockitoAnnotations +import org.mockito.MockitoAnnotations.initMocks /** atest SystemUITests:NoteTaskInitializerTest */ +@OptIn(ExperimentalCoroutinesApi::class, InternalNoteTaskApi::class) @SmallTest -@RunWith(AndroidJUnit4::class) +@RunWith(AndroidTestingRunner::class) internal class NoteTaskInitializerTest : SysuiTestCase() { @Mock lateinit var commandQueue: CommandQueue @Mock lateinit var bubbles: Bubbles @Mock lateinit var controller: NoteTaskController @Mock lateinit var roleManager: RoleManager - private val clock = FakeSystemClock() - private val executor = FakeExecutor(clock) + @Mock lateinit var userManager: UserManager + @Mock lateinit var keyguardMonitor: KeyguardUpdateMonitor + + private val executor = FakeExecutor(FakeSystemClock()) private val userTracker = FakeUserTracker() @Before fun setUp() { - MockitoAnnotations.initMocks(this) + initMocks(this) + whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true) } - private fun createNoteTaskInitializer( - isEnabled: Boolean = true, - bubbles: Bubbles? = this.bubbles, - ): NoteTaskInitializer { - return NoteTaskInitializer( + private fun createUnderTest( + isEnabled: Boolean, + bubbles: Bubbles?, + ): NoteTaskInitializer = + NoteTaskInitializer( controller = controller, commandQueue = commandQueue, optionalBubbles = Optional.ofNullable(bubbles), isEnabled = isEnabled, roleManager = roleManager, - backgroundExecutor = executor, userTracker = userTracker, + keyguardUpdateMonitor = keyguardMonitor, + backgroundExecutor = executor, ) + + @Test + fun initialize_withUserUnlocked() { + whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true) + + createUnderTest(isEnabled = true, bubbles = bubbles).initialize() + + verify(commandQueue).addCallback(any()) + verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any()) + verify(controller).setNoteTaskShortcutEnabled(any(), any()) + verify(keyguardMonitor, never()).registerCallback(any()) } - // region initializer @Test - fun initialize() { - createNoteTaskInitializer().initialize() + fun initialize_withUserLocked() { + whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(false) + + createUnderTest(isEnabled = true, bubbles = bubbles).initialize() - verify(controller).setNoteTaskShortcutEnabled(eq(true), eq(userTracker.userHandle)) verify(commandQueue).addCallback(any()) verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any()) + verify(controller, never()).setNoteTaskShortcutEnabled(any(), any()) + verify(keyguardMonitor).registerCallback(any()) } @Test fun initialize_flagDisabled() { - createNoteTaskInitializer(isEnabled = false).initialize() + val underTest = createUnderTest(isEnabled = false, bubbles = bubbles) - verify(controller, never()).setNoteTaskShortcutEnabled(any(), any()) - verify(commandQueue, never()).addCallback(any()) - verify(roleManager, never()).addOnRoleHoldersChangedListenerAsUser(any(), any(), any()) + underTest.initialize() + + verifyZeroInteractions( + commandQueue, + bubbles, + controller, + roleManager, + userManager, + keyguardMonitor, + ) } @Test fun initialize_bubblesNotPresent() { - createNoteTaskInitializer(bubbles = null).initialize() + val underTest = createUnderTest(isEnabled = true, bubbles = null) - verify(controller, never()).setNoteTaskShortcutEnabled(any(), any()) - verify(commandQueue, never()).addCallback(any()) - verify(roleManager, never()).addOnRoleHoldersChangedListenerAsUser(any(), any(), any()) + underTest.initialize() + + verifyZeroInteractions( + commandQueue, + bubbles, + controller, + roleManager, + userManager, + keyguardMonitor, + ) } - // endregion - // region handleSystemKey @Test - fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() { - createNoteTaskInitializer() - .callbacks - .handleSystemKey(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL)) + fun initialize_handleSystemKey() { + val expectedKeyEvent = KeyEvent(ACTION_DOWN, KEYCODE_STYLUS_BUTTON_TAIL) + val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) + underTest.initialize() + val callback = captureArgument { verify(commandQueue).addCallback(capture()) } + + callback.handleSystemKey(expectedKeyEvent) - verify(controller).showNoteTask(entryPoint = NoteTaskEntryPoint.TAIL_BUTTON) + verify(controller).showNoteTask(any()) } @Test - fun handleSystemKey_receiveKeyboardShortcut_shouldShowNoteTask() { - createNoteTaskInitializer() - .callbacks - .handleSystemKey( - KeyEvent( - 0, - 0, - KeyEvent.ACTION_DOWN, - KeyEvent.KEYCODE_N, - 0, - KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON - ) - ) - - verify(controller).showNoteTask(entryPoint = NoteTaskEntryPoint.KEYBOARD_SHORTCUT) + fun initialize_userUnlocked() { + whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(false) + val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) + underTest.initialize() + val callback = captureArgument { verify(keyguardMonitor).registerCallback(capture()) } + whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true) + + callback.onUserUnlocked() + verify(controller).setNoteTaskShortcutEnabled(any(), any()) } @Test - fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { - createNoteTaskInitializer() - .callbacks - .handleSystemKey(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_UNKNOWN)) + fun initialize_onRoleHoldersChanged() { + val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) + underTest.initialize() + val callback = captureArgument { + verify(roleManager) + .addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL)) + } - verifyZeroInteractions(controller) + callback.onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle) + + verify(controller).onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle) } - // endregion } + +private inline fun <reified T : Any> captureArgument(block: ArgumentCaptor<T>.() -> Unit) = + argumentCaptor<T>().apply(block).value diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java index 35c8cc70953d..87892539ccfe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java @@ -28,8 +28,11 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.test.suitebuilder.annotation.SmallTest; +import android.testing.TestableLooper; +import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import androidx.test.runner.AndroidJUnit4; @@ -48,6 +51,7 @@ import org.mockito.Mockito; @SmallTest @RunWith(AndroidJUnit4.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) public class TileLayoutTest extends SysuiTestCase { private Resources mResources; private int mLayoutSizeForOneTile; @@ -228,4 +232,53 @@ public class TileLayoutTest extends SysuiTestCase { assertEquals(false, mTileLayout.updateResources()); } + + @Test + public void fontScalingChanged_updateResources_cellHeightEnoughForTileContent() { + final float originalFontScale = mContext.getResources().getConfiguration().fontScale; + float[] testScales = {0.8f, 1.0f, 1.4f, 1.6f, 2.0f}; + for (float scale: testScales) { + changeFontScaling_updateResources_cellHeightEnoughForTileContent(scale); + } + + changeFontScaling(originalFontScale); + } + + private void changeFontScaling_updateResources_cellHeightEnoughForTileContent(float scale) { + changeFontScaling(scale); + + QSPanelControllerBase.TileRecord tileRecord = createTileRecord(); + mTileLayout.addTile(tileRecord); + + FakeTileView tileView = new FakeTileView(mContext); + QSTile.State state = new QSTile.State(); + state.label = "TEST LABEL"; + state.secondaryLabel = "TEST SECONDARY LABEL"; + tileView.changeState(state); + + mTileLayout.updateResources(); + + int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + tileView.measure(spec, spec); + assertTrue(mTileLayout.getCellHeight() >= tileView.getMeasuredHeight()); + + mTileLayout.removeTile(tileRecord); + } + + private static class FakeTileView extends QSTileViewImpl { + FakeTileView(Context context) { + super(context, new QSIconViewImpl(context), /* collapsed= */ false); + } + + void changeState(QSTile.State state) { + handleStateChanged(state); + } + } + + private void changeFontScaling(float scale) { + Configuration configuration = new Configuration(mContext.getResources().getConfiguration()); + configuration.fontScale = scale; + // updateConfiguration could help update on both resource configuration and displayMetrics + mContext.getResources().updateConfiguration(configuration, null, null); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java index 84cc977ce676..66143923132b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java @@ -43,6 +43,7 @@ import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; +import android.telephony.TelephonyCallback; import android.telephony.TelephonyDisplayInfo; import android.telephony.TelephonyManager; import android.testing.AndroidTestingRunner; @@ -961,6 +962,26 @@ public class InternetDialogControllerTest extends SysuiTestCase { assertThat(dds).contains(mContext.getString(R.string.carrier_network_change_mode)); } + @Test + public void onStop_cleanUp() { + doReturn(SUB_ID).when(mTelephonyManager).getSubscriptionId(); + assertThat(mInternetDialogController.mSubIdTelephonyManagerMap.get(SUB_ID)).isEqualTo( + mTelephonyManager); + assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.get(SUB_ID)).isNotNull(); + + mInternetDialogController.onStop(); + + verify(mTelephonyManager).unregisterTelephonyCallback(any(TelephonyCallback.class)); + assertThat(mInternetDialogController.mSubIdTelephonyDisplayInfoMap.isEmpty()).isTrue(); + assertThat(mInternetDialogController.mSubIdTelephonyManagerMap.isEmpty()).isTrue(); + assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.isEmpty()).isTrue(); + verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(mInternetDialogController + .mOnSubscriptionsChangedListener); + verify(mAccessPointController).removeAccessPointCallback(mInternetDialogController); + verify(mConnectivityManager).unregisterNetworkCallback( + any(ConnectivityManager.NetworkCallback.class)); + } + private String getResourcesString(String name) { return mContext.getResources().getString(getResourcesId(name)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt new file mode 100644 index 000000000000..105387d49bd4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1 +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +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.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class QuickSettingsSceneViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), + ) + + private val underTest = + QuickSettingsSceneViewModel( + lockscreenSceneInteractorFactory = + object : LockscreenSceneInteractor.Factory { + override fun create(containerName: String): LockscreenSceneInteractor { + return utils.lockScreenSceneInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ), + ) + } + }, + containerName = CONTAINER_1 + ) + + @Test + fun onContentClicked_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java index 9acd47e4378f..55813f60aecd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java @@ -17,8 +17,10 @@ package com.android.systemui.reardisplay; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotSame; import static junit.framework.Assert.assertTrue; +import android.content.res.Configuration; import android.hardware.devicestate.DeviceStateManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -68,6 +70,27 @@ public class RearDisplayDialogControllerTest extends SysuiTestCase { } @Test + public void testClosedDialogIsRefreshedOnConfigurationChange() { + RearDisplayDialogController controller = new RearDisplayDialogController(mContext, + mCommandQueue, mFakeExecutor); + controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback()); + controller.setFoldedStates(new int[]{0}); + controller.setAnimationRepeatCount(0); + + controller.showRearDisplayDialog(CLOSED_BASE_STATE); + assertTrue(controller.mRearDisplayEducationDialog.isShowing()); + TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById( + R.id.rear_display_title_text_view); + + controller.onConfigurationChanged(new Configuration()); + assertTrue(controller.mRearDisplayEducationDialog.isShowing()); + TextView deviceClosedTitleTextView2 = controller.mRearDisplayEducationDialog.findViewById( + R.id.rear_display_title_text_view); + + assertNotSame(deviceClosedTitleTextView, deviceClosedTitleTextView2); + } + + @Test public void testOpenDialogIsShown() { RearDisplayDialogController controller = new RearDisplayDialogController(mContext, mCommandQueue, mFakeExecutor); diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt deleted file mode 100644 index 1cdaec0c6581..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt +++ /dev/null @@ -1,51 +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.scene.data.repository - -import com.android.systemui.scene.data.model.SceneContainerConfig -import com.android.systemui.scene.shared.model.SceneKey - -fun fakeSceneContainerRepository( - containerConfigurations: Set<SceneContainerConfig> = - setOf( - fakeSceneContainerConfig("container1"), - fakeSceneContainerConfig("container2"), - ) -): SceneContainerRepository { - return SceneContainerRepository(containerConfigurations) -} - -fun fakeSceneKeys(): List<SceneKey> { - return listOf( - SceneKey.QuickSettings, - SceneKey.Shade, - SceneKey.LockScreen, - SceneKey.Bouncer, - SceneKey.Gone, - ) -} - -fun fakeSceneContainerConfig( - name: String, - sceneKeys: List<SceneKey> = fakeSceneKeys(), -): SceneContainerConfig { - return SceneContainerConfig( - name = name, - sceneKeys = sceneKeys, - initialSceneKey = SceneKey.LockScreen, - ) -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index 9e264db845e1..de15c7711bb3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -21,6 +21,7 @@ package com.android.systemui.scene.data.repository import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -34,15 +35,17 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class SceneContainerRepositoryTest : SysuiTestCase() { + private val utils = SceneTestUtils(this) + @Test fun allSceneKeys() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() assertThat(underTest.allSceneKeys("container1")) .isEqualTo( listOf( SceneKey.QuickSettings, SceneKey.Shade, - SceneKey.LockScreen, + SceneKey.Lockscreen, SceneKey.Bouncer, SceneKey.Gone, ) @@ -51,15 +54,15 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun allSceneKeys_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.allSceneKeys("nonExistingContainer") } @Test fun currentScene() = runTest { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() val currentScene by collectLastValue(underTest.currentScene("container1")) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) underTest.setCurrentScene("container1", SceneModel(SceneKey.Shade)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) @@ -67,25 +70,25 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun currentScene_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.currentScene("nonExistingContainer") } @Test(expected = IllegalStateException::class) fun setCurrentScene_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.setCurrentScene("nonExistingContainer", SceneModel(SceneKey.Shade)) } @Test(expected = IllegalStateException::class) fun setCurrentScene_noSuchSceneInContainer_throws() { val underTest = - fakeSceneContainerRepository( + utils.fakeSceneContainerRepository( setOf( - fakeSceneContainerConfig("container1"), - fakeSceneContainerConfig( + utils.fakeSceneContainerConfig("container1"), + utils.fakeSceneContainerConfig( "container2", - listOf(SceneKey.QuickSettings, SceneKey.LockScreen) + listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) ), ) ) @@ -94,7 +97,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test fun isVisible() = runTest { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() val isVisible by collectLastValue(underTest.isVisible("container1")) assertThat(isVisible).isTrue() @@ -107,19 +110,19 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun isVisible_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.isVisible("nonExistingContainer") } @Test(expected = IllegalStateException::class) fun setVisible_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.setVisible("nonExistingContainer", false) } @Test fun sceneTransitionProgress() = runTest { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() val sceneTransitionProgress by collectLastValue(underTest.sceneTransitionProgress("container1")) assertThat(sceneTransitionProgress).isEqualTo(1f) @@ -133,7 +136,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun sceneTransitionProgress_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.sceneTransitionProgress("nonExistingContainer") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index c5ce09246862..ee4f6c23ca8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -21,8 +21,7 @@ package com.android.systemui.scene.domain.interactor import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.data.repository.fakeSceneKeys +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -36,20 +35,18 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class SceneInteractorTest : SysuiTestCase() { - private val underTest = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) + private val utils = SceneTestUtils(this) + private val underTest = utils.sceneInteractor() @Test fun allSceneKeys() { - assertThat(underTest.allSceneKeys("container1")).isEqualTo(fakeSceneKeys()) + assertThat(underTest.allSceneKeys("container1")).isEqualTo(utils.fakeSceneKeys()) } @Test fun sceneTransitions() = runTest { val currentScene by collectLastValue(underTest.currentScene("container1")) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) underTest.setCurrentScene("container1", SceneModel(SceneKey.Shade)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index ab61ddddaeab..cd2f5af592cf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -21,9 +21,7 @@ package com.android.systemui.scene.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.data.repository.fakeSceneKeys -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -36,10 +34,9 @@ import org.junit.runners.JUnit4 @SmallTest @RunWith(JUnit4::class) class SceneContainerViewModelTest : SysuiTestCase() { - private val interactor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) + + private val utils = SceneTestUtils(this) + private val interactor = utils.sceneInteractor() private val underTest = SceneContainerViewModel( interactor = interactor, @@ -60,13 +57,13 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun allSceneKeys() { - assertThat(underTest.allSceneKeys).isEqualTo(fakeSceneKeys()) + assertThat(underTest.allSceneKeys).isEqualTo(utils.fakeSceneKeys()) } @Test fun sceneTransition() = runTest { val currentScene by collectLastValue(underTest.currentScene) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) underTest.setCurrentScene(SceneModel(SceneKey.Shade)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java index 3c08d58cbb67..27eec801ef62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java @@ -37,8 +37,8 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; +import android.os.Process; import android.os.ResultReceiver; -import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.widget.ImageView; @@ -120,7 +120,8 @@ public final class AppClipsActivityTest extends SysuiTestCase { ImageExporter.Result result = new ImageExporter.Result(); result.uri = TEST_URI; when(mImageExporter.export(any(Executor.class), any(UUID.class), any(Bitmap.class), - any(UserHandle.class))).thenReturn(Futures.immediateFuture(result)); + eq(Process.myUserHandle()))) + .thenReturn(Futures.immediateFuture(result)); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java index cbd9dba3cdbf..e9007ff84f13 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java @@ -27,6 +27,7 @@ import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_USE_WP_USER; import static com.google.common.truth.Truth.assertThat; @@ -74,6 +75,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; import java.util.Optional; @RunWith(AndroidTestingRunner.class) @@ -82,7 +84,6 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { private static final String TEST_URI_STRING = "www.test-uri.com"; private static final Uri TEST_URI = Uri.parse(TEST_URI_STRING); private static final int TEST_UID = 42; - private static final int TEST_USER_ID = 43; private static final String TEST_CALLING_PACKAGE = "test-calling-package"; @Mock @@ -287,6 +288,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualIntent.getComponent()).isEqualTo( new ComponentName(mContext, AppClipsTrampolineActivity.class)); assertThat(actualIntent.getFlags()).isEqualTo(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + assertThat(actualIntent.getBooleanExtra(EXTRA_USE_WP_USER, false)).isTrue(); assertThat(activity.mStartingUser).isEqualTo(UserHandle.SYSTEM); } @@ -313,13 +315,13 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { when(mOptionalBubbles.get()).thenReturn(mBubbles); when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true); when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); - when(mUserTracker.getUserId()).thenReturn(TEST_USER_ID); + when(mUserTracker.getUserProfiles()).thenReturn(List.of()); ApplicationInfo testApplicationInfo = new ApplicationInfo(); testApplicationInfo.uid = TEST_UID; when(mPackageManager.getApplicationInfoAsUser(eq(TEST_CALLING_PACKAGE), any(ApplicationInfoFlags.class), - eq(TEST_USER_ID))).thenReturn(testApplicationInfo); + eq(mContext.getUser().getIdentifier()))).thenReturn(testApplicationInfo); } public static final class AppClipsTrampolineActivityTestable extends diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java index e7c3c0578627..b7b8b11ba887 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java @@ -31,6 +31,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.net.Uri; +import android.os.Process; import android.os.UserHandle; import androidx.test.runner.AndroidJUnit4; @@ -57,10 +58,10 @@ public final class AppClipsViewModelTest extends SysuiTestCase { private static final Drawable FAKE_DRAWABLE = new ShapeDrawable(); private static final Rect FAKE_RECT = new Rect(); private static final Uri FAKE_URI = Uri.parse("www.test-uri.com"); + private static final UserHandle USER_HANDLE = Process.myUserHandle(); @Mock private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; @Mock private ImageExporter mImageExporter; - private AppClipsViewModel mViewModel; @Before @@ -99,10 +100,10 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_throwsError_shouldUpdateErrorWithFailed() { when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), - any(UserHandle.class))).thenReturn( + eq(USER_HANDLE))).thenReturn( Futures.immediateFailedFuture(new ExecutionException(new Throwable()))); - mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); + mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); assertThat(mViewModel.getErrorLiveData().getValue()) @@ -113,10 +114,9 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_failsSilently_shouldUpdateErrorWithFailed() { when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), - any(UserHandle.class))).thenReturn( - Futures.immediateFuture(new ImageExporter.Result())); + eq(USER_HANDLE))).thenReturn(Futures.immediateFuture(new ImageExporter.Result())); - mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); + mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); assertThat(mViewModel.getErrorLiveData().getValue()) @@ -129,9 +129,9 @@ public final class AppClipsViewModelTest extends SysuiTestCase { ImageExporter.Result result = new ImageExporter.Result(); result.uri = FAKE_URI; when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), - any(UserHandle.class))).thenReturn(Futures.immediateFuture(result)); + eq(USER_HANDLE))).thenReturn(Futures.immediateFuture(result)); - mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); + mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); assertThat(mViewModel.getErrorLiveData().getValue()).isNull(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 9a8ec88e992a..fe89a143e880 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -679,6 +679,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mFeatureFlags, mInteractionJankMonitor, mShadeLog, + mDumpManager, mKeyguardFaceAuthInteractor, mShadeRepository, mCastController diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 48e0b53fc931..a5a9de54c558 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -902,6 +902,13 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + public void isExpandingOrCollapsing_returnsTrue_whenQsLockscreenDragInProgress() { + when(mQsController.getLockscreenShadeDragProgress()).thenReturn(0.5f); + assertThat(mNotificationPanelViewController.isExpandingOrCollapsing()).isTrue(); + } + + + @Test public void getMaxPanelTransitionDistance_inSplitShade_withHeadsUp_returnsBiggerValue() { enableSplitShade(true); mNotificationPanelViewController.expandToQs(); @@ -1099,7 +1106,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test - public void shadeExpanded_inShadeState() { + public void shadeFullyExpanded_inShadeState() { mStatusBarStateController.setState(SHADE); mNotificationPanelViewController.setExpandedHeight(0); @@ -1111,7 +1118,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test - public void shadeExpanded_onKeyguard() { + public void shadeFullyExpanded_onKeyguard() { mStatusBarStateController.setState(KEYGUARD); int transitionDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance(); @@ -1120,8 +1127,39 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test - public void shadeExpanded_onShadeLocked() { + public void shadeFullyExpanded_onShadeLocked() { mStatusBarStateController.setState(SHADE_LOCKED); assertThat(mNotificationPanelViewController.isShadeFullyExpanded()).isTrue(); } + + @Test + public void shadeExpanded_whenHasHeight() { + int transitionDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance(); + mNotificationPanelViewController.setExpandedHeight(transitionDistance); + assertThat(mNotificationPanelViewController.isExpanded()).isTrue(); + } + + @Test + public void shadeExpanded_whenInstantExpanding() { + mNotificationPanelViewController.expand(true); + assertThat(mNotificationPanelViewController.isExpanded()).isTrue(); + } + + @Test + public void shadeExpanded_whenHunIsPresent() { + when(mHeadsUpManager.hasPinnedHeadsUp()).thenReturn(true); + assertThat(mNotificationPanelViewController.isExpanded()).isTrue(); + } + + @Test + public void shadeExpanded_whenWaitingForExpandGesture() { + mNotificationPanelViewController.startWaitingForExpandGesture(); + assertThat(mNotificationPanelViewController.isExpanded()).isTrue(); + } + + @Test + public void shadeExpanded_whenUnlockedOffscreenAnimationRunning() { + when(mUnlockedScreenOffAnimationController.isAnimationPlaying()).thenReturn(true); + assertThat(mNotificationPanelViewController.isExpanded()).isTrue(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt index dfb1bce20ff8..168cbb7b8da3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt @@ -229,25 +229,6 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { } @Test - fun testCustomizingInSinglePaneShade() { - disableSplitShade() - controller.setCustomizerShowing(true) - - // always sets spacings to 0 - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = 0) - - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = emptyInsets()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = 0) - } - - @Test fun testDetailShowingInSinglePaneShade() { disableSplitShade() controller.setDetailShowing(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java index 1cf38732fdb2..34d09a912c87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java @@ -247,6 +247,7 @@ public class QuickSettingsControllerTest extends SysuiTestCase { mFeatureFlags, mInteractionJankMonitor, mShadeLogger, + mDumpManager, mock(KeyguardFaceAuthInteractor.class), mock(ShadeRepository.class), mCastController diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt index a601b678c905..15c04eb2e2bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt @@ -35,7 +35,8 @@ class ShadeExpansionStateManagerTest : SysuiTestCase() { @Test fun onPanelExpansionChanged_listenerNotified() { val listener = TestShadeExpansionListener() - shadeExpansionStateManager.addExpansionListener(listener) + val currentState = shadeExpansionStateManager.addExpansionListener(listener) + listener.onPanelExpansionChanged(currentState) val fraction = 0.6f val expanded = true val tracking = true @@ -68,7 +69,8 @@ class ShadeExpansionStateManagerTest : SysuiTestCase() { ) val listener = TestShadeExpansionListener() - shadeExpansionStateManager.addExpansionListener(listener) + val currentState = shadeExpansionStateManager.addExpansionListener(listener) + listener.onPanelExpansionChanged(currentState) assertThat(listener.fraction).isEqualTo(fraction) assertThat(listener.expanded).isEqualTo(expanded) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java index 2ef3d60c7754..57ae621f01ef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -109,6 +110,8 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { .thenReturn(mCarrierTextControllerBuilder); when(mCarrierTextControllerBuilder.setShowMissingSim(anyBoolean())) .thenReturn(mCarrierTextControllerBuilder); + when(mCarrierTextControllerBuilder.setDebugLocationString(anyString())) + .thenReturn(mCarrierTextControllerBuilder); when(mCarrierTextControllerBuilder.build()).thenReturn(mCarrierTextManager); doAnswer(invocation -> mCallback = invocation.getArgument(0)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt index 8f2c93b3bf43..e26a8bd5ec8c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.shade.domain.model.ShadeModel +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -37,6 +38,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @@ -56,6 +58,9 @@ class ShadeRepositoryImplTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) underTest = ShadeRepositoryImpl(shadeExpansionStateManager) + `when`(shadeExpansionStateManager.addExpansionListener(any())).thenReturn( + ShadeExpansionChangeEvent(0f, false, false, 0f) + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt new file mode 100644 index 000000000000..69d03d9b0e4c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -0,0 +1,115 @@ +/* + * 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.shade.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1 +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +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.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class ShadeSceneViewModelTest : SysuiTestCase() { + + private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), + ) + + private val underTest = + ShadeSceneViewModel( + applicationScope = testScope.backgroundScope, + lockscreenSceneInteractorFactory = + object : LockscreenSceneInteractor.Factory { + override fun create(containerName: String): LockscreenSceneInteractor { + return utils.lockScreenSceneInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ), + ) + } + }, + containerName = SceneTestUtils.CONTAINER_1 + ) + + @Test + fun upTransitionSceneKey_deviceLocked_lockScreen() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Lockscreen) + } + + @Test + fun upTransitionSceneKey_deviceUnlocked_gone() = + testScope.runTest { + val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() + + assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) + } + + @Test + fun onContentClicked_deviceUnlocked_switchesToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) + } + + @Test + fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() + runCurrent() + + underTest.onContentClicked() + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java index de5824d1f463..b7241759ca24 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java @@ -32,6 +32,7 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.log.TableLogBufferBase; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -44,6 +45,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import kotlinx.coroutines.CoroutineScope; @@ -59,6 +61,8 @@ public class ConditionMonitorTest extends SysuiTestCase { @Mock private CoroutineScope mScope; + @Mock + private TableLogBufferBase mLogBuffer; private Monitor mConditionMonitor; @@ -630,4 +634,42 @@ public class ConditionMonitorTest extends SysuiTestCase { verify(callback).onActiveChanged(eq(true)); verify(callback).onConditionsChanged(eq(true)); } + + @Test + public void testLoggingCallback() { + final Monitor monitor = new Monitor(mExecutor, Collections.emptySet(), mLogBuffer); + + final FakeCondition condition = new FakeCondition(mScope); + final FakeCondition overridingCondition = new FakeCondition( + mScope, + /* initialValue= */ false, + /* overriding= */ true); + + final Monitor.Callback callback = mock(Monitor.Callback.class); + monitor.addSubscription(getDefaultBuilder(callback) + .addCondition(condition) + .addCondition(overridingCondition) + .build()); + mExecutor.runAllReady(); + + // condition set to true + condition.fakeUpdateCondition(true); + mExecutor.runAllReady(); + verify(mLogBuffer).logChange("", "FakeCondition", "True"); + + // condition set to false + condition.fakeUpdateCondition(false); + mExecutor.runAllReady(); + verify(mLogBuffer).logChange("", "FakeCondition", "False"); + + // condition unset + condition.fakeClearCondition(); + mExecutor.runAllReady(); + verify(mLogBuffer).logChange("", "FakeCondition", "Invalid"); + + // overriding condition set to true + overridingCondition.fakeUpdateCondition(true); + mExecutor.runAllReady(); + verify(mLogBuffer).logChange("", "FakeCondition[OVRD]", "True"); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java index a325cbf25ffe..5416536305fc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java @@ -47,4 +47,8 @@ public class FakeCondition extends Condition { public void fakeUpdateCondition(boolean isConditionMet) { updateCondition(isConditionMet); } + + public void fakeClearCondition() { + clearCondition(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt index ae1c8cbe2a65..1031621e2e7e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt @@ -1,17 +1,30 @@ package com.android.systemui.shared.regionsampling +import android.app.WallpaperColors import android.app.WallpaperManager +import android.graphics.Color +import android.graphics.RectF import android.testing.AndroidTestingRunner import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.util.concurrent.Executor +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.eq +import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit @@ -26,25 +39,188 @@ class RegionSamplerTest : SysuiTestCase() { @Mock private lateinit var bgExecutor: Executor @Mock private lateinit var pw: PrintWriter @Mock private lateinit var wallpaperManager: WallpaperManager + @Mock private lateinit var updateForegroundColor: UpdateColorCallback - private lateinit var mRegionSampler: RegionSampler - private var updateFun: UpdateColorCallback = {} + private lateinit var regionSampler: RegionSampler // lockscreen + private lateinit var homescreenRegionSampler: RegionSampler + + @Captor + private lateinit var colorsChangedListener: + ArgumentCaptor<WallpaperManager.LocalWallpaperColorConsumer> + + @Captor private lateinit var layoutChangedListener: ArgumentCaptor<View.OnLayoutChangeListener> @Before fun setUp() { whenever(sampledView.isAttachedToWindow).thenReturn(true) + whenever(sampledView.width).thenReturn(100) + whenever(sampledView.height).thenReturn(100) + whenever(sampledView.isLaidOut).thenReturn(true) + whenever(sampledView.locationOnScreen).thenReturn(intArrayOf(0, 0)) + + regionSampler = + RegionSampler( + sampledView, + mainExecutor, + bgExecutor, + regionSamplingEnabled = true, + isLockscreen = true, + wallpaperManager, + updateForegroundColor + ) + regionSampler.displaySize.set(1080, 2050) + + // TODO(b/265969235): test sampling on home screen via WallpaperManager.FLAG_SYSTEM + homescreenRegionSampler = + RegionSampler( + sampledView, + mainExecutor, + bgExecutor, + regionSamplingEnabled = true, + isLockscreen = false, + wallpaperManager, + updateForegroundColor + ) + } + + @Test + fun testCalculatedBounds_inRange() { + // test calculations return region within [0,1] + sampledView.setLeftTopRightBottom(100, 100, 200, 200) + var fractionalBounds = + regionSampler.calculateScreenLocation(sampledView)?.let { + regionSampler.convertBounds(it) + } + + assertTrue(fractionalBounds?.left!! >= 0.0f) + assertTrue(fractionalBounds.right <= 1.0f) + assertTrue(fractionalBounds.top >= 0.0f) + assertTrue(fractionalBounds.bottom <= 1.0f) + } + + @Test + fun testEmptyView_returnsEarly() { + sampledView.setLeftTopRightBottom(0, 0, 0, 0) + whenever(sampledView.width).thenReturn(0) + whenever(sampledView.height).thenReturn(0) + regionSampler.startRegionSampler() + // returns early so should never call this function + verify(wallpaperManager, never()) + .addOnColorsChangedListener( + any(WallpaperManager.LocalWallpaperColorConsumer::class.java), + any(), + any() + ) + } + + @Test + fun testLayoutChange_notifiesListener() { + regionSampler.startRegionSampler() + // don't count addOnColorsChangedListener() call made in startRegionSampler() + clearInvocations(wallpaperManager) - mRegionSampler = - RegionSampler(sampledView, mainExecutor, bgExecutor, true, updateFun, wallpaperManager) + verify(sampledView).addOnLayoutChangeListener(capture(layoutChangedListener)) + layoutChangedListener.value.onLayoutChange( + sampledView, + 300, + 300, + 400, + 400, + 100, + 100, + 200, + 200 + ) + verify(sampledView).removeOnLayoutChangeListener(layoutChangedListener.value) + verify(wallpaperManager) + .removeOnColorsChangedListener( + any(WallpaperManager.LocalWallpaperColorConsumer::class.java) + ) + verify(wallpaperManager) + .addOnColorsChangedListener( + any(WallpaperManager.LocalWallpaperColorConsumer::class.java), + any(), + any() + ) } @Test - fun testStartRegionSampler() { - mRegionSampler.startRegionSampler() + fun testColorsChanged_triggersCallback() { + regionSampler.startRegionSampler() + verify(wallpaperManager) + .addOnColorsChangedListener( + capture(colorsChangedListener), + any(), + eq(WallpaperManager.FLAG_LOCK) + ) + setWhiteWallpaper() + verify(updateForegroundColor).invoke() + } + + @Test + fun testRegionDarkness() { + regionSampler.startRegionSampler() + verify(wallpaperManager) + .addOnColorsChangedListener( + capture(colorsChangedListener), + any(), + eq(WallpaperManager.FLAG_LOCK) + ) + + // should detect dark region + setBlackWallpaper() + assertThat(regionSampler.currentRegionDarkness()).isEqualTo(RegionDarkness.DARK) + + // should detect light region + setWhiteWallpaper() + assertThat(regionSampler.currentRegionDarkness()).isEqualTo(RegionDarkness.LIGHT) + } + + @Test + fun testForegroundColor() { + regionSampler.setForegroundColors(Color.WHITE, Color.BLACK) + regionSampler.startRegionSampler() + verify(wallpaperManager) + .addOnColorsChangedListener( + capture(colorsChangedListener), + any(), + eq(WallpaperManager.FLAG_LOCK) + ) + + // dark background, light text + setBlackWallpaper() + assertThat(regionSampler.currentForegroundColor()).isEqualTo(Color.WHITE) + + // light background, dark text + setWhiteWallpaper() + assertThat(regionSampler.currentForegroundColor()).isEqualTo(Color.BLACK) + } + + private fun setBlackWallpaper() { + val wallpaperColors = + WallpaperColors(Color.valueOf(Color.BLACK), Color.valueOf(Color.BLACK), null) + colorsChangedListener.value.onColorsChanged( + RectF(100.0f, 100.0f, 200.0f, 200.0f), + wallpaperColors + ) + } + private fun setWhiteWallpaper() { + val wallpaperColors = + WallpaperColors( + Color.valueOf(Color.WHITE), + Color.valueOf(Color.WHITE), + null, + WallpaperColors.HINT_SUPPORTS_DARK_TEXT + ) + colorsChangedListener.value.onColorsChanged( + RectF(100.0f, 100.0f, 200.0f, 200.0f), + wallpaperColors + ) } @Test fun testDump() { - mRegionSampler.dump(pw) + regionSampler.dump(pw) + homescreenRegionSampler.dump(pw) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java index f7fcab1b769b..542e0cb728a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -1066,11 +1066,11 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { } @Test - public void onRefreshBatteryInfo_chargingWithOverheat_presentChargingLimited() { + public void onRefreshBatteryInfo_chargingWithLongLife_presentChargingLimited() { createController(); BatteryStatus status = new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING, 80 /* level */, BatteryManager.BATTERY_PLUGGED_AC, - BatteryManager.BATTERY_HEALTH_OVERHEAT, 0 /* maxChargingWattage */, + BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE, 0 /* maxChargingWattage */, true /* present */); mController.getKeyguardCallback().onRefreshBatteryInfo(status); @@ -1084,11 +1084,11 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { } @Test - public void onRefreshBatteryInfo_fullChargedWithOverheat_presentChargingLimited() { + public void onRefreshBatteryInfo_fullChargedWithLongLife_presentChargingLimited() { createController(); BatteryStatus status = new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING, 100 /* level */, BatteryManager.BATTERY_PLUGGED_AC, - BatteryManager.BATTERY_HEALTH_OVERHEAT, 0 /* maxChargingWattage */, + BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE, 0 /* maxChargingWattage */, true /* present */); mController.getKeyguardCallback().onRefreshBatteryInfo(status); @@ -1102,11 +1102,11 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { } @Test - public void onRefreshBatteryInfo_fullChargedWithoutOverheat_presentCharged() { + public void onRefreshBatteryInfo_fullChargedWithoutLongLife_presentCharged() { createController(); BatteryStatus status = new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING, 100 /* level */, BatteryManager.BATTERY_PLUGGED_AC, - BatteryManager.BATTERY_HEALTH_GOOD, 0 /* maxChargingWattage */, + BatteryManager.CHARGING_POLICY_DEFAULT, 0 /* maxChargingWattage */, true /* present */); mController.getKeyguardCallback().onRefreshBatteryInfo(status); @@ -1118,11 +1118,11 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { } @Test - public void onRefreshBatteryInfo_dozing_dischargingWithOverheat_presentBatteryPercentage() { + public void onRefreshBatteryInfo_dozing_dischargingWithLongLife_presentBatteryPercentage() { createController(); mController.setVisible(true); BatteryStatus status = new BatteryStatus(BatteryManager.BATTERY_STATUS_DISCHARGING, - 90 /* level */, 0 /* plugged */, BatteryManager.BATTERY_HEALTH_OVERHEAT, + 90 /* level */, 0 /* plugged */, BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE, 0 /* maxChargingWattage */, true /* present */); mController.getKeyguardCallback().onRefreshBatteryInfo(status); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt index cd0646543e69..839770267c74 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt @@ -27,3 +27,9 @@ class FakeStatusEvent( override val showAnimation: Boolean = true, override var contentDescription: String? = "", ) : StatusEvent + +class FakePrivacyStatusEvent( + override val viewCreator: ViewCreator, + override val showAnimation: Boolean = true, + override var contentDescription: String? = "", +) : PrivacyEvent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt index 08a9f3139d71..39ed5535ff3b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt @@ -380,6 +380,53 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { } @Test + fun testPrivacyEvent_forceVisibleIsUpdated_whenRescheduledDuringQueuedState() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule privacy event + createAndScheduleFakePrivacyEvent() + // request removal of persistent dot (sets forceVisible to false) + systemStatusAnimationScheduler.removePersistentDot() + // create and schedule a privacy event again (resets forceVisible to true) + createAndScheduleFakePrivacyEvent() + + // skip chip animation lifecycle and fast forward to SHOWING_PERSISTENT_DOT state + fastForwardAnimationToState(SHOWING_PERSISTENT_DOT) + + // verify that we reach SHOWING_PERSISTENT_DOT and that listener callback is invoked + assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any()) + } + + @Test + fun testPrivacyEvent_forceVisibleIsUpdated_whenRescheduledDuringAnimatingState() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule privacy event + createAndScheduleFakePrivacyEvent() + // request removal of persistent dot (sets forceVisible to false) + systemStatusAnimationScheduler.removePersistentDot() + fastForwardAnimationToState(RUNNING_CHIP_ANIM) + + // create and schedule a privacy event again (resets forceVisible to true) + createAndScheduleFakePrivacyEvent() + + // skip status chip display time + advanceTimeBy(DISPLAY_LENGTH + 1) + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemEventAnimationFinish(anyBoolean()) + + // skip disappear animation + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + + // verify that we reach SHOWING_PERSISTENT_DOT and that listener callback is invoked + assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any()) + } + + @Test fun testNewEvent_isScheduled_whenPostedDuringRemovalAnimation() = runTest { // Instantiate class under test with TestScope from runTest initializeSystemStatusAnimationScheduler(testScope = this) @@ -440,8 +487,7 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { private fun createAndScheduleFakePrivacyEvent(): OngoingPrivacyChip { val privacyChip = OngoingPrivacyChip(mContext) - val fakePrivacyStatusEvent = - FakeStatusEvent(viewCreator = { privacyChip }, priority = 100, forceVisible = true) + val fakePrivacyStatusEvent = FakePrivacyStatusEvent(viewCreator = { privacyChip }) systemStatusAnimationScheduler.onStatusEvent(fakePrivacyStatusEvent) return privacyChip } 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 f77160601f4d..a4ee349f5b71 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 @@ -188,13 +188,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { .thenReturn(mNotificationRoundnessManager); mStackScroller.setController(mStackScrollLayoutController); - // Stub out functionality that isn't necessary to test. - doNothing().when(mCentralSurfaces) - .executeRunnableDismissingKeyguard(any(Runnable.class), - any(Runnable.class), - anyBoolean(), - anyBoolean(), - anyBoolean()); doNothing().when(mGroupExpansionManager).collapseGroups(); doNothing().when(mExpandHelper).cancelImmediately(); doNothing().when(mNotificationShelf).setAnimationsEnabled(anyBoolean()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 7f20f1e53d97..e12d179c5aa5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -716,6 +716,94 @@ class StackScrollAlgorithmTest : SysuiTestCase() { .isLessThan(px(R.dimen.heads_up_pinned_elevation)) } + @Test + fun aodToLockScreen_hasPulsingNotification_pulsingNotificationRowDoesNotChange() { + // Given: Before AOD to LockScreen, there was a pulsing notification + val pulsingNotificationView = createPulsingViewMock() + val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() + algorithmState.visibleChildren.add(pulsingNotificationView) + ambientState.setPulsingRow(pulsingNotificationView) + + // When: during AOD to LockScreen, any dozeAmount between (0, 1.0) is equivalent as a middle + // stage; here we use 0.5 for testing. + // stackScrollAlgorithm.updatePulsingStates is called + ambientState.dozeAmount = 0.5f + stackScrollAlgorithm.updatePulsingStates(algorithmState, ambientState) + + // Then: ambientState.pulsingRow should still be pulsingNotificationView + assertTrue(ambientState.isPulsingRow(pulsingNotificationView)) + } + + @Test + fun deviceOnAod_hasPulsingNotification_recordPulsingNotificationRow() { + // Given: Device is on AOD, there is a pulsing notification + // ambientState.pulsingRow is null before stackScrollAlgorithm.updatePulsingStates + ambientState.dozeAmount = 1.0f + val pulsingNotificationView = createPulsingViewMock() + val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() + algorithmState.visibleChildren.add(pulsingNotificationView) + ambientState.setPulsingRow(null) + + // When: stackScrollAlgorithm.updatePulsingStates is called + stackScrollAlgorithm.updatePulsingStates(algorithmState, ambientState) + + // Then: ambientState.pulsingRow should record the pulsingNotificationView + assertTrue(ambientState.isPulsingRow(pulsingNotificationView)) + } + + @Test + fun deviceOnLockScreen_hasPulsingNotificationBefore_clearPulsingNotificationRowRecord() { + // Given: Device finished AOD to LockScreen, there was a pulsing notification, and + // ambientState.pulsingRow was not null before AOD to LockScreen + // pulsingNotificationView.showingPulsing() returns false since the device is on LockScreen + ambientState.dozeAmount = 0.0f + val pulsingNotificationView = createPulsingViewMock() + whenever(pulsingNotificationView.showingPulsing()).thenReturn(false) + val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() + algorithmState.visibleChildren.add(pulsingNotificationView) + ambientState.setPulsingRow(pulsingNotificationView) + + // When: stackScrollAlgorithm.updatePulsingStates is called + stackScrollAlgorithm.updatePulsingStates(algorithmState, ambientState) + + // Then: ambientState.pulsingRow should be null + assertTrue(ambientState.isPulsingRow(null)) + } + + @Test + fun aodToLockScreen_hasPulsingNotification_pulsingNotificationRowShowAtFullHeight() { + // Given: Before AOD to LockScreen, there was a pulsing notification + val pulsingNotificationView = createPulsingViewMock() + val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() + algorithmState.visibleChildren.add(pulsingNotificationView) + ambientState.setPulsingRow(pulsingNotificationView) + + // When: during AOD to LockScreen, any dozeAmount between (0, 1.0) is equivalent as a middle + // stage; here we use 0.5 for testing. The expansionFraction is also 0.5. + // stackScrollAlgorithm.resetViewStates is called. + ambientState.dozeAmount = 0.5f + setExpansionFractionWithoutShelfDuringAodToLockScreen( + ambientState, + algorithmState, + fraction = 0.5f + ) + stackScrollAlgorithm.resetViewStates(ambientState, 0) + + // Then: pulsingNotificationView should show at full height + assertEquals( + stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView), + pulsingNotificationView.viewState.height + ) + + // After: reset dozeAmount and expansionFraction + ambientState.dozeAmount = 0f + setExpansionFractionWithoutShelfDuringAodToLockScreen( + ambientState, + algorithmState, + fraction = 1f + ) + } + private fun createHunViewMock( isShadeOpen: Boolean, fullyVisible: Boolean, @@ -744,6 +832,29 @@ class StackScrollAlgorithmTest : SysuiTestCase() { headsUpIsVisible = fullyVisible } + private fun createPulsingViewMock( + ) = + mock<ExpandableNotificationRow>().apply { + whenever(this.viewState).thenReturn(ExpandableViewState()) + whenever(this.showingPulsing()).thenReturn(true) + } + + private fun setExpansionFractionWithoutShelfDuringAodToLockScreen( + ambientState: AmbientState, + algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState, + fraction: Float + ) { + // showingShelf: false + algorithmState.firstViewInShelf = null + // scrimPadding: 0, because device is on lock screen + ambientState.setStatusBarState(StatusBarState.KEYGUARD) + ambientState.dozeAmount = 0.0f + // set stackEndHeight and stackHeight + // ExpansionFractionWithoutShelf == stackHeight / stackEndHeight + ambientState.stackEndHeight = 100f + ambientState.stackHeight = ambientState.stackEndHeight * fraction + } + private fun resetViewStates_expansionChanging_notificationAlphaUpdated( expansionFraction: Float, expectedAlpha: Float, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index c83769d84d2a..fd9f6a73aee4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -22,8 +22,6 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; -import static com.google.common.truth.Truth.assertThat; - import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static junit.framework.TestCase.fail; @@ -54,7 +52,6 @@ import android.app.WallpaperManager; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; -import android.content.Intent; import android.content.IntentFilter; import android.hardware.devicestate.DeviceStateManager; import android.hardware.display.AmbientDisplayConfiguration; @@ -120,6 +117,7 @@ import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteracto import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.notetask.NoteTaskController; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import com.android.systemui.plugins.PluginDependencyProvider; import com.android.systemui.plugins.PluginManager; @@ -327,6 +325,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private FingerprintManager mFingerprintManager; @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; @Mock IPowerManager mPowerManagerService; + @Mock ActivityStarter mActivityStarter; private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); @@ -335,6 +334,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); private final InitController mInitController = new InitController(); private final DumpManager mDumpManager = new DumpManager(); + private final ScreenLifecycle mScreenLifecycle = new ScreenLifecycle(mDumpManager); @Before public void setup() throws Exception { @@ -487,7 +487,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mUserSwitcherController, mBatteryController, mColorExtractor, - new ScreenLifecycle(mDumpManager), + mScreenLifecycle, mWakefulnessLifecycle, mStatusBarStateController, Optional.of(mBubbles), @@ -547,13 +547,15 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mLightRevealScrim, mAlternateBouncerInteractor, mUserTracker, - () -> mFingerprintManager + () -> mFingerprintManager, + mActivityStarter ) { @Override protected ViewRootImpl getViewRootImpl() { return mViewRootImpl; } }; + mScreenLifecycle.addObserver(mCentralSurfaces.mScreenObserver); mCentralSurfaces.initShadeVisibilityListener(); when(mViewRootImpl.getOnBackInvokedDispatcher()) .thenReturn(mOnBackInvokedDispatcher); @@ -593,60 +595,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test - public void executeRunnableDismissingKeyguard_nullRunnable_showingAndOccluded() { - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isOccluded()).thenReturn(true); - - mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false); - } - - @Test - public void executeRunnableDismissingKeyguard_nullRunnable_showing() { - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isOccluded()).thenReturn(false); - - mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false); - } - - @Test - public void executeRunnableDismissingKeyguard_nullRunnable_notShowing() { - when(mKeyguardStateController.isShowing()).thenReturn(false); - when(mKeyguardStateController.isOccluded()).thenReturn(false); - - mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false); - } - - @Test - public void executeRunnableDismissingKeyguard_dreaming_notShowing() throws RemoteException { - when(mKeyguardStateController.isShowing()).thenReturn(false); - when(mKeyguardStateController.isOccluded()).thenReturn(false); - when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(true); - - mCentralSurfaces.executeRunnableDismissingKeyguard(() -> {}, - /* cancelAction= */ null, - /* dismissShade= */ false, - /* afterKeyguardGone= */ false, - /* deferred= */ false); - mUiBgExecutor.runAllReady(); - verify(mDreamManager, times(1)).awaken(); - } - - @Test - public void executeRunnableDismissingKeyguard_notDreaming_notShowing() throws RemoteException { - when(mKeyguardStateController.isShowing()).thenReturn(false); - when(mKeyguardStateController.isOccluded()).thenReturn(false); - when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(false); - - mCentralSurfaces.executeRunnableDismissingKeyguard(() -> {}, - /* cancelAction= */ null, - /* dismissShade= */ false, - /* afterKeyguardGone= */ false, - /* deferred= */ false); - mUiBgExecutor.runAllReady(); - verify(mDreamManager, never()).awaken(); - } - - @Test public void lockscreenStateMetrics_notShowing() { // uninteresting state, except that fingerprint must be non-zero when(mKeyguardStateController.isOccluded()).thenReturn(false); @@ -1253,20 +1201,29 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test - public void startActivityDismissingKeyguard_isShowingAndIsOccluded() { - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isOccluded()).thenReturn(true); - mCentralSurfaces.startActivityDismissingKeyguard( - new Intent(), - /* onlyProvisioned = */false, - /* dismissShade = */false); - ArgumentCaptor<OnDismissAction> onDismissActionCaptor = - ArgumentCaptor.forClass(OnDismissAction.class); - verify(mStatusBarKeyguardViewManager) - .dismissWithAction(onDismissActionCaptor.capture(), any(Runnable.class), eq(true), - eq(null)); - assertThat(onDismissActionCaptor.getValue().onDismiss()).isFalse(); - verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any(Runnable.class)); + public void deviceStateChange_unfolded_shadeExpanding_onKeyguard_closesQS() { + setFoldedStates(FOLD_STATE_FOLDED); + setGoToSleepStates(FOLD_STATE_FOLDED); + mCentralSurfaces.setBarStateForTest(KEYGUARD); + when(mNotificationPanelViewController.isExpandingOrCollapsing()).thenReturn(true); + + setDeviceState(FOLD_STATE_UNFOLDED); + mScreenLifecycle.dispatchScreenTurnedOff(); + + verify(mQuickSettingsController).closeQs(); + } + + @Test + public void deviceStateChange_unfolded_shadeExpanded_onKeyguard_closesQS() { + setFoldedStates(FOLD_STATE_FOLDED); + setGoToSleepStates(FOLD_STATE_FOLDED); + mCentralSurfaces.setBarStateForTest(KEYGUARD); + when(mNotificationPanelViewController.isShadeFullyExpanded()).thenReturn(true); + + setDeviceState(FOLD_STATE_UNFOLDED); + mScreenLifecycle.dispatchScreenTurnedOff(); + + verify(mQuickSettingsController).closeQs(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainerTest.kt new file mode 100644 index 000000000000..ec074d76da10 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallBackgroundContainerTest.kt @@ -0,0 +1,87 @@ +/* + * 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.statusbar.phone.ongoingcall + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class OngoingCallBackgroundContainerTest : SysuiTestCase() { + + private lateinit var underTest: OngoingCallBackgroundContainer + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + TestableLooper.get(this).runWithLooper { + val chipView = LayoutInflater.from(context).inflate(R.layout.ongoing_call_chip, null) + underTest = chipView.requireViewById(R.id.ongoing_call_chip_background) + } + } + + @Test + fun onMeasure_maxHeightFetcherNotSet_usesDesired() { + underTest.maxHeightFetcher = null + + underTest.measure( + WIDTH_SPEC, + View.MeasureSpec.makeMeasureSpec(123, View.MeasureSpec.EXACTLY), + ) + + assertThat(underTest.measuredHeight).isEqualTo(123) + } + + @Test + fun onMeasure_maxLargerThanDesired_usesDesired() { + underTest.maxHeightFetcher = { 234 } + + underTest.measure( + WIDTH_SPEC, + View.MeasureSpec.makeMeasureSpec(123, View.MeasureSpec.EXACTLY), + ) + + assertThat(underTest.measuredHeight).isEqualTo(123) + } + + @Test + fun onMeasure_desiredLargerThanMax_usesMaxMinusOne() { + underTest.maxHeightFetcher = { 234 } + + underTest.measure( + WIDTH_SPEC, + View.MeasureSpec.makeMeasureSpec(567, View.MeasureSpec.EXACTLY), + ) + + // We use the max - 1 to give a bit extra space + assertThat(underTest.measuredHeight).isEqualTo(233) + } + + private companion object { + val WIDTH_SPEC = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java index 91c88cebff79..c886f9bee07e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java @@ -226,31 +226,33 @@ public class BatteryControllerTest extends SysuiTestCase { } @Test - public void batteryStateChanged_healthNotOverheated_outputsFalse() { + public void batteryStateChanged_chargingStatusNotLongLife_outputsFalse() { Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); - intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_GOOD); + intent.putExtra(BatteryManager.EXTRA_CHARGING_STATUS, + BatteryManager.CHARGING_POLICY_DEFAULT); mBatteryController.onReceive(getContext(), intent); - Assert.assertFalse(mBatteryController.isOverheated()); + Assert.assertFalse(mBatteryController.isBatteryDefender()); } @Test - public void batteryStateChanged_healthOverheated_outputsTrue() { + public void batteryStateChanged_chargingStatusLongLife_outputsTrue() { Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); - intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT); + intent.putExtra(BatteryManager.EXTRA_CHARGING_STATUS, + BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE); mBatteryController.onReceive(getContext(), intent); - Assert.assertTrue(mBatteryController.isOverheated()); + Assert.assertTrue(mBatteryController.isBatteryDefender()); } @Test - public void batteryStateChanged_noHealthGiven_outputsFalse() { + public void batteryStateChanged_noChargingStatusGiven_outputsFalse() { Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); mBatteryController.onReceive(getContext(), intent); - Assert.assertFalse(mBatteryController.isOverheated()); + Assert.assertFalse(mBatteryController.isBatteryDefender()); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java index 7402b4d64b16..243f881f9357 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java @@ -284,16 +284,8 @@ public class BluetoothControllerImplTest extends SysuiTestCase { assertFalse(mBluetoothControllerImpl.isBluetoothAudioActive()); assertFalse(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()); - CachedBluetoothDevice device = mock(CachedBluetoothDevice.class); - mDevices.add(device); - when(device.isActiveDevice(BluetoothProfile.HEADSET)).thenReturn(true); - - List<LocalBluetoothProfile> profiles = new ArrayList<>(); - LocalBluetoothProfile profile = mock(LocalBluetoothProfile.class); - profiles.add(profile); - when(profile.getProfileId()).thenReturn(BluetoothProfile.HEADSET); - when(device.getProfiles()).thenReturn(profiles); - when(device.isConnectedProfile(profile)).thenReturn(true); + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.HEADSET, /* isConnected= */ true, /* isActive= */ true); mBluetoothControllerImpl.onAclConnectionStateChanged(device, BluetoothProfile.STATE_CONNECTED); @@ -304,6 +296,149 @@ public class BluetoothControllerImplTest extends SysuiTestCase { } @Test + public void isBluetoothAudioActive_headsetIsActive_true() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.HEADSET, /* isConnected= */ true, /* isActive= */ true); + + mBluetoothControllerImpl.onActiveDeviceChanged(device, BluetoothProfile.HEADSET); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioActive()).isTrue(); + } + + @Test + public void isBluetoothAudioActive_a2dpIsActive_true() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.A2DP, /* isConnected= */ true, /* isActive= */ true); + + mBluetoothControllerImpl.onActiveDeviceChanged(device, BluetoothProfile.A2DP); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioActive()).isTrue(); + } + + @Test + public void isBluetoothAudioActive_hearingAidIsActive_true() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.HEARING_AID, /* isConnected= */ true, /* isActive= */ true); + + mBluetoothControllerImpl.onActiveDeviceChanged(device, BluetoothProfile.HEARING_AID); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioActive()).isTrue(); + } + + @Test + public void isBluetoothAudioActive_leAudioIsActive_true() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.LE_AUDIO, /* isConnected= */ true, /* isActive= */ true); + + mBluetoothControllerImpl.onActiveDeviceChanged(device, BluetoothProfile.LE_AUDIO); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioActive()).isTrue(); + } + + @Test + public void isBluetoothAudioActive_otherProfile_false() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.PAN, /* isConnected= */ true, /* isActive= */ true); + + mBluetoothControllerImpl.onActiveDeviceChanged(device, BluetoothProfile.PAN); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioActive()).isFalse(); + } + + @Test + public void isBluetoothAudioActive_leAudio_butNotActive_false() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.LE_AUDIO, /* isConnected= */ true, /* isActive= */ false); + + mBluetoothControllerImpl.onActiveDeviceChanged(device, BluetoothProfile.LE_AUDIO); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioActive()).isFalse(); + } + + @Test + public void isBluetoothAudioProfileOnly_noneConnected_false() { + CachedBluetoothDevice device1 = createBluetoothDevice( + BluetoothProfile.LE_AUDIO, /* isConnected= */ false, /* isActive= */ false); + CachedBluetoothDevice device2 = createBluetoothDevice( + BluetoothProfile.HEADSET, /* isConnected= */ false, /* isActive= */ false); + + mBluetoothControllerImpl.onDeviceAdded(device1); + mBluetoothControllerImpl.onDeviceAdded(device2); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()).isFalse(); + } + + /** Regression test for b/278982782. */ + @Test + public void isBluetoothAudioProfileOnly_onlyLeAudioConnected_true() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.LE_AUDIO, /* isConnected= */ true, /* isActive= */ false); + + mBluetoothControllerImpl.onDeviceAdded(device); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()).isTrue(); + } + + @Test + public void isBluetoothAudioProfileOnly_onlyHeadsetConnected_true() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.HEADSET, /* isConnected= */ true, /* isActive= */ false); + + mBluetoothControllerImpl.onDeviceAdded(device); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()).isTrue(); + } + + @Test + public void isBluetoothAudioProfileOnly_onlyA2dpConnected_true() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.A2DP, /* isConnected= */ true, /* isActive= */ false); + + mBluetoothControllerImpl.onDeviceAdded(device); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()).isTrue(); + } + + @Test + public void isBluetoothAudioProfileOnly_onlyHearingAidConnected_true() { + CachedBluetoothDevice device = createBluetoothDevice( + BluetoothProfile.HEARING_AID, /* isConnected= */ true, /* isActive= */ false); + + mBluetoothControllerImpl.onDeviceAdded(device); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()).isTrue(); + } + + @Test + public void isBluetoothAudioProfileOnly_multipleAudioOnlyProfilesConnected_true() { + CachedBluetoothDevice device1 = createBluetoothDevice( + BluetoothProfile.LE_AUDIO, /* isConnected= */ true, /* isActive= */ false); + CachedBluetoothDevice device2 = createBluetoothDevice( + BluetoothProfile.A2DP, /* isConnected= */ true, /* isActive= */ false); + CachedBluetoothDevice device3 = createBluetoothDevice( + BluetoothProfile.HEADSET, /* isConnected= */ true, /* isActive= */ false); + + mBluetoothControllerImpl.onDeviceAdded(device1); + mBluetoothControllerImpl.onDeviceAdded(device2); + mBluetoothControllerImpl.onDeviceAdded(device3); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()).isTrue(); + } + + @Test + public void isBluetoothAudioProfileOnly_leAudioAndOtherProfileConnected_false() { + CachedBluetoothDevice device1 = createBluetoothDevice( + BluetoothProfile.LE_AUDIO, /* isConnected= */ true, /* isActive= */ false); + CachedBluetoothDevice device2 = createBluetoothDevice( + BluetoothProfile.PAN, /* isConnected= */ true, /* isActive= */ false); + + mBluetoothControllerImpl.onDeviceAdded(device1); + mBluetoothControllerImpl.onDeviceAdded(device2); + + assertThat(mBluetoothControllerImpl.isBluetoothAudioProfileOnly()).isFalse(); + } + + @Test public void testAddOnMetadataChangedListener_registersListenerOnAdapter() { CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); BluetoothDevice device = mock(BluetoothDevice.class); @@ -336,4 +471,21 @@ public class BluetoothControllerImplTest extends SysuiTestCase { mBluetoothControllerImpl.onActiveDeviceChanged(null, BluetoothProfile.HEADSET); // No assert, just need no crash. } + + private CachedBluetoothDevice createBluetoothDevice( + int profile, boolean isConnected, boolean isActive) { + CachedBluetoothDevice device = mock(CachedBluetoothDevice.class); + mDevices.add(device); + when(device.isActiveDevice(profile)).thenReturn(isActive); + + List<LocalBluetoothProfile> localBluetoothProfiles = new ArrayList<>(); + LocalBluetoothProfile localBluetoothProfile = mock(LocalBluetoothProfile.class); + localBluetoothProfiles.add(localBluetoothProfile); + when(device.getProfiles()).thenReturn(localBluetoothProfiles); + + when(localBluetoothProfile.getProfileId()).thenReturn(profile); + when(device.isConnectedProfile(localBluetoothProfile)).thenReturn(isConnected); + + return device; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 6e24941ac937..d33271b9d88f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.temporarydisplay.chipbar import android.os.PowerManager +import android.os.VibrationAttributes import android.os.VibrationEffect import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -461,7 +462,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { } @Test - fun displayView_vibrationEffect_doubleClickEffect() { + fun displayView_vibrationEffect_doubleClickEffectWithHardwareFeedback() { underTest.displayView( createChipbarInfo( Icon.Resource(R.id.check_box, null), @@ -471,7 +472,14 @@ class ChipbarCoordinatorTest : SysuiTestCase() { ) ) - verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)) + verify(vibratorHelper) + .vibrate( + any(), + any(), + eq(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)), + any(), + eq(VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK)), + ) } /** Regression test for b/266119467. */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt index 2b86cfdec04e..6db35ae94a3f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt @@ -6,6 +6,7 @@ import android.testing.TestableLooper import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever @@ -27,9 +28,7 @@ class CreateUserActivityTest : SysuiTestCase() { createDialog( /* activity = */ nullable(), /* activityStarter = */ nullable(), - /* oldUserIcon = */ nullable(), - /* defaultUserName = */ nullable(), - /* title = */ nullable(), + /* isMultipleAdminsEnabled = */ any(), /* successCallback = */ nullable(), /* cancelCallback = */ nullable() ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index d252d5317d06..ca83d49b19ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -482,30 +482,17 @@ class UserInteractorTest : SysuiTestCase() { } @Test - fun executeAction_addUser_dialogShown() = + fun executeAction_addUser_dismissesDialogAndStartsActivity() = testScope.runTest { val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) keyguardRepository.setKeyguardShowing(false) - val dialogRequest = collectLastValue(underTest.dialogShowRequests) - val dialogShower: UserSwitchDialogController.DialogShower = mock() - underTest.executeAction(UserActionModel.ADD_USER, dialogShower) + underTest.executeAction(UserActionModel.ADD_USER) verify(uiEventLogger, times(1)) .log(MultiUserActionsEvent.CREATE_USER_FROM_USER_SWITCHER) - assertThat(dialogRequest()) - .isEqualTo( - ShowDialogRequestModel.ShowAddUserDialog( - userHandle = userInfos[0].userHandle, - isKeyguardShowing = false, - showEphemeralMessage = false, - dialogShower = dialogShower, - ) - ) - underTest.onDialogShown() - assertThat(dialogRequest()).isNull() } @Test @@ -862,7 +849,7 @@ class UserInteractorTest : SysuiTestCase() { // Dialog is shown. assertThat(dialogRequest()) - .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) + .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) underTest.onDialogShown() assertThat(dialogRequest()).isNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/TraceUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/TraceUtilsTest.kt new file mode 100644 index 000000000000..6aad0ad46c2f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/TraceUtilsTest.kt @@ -0,0 +1,100 @@ +/* + * 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 + +import android.os.Handler +import android.os.Looper +import android.os.Trace.TRACE_TAG_APP +import android.testing.AndroidTestingRunner +import android.util.Log +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.After +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class TraceUtilsTest : SysuiTestCase() { + + companion object { + private const val TAG = "TraceUtilsTest" + private const val TEST_FAIL_TIMEOUT = 5000L + + // A string that is 128 characters long + private const val SECTION_NAME_THATS_TOO_LONG = + "123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_" + + "123456789_123456789_123456789_123456789_12345678" + } + + @Before + fun setUp() { + // Enable tracing via atrace in order to see the expected IllegalArgumentException. Trace + // sections won't run if tracing is disabled. + uiDevice.executeShellCommand("atrace --async_start -a com.android.*") + } + + @After + fun tearDown() { + uiDevice.executeShellCommand("atrace --async_stop") + } + + @Test + fun testLongTraceSection_throws_whenUsingPublicAPI() { + // Expects: "java.lang.IllegalArgumentException: sectionName is too long" + assertThrows(IllegalArgumentException::class.java) { + android.os.Trace.beginSection(SECTION_NAME_THATS_TOO_LONG) + } + } + + @Test + fun testLongTraceSection_doesNotThrow_whenUsingPrivateAPI() { + android.os.Trace.traceBegin(TRACE_TAG_APP, SECTION_NAME_THATS_TOO_LONG) + } + + @Test + @Ignore("b/267482189 - Enable once androidx.tracing >= 1.2.0-beta04") + fun testLongTraceSection_doesNotThrow_whenUsingAndroidX() { + androidx.tracing.Trace.beginSection(SECTION_NAME_THATS_TOO_LONG) + } + + @Test + fun testLongTraceSection_doesNotThrow_whenUsingHelper() { + traceSection(SECTION_NAME_THATS_TOO_LONG) { + Log.v(TAG, "com.android.systemui.util.traceSection() block.") + } + } + + @Test + fun testLongTraceSection_doesNotThrow_whenUsedAsTraceNameSupplier() { + Handler(Looper.getMainLooper()) + .runWithScissors( + TraceUtils.namedRunnable(SECTION_NAME_THATS_TOO_LONG) { + Log.v(TAG, "TraceUtils.namedRunnable() block.") + }, + TEST_FAIL_TIMEOUT + ) + } + + @Test + fun testLongTraceSection_doesNotThrow_whenUsingTraceRunnable() { + TraceUtils.traceRunnable(SECTION_NAME_THATS_TOO_LONG) { + Log.v(TAG, "TraceUtils.traceRunnable() block.") + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt index 1bdee3667d04..e3e7933405a4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/udfps/FakeOverlapDetector.kt @@ -21,7 +21,11 @@ import android.graphics.Rect class FakeOverlapDetector : OverlapDetector { var shouldReturn: Map<Int, Boolean> = mapOf() - override fun isGoodOverlap(touchData: NormalizedTouchData, nativeSensorBounds: Rect): Boolean { + override fun isGoodOverlap( + touchData: NormalizedTouchData, + nativeSensorBounds: Rect, + nativeOverlayBounds: Rect + ): Boolean { return shouldReturn[touchData.pointerId] ?: error("Unexpected PointerId not declared in TestCase currentPointers") } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt index 65735f028c41..4aaf3478a31d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt @@ -17,10 +17,13 @@ package com.android.systemui.keyguard.data.repository +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.keyguard.shared.model.AuthenticationFlags import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map class FakeBiometricSettingsRepository : BiometricSettingsRepository { @@ -50,9 +53,12 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> get() = _isFaceAuthSupportedInCurrentPosture - private val _isCurrentUserInLockdown = MutableStateFlow(false) override val isCurrentUserInLockdown: Flow<Boolean> - get() = _isCurrentUserInLockdown + get() = _authFlags.map { it.isInUserLockdown } + + private val _authFlags = MutableStateFlow(AuthenticationFlags(0, 0)) + override val authenticationFlags: Flow<AuthenticationFlags> + get() = _authFlags fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) { _isFingerprintEnrolled.value = isFingerprintEnrolled @@ -66,6 +72,10 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { _isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy } + fun setAuthenticationFlags(value: AuthenticationFlags) { + _authFlags.value = value + } + fun setFaceEnrolled(isFaceEnrolled: Boolean) { _isFaceEnrolled.value = isFaceEnrolled } @@ -79,7 +89,22 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { } fun setIsUserInLockdown(value: Boolean) { - _isCurrentUserInLockdown.value = value + if (value) { + setAuthenticationFlags( + AuthenticationFlags( + _authFlags.value.userId, + _authFlags.value.flag or + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN + ) + ) + } else { + setAuthenticationFlags( + AuthenticationFlags( + _authFlags.value.userId, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED + ) + ) + } } fun setIsNonStrongBiometricAllowed(value: Boolean) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt new file mode 100644 index 000000000000..b03b4ba3687d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository +import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeBouncerMessageRepository : BouncerMessageRepository { + private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val primaryAuthMessage: StateFlow<BouncerMessageModel?> + get() = _primaryAuthMessage + + private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val faceAcquisitionMessage: StateFlow<BouncerMessageModel?> + get() = _faceAcquisitionMessage + private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val fingerprintAcquisitionMessage: StateFlow<BouncerMessageModel?> + get() = _fingerprintAcquisitionMessage + private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val customMessage: StateFlow<BouncerMessageModel?> + get() = _customMessage + private val _biometricAuthMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val biometricAuthMessage: StateFlow<BouncerMessageModel?> + get() = _biometricAuthMessage + private val _authFlagsMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val authFlagsMessage: StateFlow<BouncerMessageModel?> + get() = _authFlagsMessage + + private val _biometricLockedOutMessage = MutableStateFlow<BouncerMessageModel?>(null) + override val biometricLockedOutMessage: Flow<BouncerMessageModel?> + get() = _biometricLockedOutMessage + + override fun setPrimaryAuthMessage(value: BouncerMessageModel?) { + _primaryAuthMessage.value = value + } + + override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) { + _faceAcquisitionMessage.value = value + } + + override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) { + _fingerprintAcquisitionMessage.value = value + } + + override fun setCustomMessage(value: BouncerMessageModel?) { + _customMessage.value = value + } + + fun setBiometricAuthMessage(value: BouncerMessageModel?) { + _biometricAuthMessage.value = value + } + + fun setAuthFlagsMessage(value: BouncerMessageModel?) { + _authFlagsMessage.value = value + } + + fun setBiometricLockedOutMessage(value: BouncerMessageModel?) { + _biometricLockedOutMessage.value = value + } + + override fun clearMessage() { + _primaryAuthMessage.value = null + _faceAcquisitionMessage.value = null + _fingerprintAcquisitionMessage.value = null + _customMessage.value = null + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index fd8c4b81063d..b52a76839a99 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -147,6 +147,10 @@ class FakeKeyguardRepository : KeyguardRepository { _isAodAvailable.value = isAodAvailable } + fun setDreaming(isDreaming: Boolean) { + _isDreaming.value = isDreaming + } + fun setDreamingWithOverlay(isDreaming: Boolean) { _isDreamingWithOverlay.value = isDreaming } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt index 1340a47ab6de..817e1db4876a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt @@ -28,7 +28,7 @@ class FakeTrustRepository : TrustRepository { get() = _isCurrentUserTrusted private val _isCurrentUserActiveUnlockAvailable = MutableStateFlow(false) - override val isCurrentUserActiveUnlockAvailable: StateFlow<Boolean> = + override val isCurrentUserActiveUnlockRunning: StateFlow<Boolean> = _isCurrentUserActiveUnlockAvailable.asStateFlow() private val _isCurrentUserTrustManaged = MutableStateFlow(false) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt new file mode 100644 index 000000000000..be3d54acd739 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -0,0 +1,161 @@ +/* + * 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.scene + +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.AuthenticationRepository +import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor +import com.android.systemui.scene.data.model.SceneContainerConfig +import com.android.systemui.scene.data.repository.SceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope + +/** + * Utilities for creating scene container framework related repositories, interactors, and + * view-models for tests. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class SceneTestUtils( + test: SysuiTestCase, + private val testScope: TestScope? = null, +) { + + private val context = test.context + + fun fakeSceneContainerRepository( + containerConfigurations: Set<SceneContainerConfig> = + setOf( + fakeSceneContainerConfig(CONTAINER_1), + fakeSceneContainerConfig(CONTAINER_2), + ) + ): SceneContainerRepository { + return SceneContainerRepository(containerConfigurations) + } + + fun fakeSceneKeys(): List<SceneKey> { + return listOf( + SceneKey.QuickSettings, + SceneKey.Shade, + SceneKey.Lockscreen, + SceneKey.Bouncer, + SceneKey.Gone, + ) + } + + fun fakeSceneContainerConfig( + name: String, + sceneKeys: List<SceneKey> = fakeSceneKeys(), + ): SceneContainerConfig { + return SceneContainerConfig( + name = name, + sceneKeys = sceneKeys, + initialSceneKey = SceneKey.Lockscreen, + ) + } + + fun sceneInteractor(): SceneInteractor { + return SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + } + + fun authenticationRepository(): AuthenticationRepository { + return AuthenticationRepositoryImpl() + } + + fun authenticationInteractor( + repository: AuthenticationRepository, + ): AuthenticationInteractor { + return AuthenticationInteractor( + applicationScope = applicationScope(), + repository = repository, + ) + } + + private fun applicationScope(): CoroutineScope { + return checkNotNull(testScope) { + """ + TestScope not initialized, please create a TestScope and inject it into + SceneTestUtils. + """ + .trimIndent() + } + .backgroundScope + } + + fun bouncerInteractor( + authenticationInteractor: AuthenticationInteractor, + sceneInteractor: SceneInteractor, + ): BouncerInteractor { + return BouncerInteractor( + applicationScope = applicationScope(), + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_1, + ) + } + + fun bouncerViewModel( + bouncerInteractor: BouncerInteractor, + ): BouncerViewModel { + return BouncerViewModel( + applicationContext = context, + applicationScope = applicationScope(), + interactorFactory = + object : BouncerInteractor.Factory { + override fun create(containerName: String): BouncerInteractor { + return bouncerInteractor + } + }, + containerName = CONTAINER_1, + ) + } + + fun lockScreenSceneInteractor( + authenticationInteractor: AuthenticationInteractor, + sceneInteractor: SceneInteractor, + bouncerInteractor: BouncerInteractor, + ): LockscreenSceneInteractor { + return LockscreenSceneInteractor( + applicationScope = applicationScope(), + authenticationInteractor = authenticationInteractor, + bouncerInteractorFactory = + object : BouncerInteractor.Factory { + override fun create(containerName: String): BouncerInteractor { + return bouncerInteractor + } + }, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_1, + ) + } + + companion object { + const val CONTAINER_1 = "container1" + const val CONTAINER_2 = "container2" + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index a324b2ff88e8..e89e33f3c24b 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -81,7 +81,6 @@ import android.view.MagnificationSpec; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; -import android.view.WindowInfo; import android.view.accessibility.AccessibilityCache; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -103,7 +102,6 @@ import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityWindowManager.RemoteAccessibilityConnection; import com.android.server.accessibility.magnification.MagnificationProcessor; import com.android.server.inputmethod.InputMethodManagerInternal; -import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; @@ -2081,7 +2079,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ IAccessibilityInteractionConnectionCallback callback, int fetchFlags, long interrogatingTid) { RemoteAccessibilityConnection connection; - IBinder activityToken = null; + IBinder windowToken = null; synchronized (mLock) { connection = mA11yWindowManager.getConnectionLocked(userId, resolvedWindowId); if (connection == null) { @@ -2090,9 +2088,8 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ final boolean isA11yFocusAction = (action == ACTION_ACCESSIBILITY_FOCUS) || (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS); if (!isA11yFocusAction) { - final WindowInfo windowInfo = - mA11yWindowManager.findWindowInfoByIdLocked(resolvedWindowId); - if (windowInfo != null) activityToken = windowInfo.activityToken; + windowToken = mA11yWindowManager.getWindowTokenForUserAndWindowIdLocked( + userId, resolvedWindowId); } final AccessibilityWindowInfo a11yWindowInfo = mA11yWindowManager.findA11yWindowInfoByIdLocked(resolvedWindowId); @@ -2113,9 +2110,8 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ if (action == ACTION_CLICK || action == ACTION_LONG_CLICK) { mA11yWindowManager.notifyOutsideTouch(userId, resolvedWindowId); } - if (activityToken != null) { - LocalServices.getService(ActivityTaskManagerInternal.class) - .setFocusedActivity(activityToken); + if (windowToken != null) { + mWindowManagerService.requestWindowFocus(windowToken); } if (intConnTracingEnabled()) { logTraceIntConn("performAccessibilityAction", diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java b/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java index 93531ddea005..92fd419885f1 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java @@ -16,6 +16,8 @@ package com.android.server.accessibility.magnification; +import android.annotation.NonNull; +import android.content.Context; import android.provider.DeviceConfig; /** @@ -29,6 +31,13 @@ public class AlwaysOnMagnificationFeatureFlag extends MagnificationFeatureFlagBa private static final String FEATURE_NAME_ENABLE_ALWAYS_ON_MAGNIFICATION = "AlwaysOnMagnifier__enable_always_on_magnifier"; + private @NonNull Context mContext; + + AlwaysOnMagnificationFeatureFlag(@NonNull Context context) { + super(); + mContext = context; + } + @Override String getNamespace() { return NAMESPACE; @@ -41,6 +50,7 @@ public class AlwaysOnMagnificationFeatureFlag extends MagnificationFeatureFlagBa @Override boolean getDefaultValue() { - return false; + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_magnification_always_on_enabled); } } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index 9b1c20431b1a..87fbee71ab59 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -156,7 +156,7 @@ public class MagnificationController implements WindowMagnificationManager.Callb mSupportWindowMagnification = context.getPackageManager().hasSystemFeature( FEATURE_WINDOW_MAGNIFICATION); - mAlwaysOnMagnificationFeatureFlag = new AlwaysOnMagnificationFeatureFlag(); + mAlwaysOnMagnificationFeatureFlag = new AlwaysOnMagnificationFeatureFlag(context); mAlwaysOnMagnificationFeatureFlag.addOnChangedListener( mBackgroundExecutor, mAms::updateAlwaysOnMagnification); } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index af6b24e1c332..2d60716104c1 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -558,10 +558,14 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku try { final Intent onClickIntent; - if (provider.maskedBySuspendedPackage) { + if (provider.maskedByQuietProfile) { + showBadge = true; + onClickIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(appUserId); + } else if (provider.maskedBySuspendedPackage) { showBadge = mUserManager.hasBadge(appUserId); final String suspendingPackage = mPackageManagerInternal.getSuspendingPackage( appInfo.packageName, appUserId); + // TODO(b/281839596): don't rely on platform always meaning suspended by admin. if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) { onClickIntent = mDevicePolicyManagerInternal.createShowAdminSupportIntent( appUserId, true); @@ -575,9 +579,6 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku appInfo.packageName, suspendingPackage, dialogInfo, null, null, appUserId); } - } else if (provider.maskedByQuietProfile) { - showBadge = true; - onClickIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(appUserId); } else /* provider.maskedByLockedProfile */ { showBadge = true; onClickIntent = mKeyguardManager diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index af5b196fe93d..fc758cba617f 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -761,6 +761,12 @@ public final class AutofillManagerService } // Called by Shell command + String getFieldDetectionServiceName(@UserIdInt int userId) { + enforceCallingPermissionForManagement(); + return mFieldClassificationResolver.readServiceName(userId); + } + + // Called by Shell command boolean setTemporaryDetectionService(@UserIdInt int userId, @NonNull String serviceName, int durationMs) { Slog.i(mTag, "setTemporaryDetectionService(" + userId + ") to " + serviceName @@ -903,9 +909,9 @@ public final class AutofillManagerService } /** - * Whether the Autofill PCC Classification feature is enabled. + * Whether the Autofill PCC Classification feature flag is enabled. */ - public boolean isPccClassificationEnabled() { + public boolean isPccClassificationFlagEnabled() { synchronized (mFlagLock) { return mPccClassificationEnabled; } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index d5dcdaf3c7b0..63a607c8d0d4 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -1730,6 +1730,23 @@ final class AutofillManagerServiceImpl return mRemoteFieldClassificationService; } + + public boolean isPccClassificationEnabled() { + boolean result = isPccClassificationEnabledInternal(); + if (sVerbose) { + Slog.v(TAG, "pccEnabled: " + result); + } + return result; + } + + public boolean isPccClassificationEnabledInternal() { + boolean flagEnabled = mMaster.isPccClassificationFlagEnabled(); + if (!flagEnabled) return false; + synchronized (mLock) { + return getRemoteFieldClassificationServiceLocked() != null; + } + } + /** * Called when the {@link AutofillManagerService#mFieldClassificationResolver} * changed (among other places). diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java index 62a29705f62e..4aeb4a4f8409 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java @@ -26,6 +26,7 @@ import android.os.RemoteCallback; import android.os.ShellCommand; import android.os.UserHandle; import android.service.autofill.AutofillFieldClassificationService.Scores; +import android.text.TextUtils; import android.view.autofill.AutofillManager; import com.android.internal.os.IResultReceiver; @@ -154,6 +155,8 @@ public final class AutofillManagerServiceShellCommand extends ShellCommand { return getBindInstantService(pw); case "default-augmented-service-enabled": return getDefaultAugmentedServiceEnabled(pw); + case "field-detection-service-enabled": + return isFieldDetectionServiceEnabled(pw); case "saved-password-count": return getSavedPasswordCount(pw); default: @@ -343,6 +346,14 @@ public final class AutofillManagerServiceShellCommand extends ShellCommand { return 0; } + private int isFieldDetectionServiceEnabled(PrintWriter pw) { + final int userId = getNextIntArgRequired(); + String name = mService.getFieldDetectionServiceName(userId); + boolean enabled = !TextUtils.isEmpty(name); + pw.println(enabled); + return 0; + } + private int setTemporaryAugmentedService(PrintWriter pw) { final int userId = getNextIntArgRequired(); final String serviceName = getNextArg(); diff --git a/services/autofill/java/com/android/server/autofill/FillRequestEventLogger.java b/services/autofill/java/com/android/server/autofill/FillRequestEventLogger.java index 06a616c3f348..994802d928f1 100644 --- a/services/autofill/java/com/android/server/autofill/FillRequestEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/FillRequestEventLogger.java @@ -74,9 +74,6 @@ public final class FillRequestEventLogger { public static final int TRIGGER_REASON_SERVED_FROM_CACHED_RESPONSE = AUTOFILL_FILL_REQUEST_REPORTED__REQUEST_TRIGGER_REASON__TRIGGER_REASON_SERVED_FROM_CACHED_RESPONSE; - // Augmented autofill currently doesn't have an assigned request_id, use -2 as the magic number. - public static final int AUGMENTED_AUTOFILL_REQUEST_ID = -2; - private final int mSessionId; private Optional<FillRequestEventInternal> mEventInternal; diff --git a/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java b/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java index 8f2ab714c4af..fc5fb1a4545c 100644 --- a/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java @@ -16,17 +16,20 @@ package com.android.server.autofill; +import static android.service.autofill.Dataset.PICK_REASON_PCC_DETECTION_ONLY; +import static android.service.autofill.Dataset.PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER; + import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__DIALOG; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_FAILURE; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_RESULT_UNKNOWN; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_SUCCESS; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_TYPE__AUTHENTICATION_TYPE_UNKNOWN; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_TYPE__DATASET_AUTHENTICATION; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_TYPE__FULL_AUTHENTICATION; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__DIALOG; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_CANCELLED; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_FAILURE; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_SESSION_DESTROYED; @@ -218,7 +221,16 @@ public final class FillResponseEventLogger { public void maybeSetAvailableCount(int val) { mEventInternal.ifPresent(event -> { - event.mAvailableCount = val; + // Don't reset if it's already populated. + // This is just a technical limitation of not having complicated logic. + // Autofill Provider may return some datasets which are applicable to data types. + // In such a case, we set available count to the number of datasets provided. + // However, it's possible that those data types aren't detected by PCC, so in effect, there + // are 0 datasets. In the codebase, we treat it as null response, which may call this again + // to set 0. But we don't want to overwrite this value. + if (event.mAvailableCount == 0) { + event.mAvailableCount = val; + } }); } @@ -318,6 +330,50 @@ public final class FillResponseEventLogger { }); } + /** + * Set available_pcc_count. + */ + public void maybeSetAvailablePccCount(int val) { + mEventInternal.ifPresent(event -> { + event.mAvailablePccCount = val; + }); + } + + /** + * Set available_pcc_only_count. + */ + public void maybeSetAvailablePccOnlyCount(int val) { + mEventInternal.ifPresent(event -> { + event.mAvailablePccOnlyCount = val; + }); + } + + /** + * Set available_pcc_count. + */ + public void maybeSetAvailableDatasetsPccCount(@Nullable List<Dataset> datasetList) { + mEventInternal.ifPresent(event -> { + int pccOnlyCount = 0; + int pccCount = 0; + if (datasetList != null) { + for (int i = 0; i < datasetList.size(); i++) { + Dataset dataset = datasetList.get(i); + if (dataset != null) { + if (dataset.getEligibleReason() == PICK_REASON_PCC_DETECTION_ONLY) { + pccOnlyCount++; + pccCount++; + } else if (dataset.getEligibleReason() + == PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER) { + pccCount++; + } + } + } + } + event.mAvailablePccOnlyCount = pccOnlyCount; + event.mAvailablePccCount = pccCount; + }); + } + /** * Log an AUTOFILL_FILL_RESPONSE_REPORTED event. @@ -344,7 +400,9 @@ public final class FillResponseEventLogger { + " mLatencyAuthenticationUiDisplayMillis=" + event.mLatencyAuthenticationUiDisplayMillis + " mLatencyDatasetDisplayMillis=" + event.mLatencyDatasetDisplayMillis + " mResponseStatus=" + event.mResponseStatus - + " mLatencyResponseProcessingMillis=" + event.mLatencyResponseProcessingMillis); + + " mLatencyResponseProcessingMillis=" + event.mLatencyResponseProcessingMillis + + " mAvailablePccCount=" + event.mAvailablePccCount + + " mAvailablePccOnlyCount=" + event.mAvailablePccOnlyCount); } FrameworkStatsLog.write( AUTOFILL_FILL_RESPONSE_REPORTED, @@ -361,7 +419,9 @@ public final class FillResponseEventLogger { event.mLatencyAuthenticationUiDisplayMillis, event.mLatencyDatasetDisplayMillis, event.mResponseStatus, - event.mLatencyResponseProcessingMillis); + event.mLatencyResponseProcessingMillis, + event.mAvailablePccCount, + event.mAvailablePccOnlyCount); mEventInternal = Optional.empty(); } @@ -379,6 +439,8 @@ public final class FillResponseEventLogger { int mLatencyDatasetDisplayMillis = 0; int mResponseStatus = RESPONSE_STATUS_UNKNOWN; int mLatencyResponseProcessingMillis = 0; + int mAvailablePccCount; + int mAvailablePccOnlyCount; FillResponseEventInternal() { } diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index ca743cbb1867..b2f9a93b3038 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -16,6 +16,8 @@ package com.android.server.autofill; +import static android.service.autofill.Dataset.PICK_REASON_PCC_DETECTION_ONLY; +import static android.service.autofill.Dataset.PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_DIALOG; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_INLINE; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_MENU; @@ -46,6 +48,12 @@ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_VIEW_CHANGED; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_VIEW_FOCUSED_BEFORE_FILL_DIALOG_RESPONSE; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_VIEW_FOCUS_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_NO_PCC; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_PCC_DETECTION_ONLY; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_PROVIDER_DETECTION_ONLY; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_PROVIDER_DETECTION_PREFERRED_WITH_PCC; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_UNKNOWN; import static com.android.server.autofill.Helper.sVerbose; import android.annotation.IntDef; @@ -116,6 +124,22 @@ public final class PresentationStatsEventLogger { public @interface AuthenticationResult { } + /** + * Reasons why the picked dataset was present. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillPresentationEventReported.DatasetPickedReason}. + * This enum is similar to {@link android.service.autofill.Dataset.DatasetEligibleReason} + */ + @IntDef(prefix = {"PICK_REASON"}, value = { + PICK_REASON_UNKNOWN, + PICK_REASON_NO_PCC, + PICK_REASON_PROVIDER_DETECTION_ONLY, + PICK_REASON_PROVIDER_DETECTION_PREFERRED_WITH_PCC, + PICK_REASON_PCC_DETECTION_ONLY, + PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DatasetPickedReason {} + public static final int NOT_SHOWN_REASON_ANY_SHOWN = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN; public static final int NOT_SHOWN_REASON_VIEW_FOCUS_CHANGED = @@ -151,6 +175,18 @@ public final class PresentationStatsEventLogger { public static final int AUTHENTICATION_RESULT_FAILURE = AUTOFILL_PRESENTATION_EVENT_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_FAILURE; + public static final int PICK_REASON_UNKNOWN = + AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_UNKNOWN; + public static final int PICK_REASON_NO_PCC = + AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_NO_PCC; + public static final int PICK_REASON_PROVIDER_DETECTION_ONLY = + AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_PROVIDER_DETECTION_ONLY; + public static final int PICK_REASON_PROVIDER_DETECTION_PREFERRED_WITH_PCC = + AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_PROVIDER_DETECTION_PREFERRED_WITH_PCC; + public static final int PICK_REASON_PCC_DETECTION_ONLY = + AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_PCC_DETECTION_ONLY; + public static final int PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER = + AUTOFILL_PRESENTATION_EVENT_REPORTED__SELECTED_DATASET_PICKED_REASON__PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER; private final int mSessionId; private Optional<PresentationStatsEventInternal> mEventInternal; @@ -194,36 +230,61 @@ public final class PresentationStatsEventLogger { public void maybeSetAvailableCount(@Nullable List<Dataset> datasetList, AutofillId currentViewId) { mEventInternal.ifPresent(event -> { - int availableCount = getDatasetCountForAutofillId(datasetList, currentViewId); - event.mAvailableCount = availableCount; - event.mIsDatasetAvailable = availableCount > 0; + CountContainer container = getDatasetCountForAutofillId(datasetList, currentViewId); + event.mAvailableCount = container.mAvailableCount; + event.mAvailablePccCount = container.mAvailablePccCount; + event.mAvailablePccOnlyCount = container.mAvailablePccOnlyCount; + event.mIsDatasetAvailable = container.mAvailableCount > 0; }); } public void maybeSetCountShown(@Nullable List<Dataset> datasetList, AutofillId currentViewId) { mEventInternal.ifPresent(event -> { - int countShown = getDatasetCountForAutofillId(datasetList, currentViewId); - event.mCountShown = countShown; - if (countShown > 0) { + CountContainer container = getDatasetCountForAutofillId(datasetList, currentViewId); + event.mCountShown = container.mAvailableCount; + if (container.mAvailableCount > 0) { event.mNoPresentationReason = NOT_SHOWN_REASON_ANY_SHOWN; } }); } - private static int getDatasetCountForAutofillId(@Nullable List<Dataset> datasetList, + private static CountContainer getDatasetCountForAutofillId(@Nullable List<Dataset> datasetList, AutofillId currentViewId) { - int availableCount = 0; + + CountContainer container = new CountContainer(); if (datasetList != null) { for (int i = 0; i < datasetList.size(); i++) { Dataset data = datasetList.get(i); if (data != null && data.getFieldIds() != null && data.getFieldIds().contains(currentViewId)) { - availableCount += 1; + container.mAvailableCount += 1; + if (data.getEligibleReason() == PICK_REASON_PCC_DETECTION_ONLY) { + container.mAvailablePccOnlyCount++; + container.mAvailablePccCount++; + } else if (data.getEligibleReason() + == PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER) { + container.mAvailablePccCount++; + } } } } - return availableCount; + return container; + } + + private static class CountContainer{ + int mAvailableCount = 0; + int mAvailablePccCount = 0; + int mAvailablePccOnlyCount = 0; + + CountContainer() {} + + CountContainer(int availableCount, int availablePccCount, + int availablePccOnlyCount) { + mAvailableCount = availableCount; + mAvailablePccCount = availablePccCount; + mAvailablePccOnlyCount = availablePccOnlyCount; + } } public void maybeSetCountFilteredUserTyping(int countFilteredUserTyping) { @@ -375,6 +436,46 @@ public final class PresentationStatsEventLogger { }); } + /** + * Set available_pcc_count. + */ + public void maybeSetAvailablePccCount(int val) { + mEventInternal.ifPresent(event -> { + event.mAvailablePccCount = val; + }); + } + + /** + * Set available_pcc_only_count. + */ + public void maybeSetAvailablePccOnlyCount(int val) { + mEventInternal.ifPresent(event -> { + event.mAvailablePccOnlyCount = val; + }); + } + + /** + * Set selected_dataset_picked_reason. + */ + public void maybeSetSelectedDatasetPickReason(@Dataset.DatasetEligibleReason int val) { + mEventInternal.ifPresent(event -> { + event.mSelectedDatasetPickedReason = convertDatasetPickReason(val); + }); + } + + private int convertDatasetPickReason(@Dataset.DatasetEligibleReason int val) { + switch (val) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + return val; + } + return PICK_REASON_UNKNOWN; + } + public void logAndEndEvent() { if (!mEventInternal.isPresent()) { @@ -410,7 +511,10 @@ public final class PresentationStatsEventLogger { + " mAuthenticationResult=" + event.mAuthenticationResult + " mLatencyAuthenticationUiDisplayMillis=" + event.mLatencyAuthenticationUiDisplayMillis - + " mLatencyDatasetDisplayMillis=" + event.mLatencyDatasetDisplayMillis); + + " mLatencyDatasetDisplayMillis=" + event.mLatencyDatasetDisplayMillis + + " mAvailablePccCount=" + event.mAvailablePccCount + + " mAvailablePccOnlyCount=" + event.mAvailablePccOnlyCount + + " mSelectedDatasetPickedReason=" + event.mSelectedDatasetPickedReason); } // TODO(b/234185326): Distinguish empty responses from other no presentation reasons. @@ -443,7 +547,10 @@ public final class PresentationStatsEventLogger { event.mAuthenticationType, event.mAuthenticationResult, event.mLatencyAuthenticationUiDisplayMillis, - event.mLatencyDatasetDisplayMillis); + event.mLatencyDatasetDisplayMillis, + event.mAvailablePccCount, + event.mAvailablePccOnlyCount, + event.mSelectedDatasetPickedReason); mEventInternal = Optional.empty(); } @@ -472,6 +579,9 @@ public final class PresentationStatsEventLogger { int mAuthenticationResult = AUTHENTICATION_RESULT_UNKNOWN; int mLatencyAuthenticationUiDisplayMillis = -1; int mLatencyDatasetDisplayMillis = -1; + int mAvailablePccCount = -1; + int mAvailablePccOnlyCount = -1; + @DatasetPickedReason int mSelectedDatasetPickedReason = PICK_REASON_UNKNOWN; PresentationStatsEventInternal() {} } diff --git a/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java b/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java index b8bac61b346b..ea31074ff5d2 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; +import android.os.Build; import android.os.ICancellationSignal; import android.os.RemoteException; import android.os.SystemClock; @@ -155,7 +156,19 @@ final class RemoteFieldClassificationService public void onSuccess(FieldClassificationResponse response) { logLatency(startTime); if (sDebug) { - Log.d(TAG, "onSuccess Response: " + response); + if (Build.IS_DEBUGGABLE) { + Slog.d(TAG, "onSuccess Response: " + response); + } else { + String msg = ""; + if (response == null + || response.getClassifications() == null) { + msg = "null response"; + } else { + msg = "size: " + + response.getClassifications().size(); + } + Slog.d(TAG, "onSuccess " + msg); + } } fieldClassificationServiceCallbacks .onClassificationRequestSuccess(response); @@ -165,7 +178,7 @@ final class RemoteFieldClassificationService public void onFailure() { logLatency(startTime); if (sDebug) { - Log.d(TAG, "onFailure"); + Slog.d(TAG, "onFailure"); } fieldClassificationServiceCallbacks .onClassificationRequestFailure(0, null); diff --git a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java index e5435c2869fd..28e8e3031a14 100644 --- a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java @@ -252,6 +252,15 @@ public final class SaveEventLogger { } /** + * Set is_framework_created_save_info as long as mEventInternal presents. + */ + public void maybeSetIsFrameworkCreatedSaveInfo(boolean val) { + mEventInternal.ifPresent(event -> { + event.mIsFrameworkCreatedSaveInfo = val; + }); + } + + /** * Log an AUTOFILL_SAVE_EVENT_REPORTED event. */ public void logAndEndEvent() { @@ -277,7 +286,8 @@ public final class SaveEventLogger { + " mIsSaved=" + event.mIsSaved + " mLatencySaveUiDisplayMillis=" + event.mLatencySaveUiDisplayMillis + " mLatencySaveRequestMillis=" + event.mLatencySaveRequestMillis - + " mLatencySaveFinishMillis=" + event.mLatencySaveFinishMillis); + + " mLatencySaveFinishMillis=" + event.mLatencySaveFinishMillis + + " mIsFrameworkCreatedSaveInfo=" + event.mIsFrameworkCreatedSaveInfo); } FrameworkStatsLog.write( AUTOFILL_SAVE_EVENT_REPORTED, @@ -295,7 +305,8 @@ public final class SaveEventLogger { event.mIsSaved, event.mLatencySaveUiDisplayMillis, event.mLatencySaveRequestMillis, - event.mLatencySaveFinishMillis); + event.mLatencySaveFinishMillis, + event.mIsFrameworkCreatedSaveInfo); mEventInternal = Optional.empty(); } @@ -314,6 +325,7 @@ public final class SaveEventLogger { long mLatencySaveUiDisplayMillis = 0; long mLatencySaveRequestMillis = 0; long mLatencySaveFinishMillis = 0; + boolean mIsFrameworkCreatedSaveInfo = false; SaveEventInternal() { } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index c4c1750e0a63..2d03daa4a579 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -19,6 +19,12 @@ package com.android.server.autofill; import static android.Manifest.permission.PROVIDE_OWN_AUTOFILL_SUGGESTIONS; import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES; import static android.service.autofill.AutofillService.EXTRA_FILL_RESPONSE; +import static android.service.autofill.Dataset.PICK_REASON_NO_PCC; +import static android.service.autofill.Dataset.PICK_REASON_PCC_DETECTION_ONLY; +import static android.service.autofill.Dataset.PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER; +import static android.service.autofill.Dataset.PICK_REASON_PROVIDER_DETECTION_ONLY; +import static android.service.autofill.Dataset.PICK_REASON_PROVIDER_DETECTION_PREFERRED_WITH_PCC; +import static android.service.autofill.Dataset.PICK_REASON_UNKNOWN; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_DIALOG; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_INLINE; import static android.service.autofill.FillEventHistory.Event.UI_TYPE_MENU; @@ -36,6 +42,7 @@ import static android.view.autofill.AutofillManager.ACTION_START_SESSION; import static android.view.autofill.AutofillManager.ACTION_VALUE_CHANGED; import static android.view.autofill.AutofillManager.ACTION_VIEW_ENTERED; import static android.view.autofill.AutofillManager.ACTION_VIEW_EXITED; +import static android.view.autofill.AutofillManager.COMMIT_REASON_SESSION_DESTROYED; import static android.view.autofill.AutofillManager.COMMIT_REASON_UNKNOWN; import static android.view.autofill.AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM; import static android.view.autofill.AutofillManager.getSmartSuggestionModeToString; @@ -79,7 +86,6 @@ import static com.android.server.autofill.SaveEventLogger.SAVE_UI_SHOWN_REASON_O import static com.android.server.autofill.SaveEventLogger.SAVE_UI_SHOWN_REASON_REQUIRED_ID_CHANGE; import static com.android.server.autofill.SaveEventLogger.SAVE_UI_SHOWN_REASON_TRIGGER_ID_SET; import static com.android.server.autofill.SaveEventLogger.SAVE_UI_SHOWN_REASON_UNKNOWN; -import static com.android.server.autofill.SessionCommittedEventLogger.COMMIT_REASON_SESSION_DESTROYED; import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_RECEIVER_EXTRAS; import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE; @@ -124,6 +130,7 @@ import android.service.autofill.AutofillFieldClassificationService.Scores; import android.service.autofill.AutofillService; import android.service.autofill.CompositeUserData; import android.service.autofill.Dataset; +import android.service.autofill.Dataset.DatasetEligibleReason; import android.service.autofill.FieldClassification; import android.service.autofill.FieldClassification.Match; import android.service.autofill.FieldClassificationUserData; @@ -798,7 +805,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState * Returns empty list if PCC is off or no types available */ private List<String> getTypeHintsForProvider() { - if (!mService.getMaster().isPccClassificationEnabled()) { + if (!mService.isPccClassificationEnabled()) { return Collections.EMPTY_LIST; } final String typeHints = mService.getMaster().getPccProviderHints(); @@ -1158,7 +1165,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } mSessionFlags.mAugmentedAutofillOnly = true; mFillRequestEventLogger.maybeSetRequestId(AUGMENTED_AUTOFILL_REQUEST_ID); - mFillRequestEventLogger.maybeSetIsAugmented(mSessionFlags.mAugmentedAutofillOnly); + mFillRequestEventLogger.maybeSetIsAugmented(true); mFillRequestEventLogger.logAndEndEvent(); triggerAugmentedAutofillLocked(flags); return; @@ -1200,7 +1207,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // structure is taken. This causes only one fill request per burst of focus changes. cancelCurrentRequestLocked(); - if (mService.getMaster().isPccClassificationEnabled() + if (mService.isPccClassificationEnabled() && mClassificationState.mHintsToAutofillIdMap == null) { if (sVerbose) { Slog.v(TAG, "triggering field classification"); @@ -1554,9 +1561,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Slog.d(TAG, message.toString()); } } - - if (((response.getDatasets() == null || response.getDatasets().isEmpty()) - && response.getAuthentication() == null) + List<Dataset> datasetList = response.getDatasets(); + if (((datasetList == null || datasetList.isEmpty()) && response.getAuthentication() == null) || autofillDisabled) { // Response is "empty" from a UI point of view, need to notify client. notifyUnavailableToClient( @@ -1578,6 +1584,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + mFillResponseEventLogger.maybeSetAvailableCount( + datasetList == null ? 0 : datasetList.size()); + // TODO(b/266379948): Ideally wait for PCC request to finish for a while more // (say 100ms) before proceeding further on. @@ -1631,7 +1640,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Slog.d(TAG, "DBG: computeDatasetsForProviderAndUpdateContainer: " + autofillProviderContainer); } - if (!mService.getMaster().isPccClassificationEnabled()) { + if (!mService.isPccClassificationEnabled()) { if (sVerbose) { Slog.v(TAG, "PCC classification is disabled"); } @@ -1728,6 +1737,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } if (ids.isEmpty()) return saveInfo; AutofillId[] autofillIds = new AutofillId[ids.size()]; + mSaveEventLogger.maybeSetIsFrameworkCreatedSaveInfo(true); ids.toArray(autofillIds); return SaveInfo.copy(saveInfo, autofillIds); } @@ -1798,6 +1808,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState private void computeDatasetsForProviderAndUpdateContainer( FillResponse response, DatasetComputationContainer container) { + @DatasetEligibleReason int globalPickReason = PICK_REASON_UNKNOWN; + boolean isPccEnabled = mService.isPccClassificationEnabled(); + if (isPccEnabled) { + globalPickReason = PICK_REASON_PROVIDER_DETECTION_ONLY; + } else { + globalPickReason = PICK_REASON_NO_PCC; + } List<Dataset> datasets = response.getDatasets(); if (datasets == null) return; ArrayMap<AutofillId, Set<Dataset>> autofillIdToDatasetMap = new ArrayMap<>(); @@ -1805,6 +1822,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Set<AutofillId> eligibleAutofillIds = new ArraySet<>(); for (Dataset dataset : response.getDatasets()) { if (dataset.getFieldIds() == null || dataset.getFieldIds().isEmpty()) continue; + @DatasetEligibleReason int pickReason = globalPickReason; if (dataset.getAutofillDatatypes() != null && !dataset.getAutofillDatatypes().isEmpty()) { // This dataset has information relevant for detection too, so we should filter @@ -1827,6 +1845,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (newSize == 0) continue; if (conversionRequired) { + pickReason = PICK_REASON_PROVIDER_DETECTION_PREFERRED_WITH_PCC; ArrayList<AutofillId> fieldIds = new ArrayList<>(newSize); ArrayList<AutofillValue> fieldValues = new ArrayList<>(newSize); ArrayList<RemoteViews> fieldPresentations = new ArrayList<>(newSize); @@ -1870,6 +1889,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState dataset.getAuthentication()); } } + dataset.setEligibleReasonReason(pickReason); eligibleDatasets.add(dataset); for (AutofillId id : dataset.getFieldIds()) { eligibleAutofillIds.add(id); @@ -1905,6 +1925,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Set<AutofillId> eligibleAutofillIds = new ArraySet<>(); for (int i = 0; i < datasets.size(); i++) { + + @DatasetEligibleReason int pickReason = PICK_REASON_PCC_DETECTION_ONLY; Dataset dataset = datasets.get(i); if (dataset.getAutofillDatatypes() == null || dataset.getAutofillDatatypes().isEmpty()) continue; @@ -1919,7 +1941,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Set<AutofillId> datasetAutofillIds = new ArraySet<>(); for (int j = 0; j < dataset.getAutofillDatatypes().size(); j++) { - if (dataset.getAutofillDatatypes().get(j) == null) continue; + if (dataset.getAutofillDatatypes().get(j) == null) { + if (dataset.getFieldIds() != null && dataset.getFieldIds().get(j) != null) { + pickReason = PICK_REASON_PCC_DETECTION_PREFERRED_WITH_PROVIDER; + } + continue; + } String hint = dataset.getAutofillDatatypes().get(j); if (hintsToAutofillIdMap.containsKey(hint)) { @@ -1966,6 +1993,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState null, dataset.getId(), dataset.getAuthentication()); + dataset.setEligibleReasonReason(pickReason); eligibleDatasets.add(newDataset); Set<Dataset> newDatasets; for (AutofillId autofillId : datasetAutofillIds) { @@ -2229,7 +2257,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @Override public void save() { synchronized (mLock) { - mSaveEventLogger.maybeSetSaveButtonClicked(true); if (mDestroyed) { Slog.w(TAG, "Call to Session#save() rejected - session: " + id + " destroyed"); @@ -2248,7 +2275,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState public void cancelSave() { synchronized (mLock) { mSessionFlags.mShowingSaveUi = false; - mSaveEventLogger.maybeSetDialogDismissed(true); if (mDestroyed) { Slog.w(TAG, "Call to Session#cancelSave() rejected - session: " + id + " destroyed"); @@ -2707,6 +2733,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mHandler.sendMessage(obtainMessage(Session::handleLogContextCommitted, this, Event.NO_SAVE_UI_REASON_NONE, COMMIT_REASON_UNKNOWN)); + logAllEvents(COMMIT_REASON_UNKNOWN); } /** @@ -2720,6 +2747,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @AutofillCommitReason int commitReason) { mHandler.sendMessage(obtainMessage(Session::handleLogContextCommitted, this, saveDialogNotShowReason, commitReason)); + logAllEvents(commitReason); } private void handleLogContextCommitted(@NoSaveReason int saveDialogNotShowReason, @@ -2951,6 +2979,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState changedFieldIds, changedDatasetIds, manuallyFilledFieldIds, manuallyFilledDatasetIds, detectedFieldIds, detectedFieldClassifications, mComponentName, mCompatMode, saveDialogNotShowReason); + logAllEvents(commitReason); } /** @@ -3403,7 +3432,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState getUiForShowing().showSaveUi(serviceLabel, serviceIcon, mService.getServicePackageName(), saveInfo, this, mComponentName, this, mContext, mPendingSaveUi, isUpdate, mCompatMode, - response.getShowSaveDialogIcon()); + response.getShowSaveDialogIcon(), mSaveEventLogger); mSaveEventLogger.maybeSetLatencySaveUiDisplayMillis( SystemClock.elapsedRealtime()- saveUiDisplayStartTimestamp); if (client != null) { @@ -4015,8 +4044,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mPresentationStatsEventLogger.maybeSetRequestId(response.getRequestId()); mPresentationStatsEventLogger.maybeSetAvailableCount( response.getDatasets(), mCurrentViewId); - mFillResponseEventLogger.maybeSetAvailableCount( - response.getDatasets(), mCurrentViewId); } if (isSameViewEntered) { @@ -4388,7 +4415,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState getUiForShowing().showFillDialog(filledId, response, filterText, mService.getServicePackageName(), mComponentName, serviceIcon, this, - id, mCompatMode); + id, mCompatMode, mPresentationStatsEventLogger); return true; } @@ -4707,6 +4734,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState autofillableIds = null; } // Log the existing FillResponse event. + mFillResponseEventLogger.maybeSetAvailableCount(0); mFillResponseEventLogger.logAndEndEvent(); mService.resetLastResponse(); @@ -4818,6 +4846,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mFillRequestEventLogger.maybeSetAppPackageUid(uid); mFillRequestEventLogger.maybeSetFlags(mFlags); mFillRequestEventLogger.maybeSetRequestId(AUGMENTED_AUTOFILL_REQUEST_ID); + mFillRequestEventLogger.maybeSetIsAugmented(true); mFillRequestEventLogger.logAndEndEvent(); final ViewState viewState = mViewStates.get(mCurrentViewId); @@ -4934,10 +4963,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mResponses.put(requestId, newResponse); mClientState = newClientState != null ? newClientState : newResponse.getClientState(); - mPresentationStatsEventLogger.maybeSetAvailableCount( - newResponse.getDatasets(), mCurrentViewId); - mFillResponseEventLogger.maybeSetAvailableCount( - newResponse.getDatasets(), mCurrentViewId); + List<Dataset> datasetList = newResponse.getDatasets(); + + mPresentationStatsEventLogger.maybeSetAvailableCount(datasetList, mCurrentViewId); + mFillResponseEventLogger.maybeSetAvailableDatasetsPccCount(datasetList); setViewStatesLocked(newResponse, ViewState.STATE_FILLABLE, false); updateFillDialogTriggerIdsLocked(); @@ -5062,6 +5091,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (generateEvent) { mService.logDatasetSelected(dataset.getId(), id, mClientState, uiType); mPresentationStatsEventLogger.maybeSetSelectedDatasetId(datasetIndex); + mPresentationStatsEventLogger.maybeSetSelectedDatasetPickReason( + dataset.getEligibleReason()); } if (mCurrentViewId != null) { mInlineSessionController.hideInlineSuggestionsUiLocked(mCurrentViewId); @@ -5655,6 +5686,19 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + @GuardedBy("mLock") + private void logAllEvents(@AutofillCommitReason int val) { + mSessionCommittedEventLogger.maybeSetCommitReason(val); + mSessionCommittedEventLogger.maybeSetRequestCount(mRequestCount); + mSessionCommittedEventLogger.maybeSetSessionDurationMillis( + SystemClock.elapsedRealtime() - mStartTime); + mFillRequestEventLogger.logAndEndEvent(); + mFillResponseEventLogger.logAndEndEvent(); + mPresentationStatsEventLogger.logAndEndEvent(); + mSaveEventLogger.logAndEndEvent(); + mSessionCommittedEventLogger.logAndEndEvent(); + } + /** * Destroy this session and perform any clean up work. * @@ -5669,15 +5713,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") RemoteFillService destroyLocked() { // Log unlogged events. - mSessionCommittedEventLogger.maybeSetCommitReason(COMMIT_REASON_SESSION_DESTROYED); - mSessionCommittedEventLogger.maybeSetRequestCount(mRequestCount); - mSessionCommittedEventLogger.maybeSetSessionDurationMillis( - SystemClock.elapsedRealtime() - mStartTime); - mSessionCommittedEventLogger.logAndEndEvent(); - mPresentationStatsEventLogger.logAndEndEvent(); - mSaveEventLogger.logAndEndEvent(); - mFillResponseEventLogger.logAndEndEvent(); - mFillRequestEventLogger.logAndEndEvent(); + logAllEvents(COMMIT_REASON_SESSION_DESTROYED); if (mDestroyed) { return null; diff --git a/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java b/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java index 541ec80e58ae..cd37073a1404 100644 --- a/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java @@ -16,13 +16,8 @@ package com.android.server.autofill; +import static android.view.autofill.AutofillManager.COMMIT_REASON_UNKNOWN; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_ACTIVITY_FINISHED; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_SESSION_DESTROYED; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_UNKNOWN; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CHANGED; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CLICKED; -import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_COMMITTED; import static com.android.server.autofill.Helper.sVerbose; import android.annotation.IntDef; @@ -32,7 +27,7 @@ import android.content.pm.PackageManager; import android.provider.Settings; import android.text.TextUtils; import android.util.Slog; - +import android.view.autofill.AutofillManager.AutofillCommitReason; import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; @@ -45,35 +40,6 @@ import java.util.Optional; public final class SessionCommittedEventLogger { private static final String TAG = "SessionCommittedEventLogger"; - /** - * Reasons why presentation was not shown. These are wrappers around - * {@link com.android.os.AtomsProto.AutofillSessionCommitted.AutofillCommitReason}. - */ - @IntDef(prefix = {"COMMIT_REASON"}, value = { - COMMIT_REASON_UNKNOWN, - COMMIT_REASON_ACTIVITY_FINISHED, - COMMIT_REASON_VIEW_COMMITTED, - COMMIT_REASON_VIEW_CLICKED, - COMMIT_REASON_VIEW_CHANGED, - COMMIT_REASON_SESSION_DESTROYED - }) - @Retention(RetentionPolicy.SOURCE) - public @interface CommitReason { - } - - public static final int COMMIT_REASON_UNKNOWN = - AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_UNKNOWN; - public static final int COMMIT_REASON_ACTIVITY_FINISHED = - AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_ACTIVITY_FINISHED; - public static final int COMMIT_REASON_VIEW_COMMITTED = - AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_COMMITTED; - public static final int COMMIT_REASON_VIEW_CLICKED = - AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CLICKED; - public static final int COMMIT_REASON_VIEW_CHANGED = - AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CHANGED; - public static final int COMMIT_REASON_SESSION_DESTROYED = - AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_SESSION_DESTROYED; - private final int mSessionId; private Optional<SessionCommittedEventInternal> mEventInternal; @@ -110,9 +76,9 @@ public final class SessionCommittedEventLogger { /** * Set commit_reason as long as mEventInternal presents. */ - public void maybeSetCommitReason(@CommitReason int val) { + public void maybeSetCommitReason(@AutofillCommitReason int val) { mEventInternal.ifPresent(event -> { - event.mCommitReason = val; + event.mCommitReason = val; }); } diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java index b3cbe45ea4a7..f92d38dc0deb 100644 --- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java @@ -52,6 +52,8 @@ import com.android.server.LocalServices; import com.android.server.UiModeManagerInternal; import com.android.server.UiThread; import com.android.server.autofill.Helper; +import com.android.server.autofill.PresentationStatsEventLogger; +import com.android.server.autofill.SaveEventLogger; import com.android.server.utils.Slogf; import java.io.PrintWriter; @@ -326,7 +328,7 @@ public final class AutoFillUI { @NonNull ValueFinder valueFinder, @NonNull ComponentName componentName, @NonNull AutoFillUiCallback callback, @NonNull Context context, @NonNull PendingUi pendingSaveUi, boolean isUpdate, boolean compatMode, - boolean showServiceIcon) { + boolean showServiceIcon, @Nullable SaveEventLogger mSaveEventLogger) { if (sVerbose) { Slogf.v(TAG, "showSaveUi(update=%b) for %s and display %d: %s", isUpdate, componentName.toShortString(), context.getDisplayId(), info); @@ -355,6 +357,9 @@ public final class AutoFillUI { @Override public void onSave() { log.setType(MetricsEvent.TYPE_ACTION); + if (mSaveEventLogger != null) { + mSaveEventLogger.maybeSetSaveButtonClicked(true); + } hideSaveUiUiThread(callback); callback.save(); destroySaveUiUiThread(pendingSaveUi, true); @@ -363,6 +368,9 @@ public final class AutoFillUI { @Override public void onCancel(IntentSender listener) { log.setType(MetricsEvent.TYPE_DISMISS); + if (mSaveEventLogger != null) { + mSaveEventLogger.maybeSetCancelButtonClicked(true); + } hideSaveUiUiThread(callback); if (listener != null) { try { @@ -384,6 +392,9 @@ public final class AutoFillUI { callback.cancelSave(); } mMetricsLogger.write(log); + if (mSaveEventLogger != null) { + mSaveEventLogger.maybeSetDialogDismissed(true); + } } @Override @@ -400,7 +411,8 @@ public final class AutoFillUI { public void showFillDialog(@NonNull AutofillId focusedId, @NonNull FillResponse response, @Nullable String filterText, @Nullable String servicePackageName, @NonNull ComponentName componentName, @Nullable Drawable serviceIcon, - @NonNull AutoFillUiCallback callback, int sessionId, boolean compatMode) { + @NonNull AutoFillUiCallback callback, int sessionId, boolean compatMode, + @Nullable PresentationStatsEventLogger mPresentationStatsEventLogger) { if (sVerbose) { Slog.v(TAG, "showFillDialog for " + componentName.toShortString() + ": " + response); @@ -442,6 +454,10 @@ public final class AutoFillUI { @Override public void onDatasetPicked(Dataset dataset) { log(MetricsEvent.TYPE_ACTION); + if (mPresentationStatsEventLogger != null) { + mPresentationStatsEventLogger.maybeSetPositiveCtaButtonClicked( + true); + } hideFillDialogUiThread(callback); if (mCallback != null) { final int datasetIndex = response.getDatasets().indexOf(dataset); @@ -453,6 +469,9 @@ public final class AutoFillUI { @Override public void onDismissed() { log(MetricsEvent.TYPE_DISMISS); + if (mPresentationStatsEventLogger != null) { + mPresentationStatsEventLogger.maybeSetDialogDismissed(true); + } hideFillDialogUiThread(callback); callback.requestShowSoftInput(focusedId); callback.requestFallbackFromFillDialog(); @@ -461,6 +480,10 @@ public final class AutoFillUI { @Override public void onCanceled() { log(MetricsEvent.TYPE_CLOSE); + if (mPresentationStatsEventLogger != null) { + mPresentationStatsEventLogger.maybeSetNegativeCtaButtonClicked( + true); + } hideFillDialogUiThread(callback); callback.requestShowSoftInput(focusedId); callback.requestFallbackFromFillDialog(); diff --git a/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java b/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java index dec0e7666b1b..dbeb624bd202 100644 --- a/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/DialogFillUi.java @@ -29,6 +29,7 @@ import android.graphics.drawable.Drawable; import android.service.autofill.Dataset; import android.service.autofill.FillResponse; import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.PluralsMessageFormatter; import android.util.Slog; import android.view.ContextThemeWrapper; @@ -177,7 +178,14 @@ final class DialogFillUi { window.setGravity(Gravity.BOTTOM | Gravity.CENTER); window.setCloseOnTouchOutside(true); final WindowManager.LayoutParams params = window.getAttributes(); - params.width = WindowManager.LayoutParams.MATCH_PARENT; + + DisplayMetrics displayMetrics = new DisplayMetrics(); + window.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + final int screenWidth = displayMetrics.widthPixels; + final int maxWidth = + mContext.getResources().getDimensionPixelSize(R.dimen.autofill_dialog_max_width); + params.width = Math.min(screenWidth, maxWidth); + params.accessibilityTitle = mContext.getString(R.string.autofill_picker_accessibility_title); params.windowAnimations = R.style.AutofillSaveAnimation; diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 12042594fefb..f035d0764279 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -48,6 +48,7 @@ import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.util.ArraySet; +import android.util.DisplayMetrics; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -361,7 +362,14 @@ final class SaveUi { window.setGravity(Gravity.BOTTOM | Gravity.CENTER); window.setCloseOnTouchOutside(true); final WindowManager.LayoutParams params = window.getAttributes(); - params.width = WindowManager.LayoutParams.MATCH_PARENT; + + DisplayMetrics displayMetrics = new DisplayMetrics(); + window.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + final int screenWidth = displayMetrics.widthPixels; + final int maxWidth = + context.getResources().getDimensionPixelSize(R.dimen.autofill_dialog_max_width); + params.width = Math.min(screenWidth, maxWidth); + params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); params.windowAnimations = R.style.AutofillSaveAnimation; params.setTrustedOverlay(); @@ -563,25 +571,7 @@ final class SaveUi { private void setServiceIcon(Context context, View view, Drawable serviceIcon) { final ImageView iconView = view.findViewById(R.id.autofill_save_icon); final Resources res = context.getResources(); - - final int maxWidth = res.getDimensionPixelSize(R.dimen.autofill_save_icon_max_size); - final int maxHeight = maxWidth; - final int actualWidth = serviceIcon.getMinimumWidth(); - final int actualHeight = serviceIcon.getMinimumHeight(); - - if (actualWidth <= maxWidth && actualHeight <= maxHeight) { - if (sDebug) { - Slog.d(TAG, "Adding service icon " - + "(" + actualWidth + "x" + actualHeight + ") as it's less than maximum " - + "(" + maxWidth + "x" + maxHeight + ")."); - } - iconView.setImageDrawable(serviceIcon); - } else { - Slog.w(TAG, "Not adding service icon of size " - + "(" + actualWidth + "x" + actualHeight + ") because maximum is " - + "(" + maxWidth + "x" + maxHeight + ")."); - ((ViewGroup)iconView.getParent()).removeView(iconView); - } + iconView.setImageDrawable(serviceIcon); } private static boolean isValidLink(PendingIntent pendingIntent, Intent intent) { diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 77990af50979..8cbb5dc03b9e 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -900,9 +900,6 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { mAgent.doRestoreFinished(mEphemeralOpToken, backupManagerService.getBackupManagerBinder()); - // Ask the agent for logs after doRestoreFinished() to allow it to finalize its logs. - BackupManagerMonitorUtils.monitorAgentLoggingResults(mMonitor, mCurrentPackage, mAgent); - // If we get this far, the callback or timeout will schedule the // next restore state, so we're done } catch (Exception e) { @@ -1323,6 +1320,11 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { EventLog.writeEvent(EventLogTags.RESTORE_PACKAGE, mCurrentPackage.packageName, size); + // Ask the agent for logs after doRestoreFinished() has completed executing to allow + // it to finalize its logs. + BackupManagerMonitorUtils.monitorAgentLoggingResults(mMonitor, mCurrentPackage, + mAgent); + // Just go back to running the restore queue keyValueAgentCleanup(); diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceConfig.java b/services/companion/java/com/android/server/companion/CompanionDeviceConfig.java index 8570515f241d..a2b71e0c3836 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceConfig.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceConfig.java @@ -16,6 +16,7 @@ package com.android.server.companion; +import android.os.Binder; import android.provider.DeviceConfig; /** @@ -34,7 +35,12 @@ public class CompanionDeviceConfig { * Returns whether the given flag is currently enabled, with a default value of {@code false}. */ public static boolean isEnabled(String flag) { - return DeviceConfig.getBoolean(NAMESPACE_COMPANION, flag, /* defaultValue= */ false); + final long token = Binder.clearCallingIdentity(); + try { + return DeviceConfig.getBoolean(NAMESPACE_COMPANION, flag, /* defaultValue= */ false); + } finally { + Binder.restoreCallingIdentity(token); + } } /** diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 7975e49541a0..6b99494f6564 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -306,6 +306,7 @@ public class CompanionDeviceManagerService extends SystemService { } else if (phase == PHASE_BOOT_COMPLETED) { // Run the Inactive Association Removal job service daily. InactiveAssociationsRemovalService.schedule(getContext()); + mCrossDeviceSyncController.onBootCompleted(); } } @@ -1382,10 +1383,11 @@ public class CompanionDeviceManagerService extends SystemService { } @Override - public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback) { + public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback, + @CrossDeviceSyncControllerCallback.Type int type) { if (CompanionDeviceConfig.isEnabled( CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { - mCrossDeviceSyncController.registerCallMetadataSyncCallback(callback); + mCrossDeviceSyncController.registerCallMetadataSyncCallback(callback, type); } } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java index 3b108e63e13d..c5ef4e49e5ea 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java @@ -36,7 +36,8 @@ public interface CompanionDeviceManagerServiceInternal { * Registers a callback from an InCallService / ConnectionService to CDM to process sync * requests and perform call control actions. */ - void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback); + void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback, + @CrossDeviceSyncControllerCallback.Type int type); /** * Requests a sync from an InCallService / ConnectionService to CDM, for the given association diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index c511429a37d8..04fbab434d1f 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -26,6 +26,7 @@ import android.os.ShellCommand; import android.util.proto.ProtoOutputStream; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; +import com.android.server.companion.datatransfer.contextsync.BitmapUtils; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; import com.android.server.companion.presence.CompanionDevicePresenceMonitor; import com.android.server.companion.transport.CompanionTransportManager; @@ -168,13 +169,17 @@ class CompanionDeviceShellCommand extends ShellCommand { case "send-context-sync-call-facilitators-message": { associationId = getNextIntArgRequired(); int numberOfFacilitators = getNextIntArgRequired(); + String facilitatorName = getNextArgRequired(); + String facilitatorId = getNextArgRequired(); final ProtoOutputStream pos = new ProtoOutputStream(); pos.write(ContextSyncMessage.VERSION, 1); final long telecomToken = pos.start(ContextSyncMessage.TELECOM); for (int i = 0; i < numberOfFacilitators; i++) { final long facilitatorsToken = pos.start(Telecom.FACILITATORS); - pos.write(Telecom.CallFacilitator.NAME, "Call Facilitator App #" + i); - pos.write(Telecom.CallFacilitator.IDENTIFIER, "com.android.test" + i); + pos.write(Telecom.CallFacilitator.NAME, + numberOfFacilitators == 1 ? facilitatorName : facilitatorName + i); + pos.write(Telecom.CallFacilitator.IDENTIFIER, + numberOfFacilitators == 1 ? facilitatorId : facilitatorId + i); pos.end(facilitatorsToken); } pos.end(telecomToken); @@ -188,6 +193,15 @@ class CompanionDeviceShellCommand extends ShellCommand { associationId = getNextIntArgRequired(); String callId = getNextArgRequired(); String facilitatorId = getNextArgRequired(); + int status = getNextIntArgRequired(); + boolean acceptControl = getNextBooleanArgRequired(); + boolean rejectControl = getNextBooleanArgRequired(); + boolean silenceControl = getNextBooleanArgRequired(); + boolean muteControl = getNextBooleanArgRequired(); + boolean unmuteControl = getNextBooleanArgRequired(); + boolean endControl = getNextBooleanArgRequired(); + boolean holdControl = getNextBooleanArgRequired(); + boolean unholdControl = getNextBooleanArgRequired(); final ProtoOutputStream pos = new ProtoOutputStream(); pos.write(ContextSyncMessage.VERSION, 1); final long telecomToken = pos.start(ContextSyncMessage.TELECOM); @@ -195,15 +209,40 @@ class CompanionDeviceShellCommand extends ShellCommand { pos.write(Telecom.Call.ID, callId); final long originToken = pos.start(Telecom.Call.ORIGIN); pos.write(Telecom.Call.Origin.CALLER_ID, "Caller Name"); + pos.write(Telecom.Call.Origin.APP_ICON, BitmapUtils.renderDrawableToByteArray( + mService.getContext().getPackageManager().getApplicationIcon( + facilitatorId))); final long facilitatorToken = pos.start( Telecom.Request.CreateAction.FACILITATOR); pos.write(Telecom.CallFacilitator.NAME, "Test App Name"); pos.write(Telecom.CallFacilitator.IDENTIFIER, facilitatorId); pos.end(facilitatorToken); pos.end(originToken); - pos.write(Telecom.Call.STATUS, Telecom.Call.RINGING); - pos.write(Telecom.Call.CONTROLS, Telecom.ACCEPT); - pos.write(Telecom.Call.CONTROLS, Telecom.REJECT); + pos.write(Telecom.Call.STATUS, status); + if (acceptControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.ACCEPT); + } + if (rejectControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.REJECT); + } + if (silenceControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.SILENCE); + } + if (muteControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.MUTE); + } + if (unmuteControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.UNMUTE); + } + if (endControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.END); + } + if (holdControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.PUT_ON_HOLD); + } + if (unholdControl) { + pos.write(Telecom.Call.CONTROLS, Telecom.TAKE_OFF_HOLD); + } pos.end(callsToken); pos.end(telecomToken); mTransportManager.createEmulatedTransport(associationId) diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/BitmapUtils.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/BitmapUtils.java new file mode 100644 index 000000000000..829041af2198 --- /dev/null +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/BitmapUtils.java @@ -0,0 +1,65 @@ +/* + * 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.server.companion.datatransfer.contextsync; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import java.io.ByteArrayOutputStream; + +/** Provides bitmap utility operations for rendering drawables to byte arrays. */ +public class BitmapUtils { + + private static final int APP_ICON_BITMAP_DIMENSION = 256; + + /** Render a drawable to a bitmap, which is then reformatted as a byte array. */ + public static byte[] renderDrawableToByteArray(Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + // Can't recycle the drawable's bitmap, so handle separately + final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); + if (bitmap.getWidth() > APP_ICON_BITMAP_DIMENSION + || bitmap.getHeight() > APP_ICON_BITMAP_DIMENSION) { + // Downscale, as the original drawable bitmap is too large. + final Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, + APP_ICON_BITMAP_DIMENSION, APP_ICON_BITMAP_DIMENSION, /* filter= */ true); + final byte[] renderedBitmap = renderBitmapToByteArray(scaledBitmap); + scaledBitmap.recycle(); + return renderedBitmap; + } + return renderBitmapToByteArray(bitmap); + } + final Bitmap bitmap = Bitmap.createBitmap(APP_ICON_BITMAP_DIMENSION, + APP_ICON_BITMAP_DIMENSION, + Bitmap.Config.ARGB_8888); + try { + final Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); + drawable.draw(canvas); + return renderBitmapToByteArray(bitmap); + } finally { + bitmap.recycle(); + } + } + + private static byte[] renderBitmapToByteArray(Bitmap bitmap) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(bitmap.getByteCount()); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); + return baos.toByteArray(); + } +} diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java index 459bf989321a..7371824e9c9c 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java @@ -17,6 +17,7 @@ package com.android.server.companion.datatransfer.contextsync; import android.media.AudioManager; +import android.net.Uri; import android.os.Bundle; import android.telecom.Call; import android.telecom.Connection; @@ -62,20 +63,14 @@ public class CallMetadataSyncConnectionService extends ConnectionService { final CallMetadataSyncConnection existingConnection = mActiveConnections.get(new CallMetadataSyncConnectionIdentifier( associationId, call.getId())); - if (existingConnection == null) { - final Bundle extras = new Bundle(); - extras.putInt(CrossDeviceSyncController.EXTRA_ASSOCIATION_ID, - associationId); - extras.putParcelable(CrossDeviceSyncController.EXTRA_CALL, call); - mTelecomManager.addNewIncomingCall(call.getPhoneAccountHandle(), - extras); - } else { + if (existingConnection != null) { existingConnection.update(call); } } // Remove obsolete calls. mActiveConnections.values().removeIf(connection -> { - if (!callMetadataSyncData.hasCall(connection.getCallId())) { + if (associationId == connection.getAssociationId() + && !callMetadataSyncData.hasCall(connection.getCallId())) { connection.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); return true; } @@ -91,7 +86,8 @@ public class CallMetadataSyncConnectionService extends ConnectionService { mAudioManager = getSystemService(AudioManager.class); mTelecomManager = getSystemService(TelecomManager.class); mCdmsi = LocalServices.getService(CompanionDeviceManagerServiceInternal.class); - mCdmsi.registerCallMetadataSyncCallback(mCrossDeviceSyncControllerCallback); + mCdmsi.registerCallMetadataSyncCallback(mCrossDeviceSyncControllerCallback, + CrossDeviceSyncControllerCallback.TYPE_CONNECTION_SERVICE); } @Override @@ -101,6 +97,11 @@ public class CallMetadataSyncConnectionService extends ConnectionService { CrossDeviceSyncController.EXTRA_ASSOCIATION_ID); final CallMetadataSyncData.Call call = connectionRequest.getExtras().getParcelable( CrossDeviceSyncController.EXTRA_CALL, CallMetadataSyncData.Call.class); + // InCallServices outside of framework (like Dialer's) might try to read this, and crash + // when they can't. Remove it once we're done with it, as well as the other internal ones. + connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_CALL); + connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_CALL_FACILITATOR_ID); + connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_ASSOCIATION_ID); final CallMetadataSyncConnection connection = new CallMetadataSyncConnection( mTelecomManager, mAudioManager, @@ -113,15 +114,17 @@ public class CallMetadataSyncConnectionService extends ConnectionService { CrossDeviceSyncController.createCallControlMessage(callId, action)); } }); - connection.setConnectionProperties( - Connection.PROPERTY_IS_EXTERNAL_CALL | Connection.PROPERTY_SELF_MANAGED); + connection.setConnectionProperties(Connection.PROPERTY_IS_EXTERNAL_CALL); + connection.setInitializing(); return connection; } @Override public void onCreateIncomingConnectionFailed(PhoneAccountHandle phoneAccountHandle, ConnectionRequest connectionRequest) { - Slog.e(TAG, "onCreateIncomingConnectionFailed for: " + phoneAccountHandle.getId()); + final String id = + phoneAccountHandle != null ? phoneAccountHandle.getId() : "unknown PhoneAccount"; + Slog.e(TAG, "onCreateOutgoingConnectionFailed for: " + id); } @Override @@ -132,7 +135,6 @@ public class CallMetadataSyncConnectionService extends ConnectionService { final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call(); call.setId(UUID.randomUUID().toString()); call.setStatus(android.companion.Telecom.Call.UNKNOWN_STATUS); - call.setPhoneAccountHandle(phoneAccountHandle); final CallMetadataSyncData.CallFacilitator callFacilitator = new CallMetadataSyncData.CallFacilitator(phoneAccount.getLabel().toString(), phoneAccount.getExtras().getString( @@ -142,6 +144,10 @@ public class CallMetadataSyncConnectionService extends ConnectionService { final int associationId = connectionRequest.getExtras().getInt( CrossDeviceSyncController.EXTRA_ASSOCIATION_ID); + connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_CALL); + connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_CALL_FACILITATOR_ID); + connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_ASSOCIATION_ID); + final CallMetadataSyncConnection connection = new CallMetadataSyncConnection( mTelecomManager, mAudioManager, @@ -154,8 +160,7 @@ public class CallMetadataSyncConnectionService extends ConnectionService { CrossDeviceSyncController.createCallControlMessage(callId, action)); } }); - connection.setConnectionProperties( - Connection.PROPERTY_IS_EXTERNAL_CALL | Connection.PROPERTY_SELF_MANAGED); + connection.setConnectionProperties(Connection.PROPERTY_IS_EXTERNAL_CALL); mCdmsi.sendCrossDeviceSyncMessage(associationId, CrossDeviceSyncController.createCallCreateMessage(call.getId(), @@ -168,13 +173,21 @@ public class CallMetadataSyncConnectionService extends ConnectionService { @Override public void onCreateOutgoingConnectionFailed(PhoneAccountHandle phoneAccountHandle, ConnectionRequest connectionRequest) { - Slog.e(TAG, "onCreateIncomingConnectionFailed for: " + phoneAccountHandle.getId()); + final String id = + phoneAccountHandle != null ? phoneAccountHandle.getId() : "unknown PhoneAccount"; + Slog.e(TAG, "onCreateOutgoingConnectionFailed for: " + id); } @Override public void onCreateConnectionComplete(Connection connection) { if (connection instanceof CallMetadataSyncConnection) { - ((CallMetadataSyncConnection) connection).initialize(); + final CallMetadataSyncConnection callMetadataSyncConnection = + (CallMetadataSyncConnection) connection; + callMetadataSyncConnection.initialize(); + mActiveConnections.put(new CallMetadataSyncConnectionIdentifier( + callMetadataSyncConnection.getAssociationId(), + callMetadataSyncConnection.getCallId()), + callMetadataSyncConnection); } } @@ -242,7 +255,11 @@ public class CallMetadataSyncConnectionService extends ConnectionService { return mCall.getId(); } - public void initialize() { + public int getAssociationId() { + return mAssociationId; + } + + private void initialize() { final int status = mCall.getStatus(); if (status == android.companion.Telecom.Call.RINGING_SILENCED) { mTelecomManager.silenceRinger(); @@ -254,12 +271,21 @@ public class CallMetadataSyncConnectionService extends ConnectionService { setActive(); } else if (state == Call.STATE_HOLDING) { setOnHold(); + } else if (state == Call.STATE_DISCONNECTED) { + setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); } else { - Slog.e(TAG, "Could not initialize call to unknown state"); + setInitialized(); + } + + final String callerId = mCall.getCallerId(); + if (callerId != null) { + setCallerDisplayName(callerId, TelecomManager.PRESENTATION_ALLOWED); + setAddress(Uri.fromParts("custom", mCall.getCallerId(), null), + TelecomManager.PRESENTATION_ALLOWED); } final Bundle extras = new Bundle(); - extras.putString(CrossDeviceCall.EXTRA_CALL_ID, mCall.getId()); + extras.putString(CrossDeviceSyncController.EXTRA_CALL_ID, mCall.getId()); putExtras(extras); int capabilities = getConnectionCapabilities(); @@ -280,7 +306,7 @@ public class CallMetadataSyncConnectionService extends ConnectionService { } } - public void update(CallMetadataSyncData.Call call) { + private void update(CallMetadataSyncData.Call call) { final int status = call.getStatus(); if (status == android.companion.Telecom.Call.RINGING_SILENCED && mCall.getStatus() != android.companion.Telecom.Call.RINGING_SILENCED) { @@ -295,31 +321,29 @@ public class CallMetadataSyncConnectionService extends ConnectionService { setActive(); } else if (state == Call.STATE_HOLDING) { setOnHold(); + } else if (state == Call.STATE_DISCONNECTED) { + setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); } else { Slog.e(TAG, "Could not update call to unknown state"); } } int capabilities = getConnectionCapabilities(); + mCall.setControls(call.getControls()); final boolean hasHoldControl = mCall.hasControl( android.companion.Telecom.PUT_ON_HOLD) || mCall.hasControl(android.companion.Telecom.TAKE_OFF_HOLD); - if (hasHoldControl != ((getConnectionCapabilities() & CAPABILITY_HOLD) - == CAPABILITY_HOLD)) { - if (hasHoldControl) { - capabilities |= CAPABILITY_HOLD; - } else { - capabilities &= ~CAPABILITY_HOLD; - } + if (hasHoldControl) { + capabilities |= CAPABILITY_HOLD; + } else { + capabilities &= ~CAPABILITY_HOLD; } - final boolean hasMuteControl = mCall.hasControl(android.companion.Telecom.MUTE); - if (hasMuteControl != ((getConnectionCapabilities() & CAPABILITY_MUTE) - == CAPABILITY_MUTE)) { - if (hasMuteControl) { - capabilities |= CAPABILITY_MUTE; - } else { - capabilities &= ~CAPABILITY_MUTE; - } + final boolean hasMuteControl = mCall.hasControl(android.companion.Telecom.MUTE) + || mCall.hasControl(android.companion.Telecom.UNMUTE); + if (hasMuteControl) { + capabilities |= CAPABILITY_MUTE; + } else { + capabilities &= ~CAPABILITY_MUTE; } mAudioManager.setMicrophoneMute( mCall.hasControl(android.companion.Telecom.UNMUTE)); diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java index b3cf772fc470..d8621cb796ab 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java @@ -20,7 +20,6 @@ import android.annotation.NonNull; import android.companion.ContextSyncMessage; import android.os.Parcel; import android.os.Parcelable; -import android.telecom.PhoneAccountHandle; import java.util.ArrayList; import java.util.Collection; @@ -189,7 +188,6 @@ class CallMetadataSyncData { private String mCallerId; private byte[] mAppIcon; private CallFacilitator mFacilitator; - private PhoneAccountHandle mPhoneAccountHandle; private int mStatus; private final Set<Integer> mControls = new HashSet<>(); @@ -200,9 +198,6 @@ class CallMetadataSyncData { call.setAppIcon(parcel.readBlob()); call.setFacilitator(parcel.readParcelable(CallFacilitator.class.getClassLoader(), CallFacilitator.class)); - call.setPhoneAccountHandle( - parcel.readParcelable(PhoneAccountHandle.class.getClassLoader(), - android.telecom.PhoneAccountHandle.class)); call.setStatus(parcel.readInt()); final int numberOfControls = parcel.readInt(); for (int i = 0; i < numberOfControls; i++) { @@ -217,7 +212,6 @@ class CallMetadataSyncData { parcel.writeString(mCallerId); parcel.writeBlob(mAppIcon); parcel.writeParcelable(mFacilitator, parcelableFlags); - parcel.writeParcelable(mPhoneAccountHandle, parcelableFlags); parcel.writeInt(mStatus); parcel.writeInt(mControls.size()); for (int control : mControls) { @@ -241,10 +235,6 @@ class CallMetadataSyncData { mFacilitator = facilitator; } - void setPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle) { - mPhoneAccountHandle = phoneAccountHandle; - } - void setStatus(int status) { mStatus = status; } @@ -253,6 +243,11 @@ class CallMetadataSyncData { mControls.add(control); } + void setControls(Set<Integer> controls) { + mControls.clear(); + mControls.addAll(controls); + } + String getId() { return mId; } @@ -269,10 +264,6 @@ class CallMetadataSyncData { return mFacilitator; } - PhoneAccountHandle getPhoneAccountHandle() { - return mPhoneAccountHandle; - } - int getStatus() { return mStatus; } diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java index 1f5e168b14e4..b46d5d3eab54 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java @@ -79,16 +79,15 @@ public class CallMetadataSyncInCallService extends InCallService { int callControlAction) { final CrossDeviceCall crossDeviceCall = getCallForId(crossDeviceCallId, mCurrentCalls.values()); + if (crossDeviceCall == null) { + return; + } switch (callControlAction) { case android.companion.Telecom.ACCEPT: - if (crossDeviceCall != null) { - crossDeviceCall.doAccept(); - } + crossDeviceCall.doAccept(); break; case android.companion.Telecom.REJECT: - if (crossDeviceCall != null) { - crossDeviceCall.doReject(); - } + crossDeviceCall.doReject(); break; case android.companion.Telecom.SILENCE: doSilence(); @@ -100,19 +99,13 @@ public class CallMetadataSyncInCallService extends InCallService { doUnmute(); break; case android.companion.Telecom.END: - if (crossDeviceCall != null) { - crossDeviceCall.doEnd(); - } + crossDeviceCall.doEnd(); break; case android.companion.Telecom.PUT_ON_HOLD: - if (crossDeviceCall != null) { - crossDeviceCall.doPutOnHold(); - } + crossDeviceCall.doPutOnHold(); break; case android.companion.Telecom.TAKE_OFF_HOLD: - if (crossDeviceCall != null) { - crossDeviceCall.doTakeOffHold(); - } + crossDeviceCall.doTakeOffHold(); break; default: } @@ -148,7 +141,8 @@ public class CallMetadataSyncInCallService extends InCallService { super.onCreate(); if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { mCdmsi = LocalServices.getService(CompanionDeviceManagerServiceInternal.class); - mCdmsi.registerCallMetadataSyncCallback(mCrossDeviceSyncControllerCallback); + mCdmsi.registerCallMetadataSyncCallback(mCrossDeviceSyncControllerCallback, + CrossDeviceSyncControllerCallback.TYPE_IN_CALL_SERVICE); } } @@ -156,7 +150,7 @@ public class CallMetadataSyncInCallService extends InCallService { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.putAll(getCalls().stream().collect(Collectors.toMap(call -> call, - call -> new CrossDeviceCall(getPackageManager(), call, getCallAudioState())))); + call -> new CrossDeviceCall(this, call, getCallAudioState())))); mCurrentCalls.keySet().forEach(call -> call.registerCallback(mTelecomCallback, getMainThreadHandler())); sync(getUserId()); @@ -182,7 +176,7 @@ public class CallMetadataSyncInCallService extends InCallService { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.put(call, - new CrossDeviceCall(getPackageManager(), call, getCallAudioState())); + new CrossDeviceCall(this, call, getCallAudioState())); call.registerCallback(mTelecomCallback); sync(getUserId()); } diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java index 168068ec6497..fec6923e4d06 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java @@ -17,20 +17,20 @@ package com.android.server.companion.datatransfer.contextsync; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; +import android.net.Uri; import android.telecom.Call; import android.telecom.CallAudioState; +import android.telecom.TelecomManager; import android.telecom.VideoProfile; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; -import java.io.ByteArrayOutputStream; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -40,47 +40,57 @@ public class CrossDeviceCall { private static final String TAG = "CrossDeviceCall"; - public static final String EXTRA_CALL_ID = - "com.android.companion.datatransfer.contextsync.extra.CALL_ID"; - private static final int APP_ICON_BITMAP_DIMENSION = 256; - private final String mId; - private Call mCall; + private final Call mCall; @VisibleForTesting boolean mIsEnterprise; - @VisibleForTesting boolean mIsOtt; private final String mCallingAppPackageName; private String mCallingAppName; private byte[] mCallingAppIcon; private String mCallerDisplayName; + private int mCallerDisplayNamePresentation; private int mStatus = android.companion.Telecom.Call.UNKNOWN_STATUS; private String mContactDisplayName; + private Uri mHandle; + private int mHandlePresentation; private boolean mIsMuted; private final Set<Integer> mControls = new HashSet<>(); + private final boolean mIsCallPlacedByContextSync; - public CrossDeviceCall(PackageManager packageManager, @NonNull Call call, + public CrossDeviceCall(Context context, @NonNull Call call, CallAudioState callAudioState) { - this(packageManager, call.getDetails(), callAudioState); - mCall = call; - call.putExtra(EXTRA_CALL_ID, mId); + this(context, call, call.getDetails(), callAudioState); } - CrossDeviceCall(PackageManager packageManager, Call.Details callDetails, + CrossDeviceCall(Context context, Call.Details callDetails, CallAudioState callAudioState) { + this(context, /* call= */ null, callDetails, callAudioState); + } + + private CrossDeviceCall(Context context, @Nullable Call call, + Call.Details callDetails, CallAudioState callAudioState) { + mCall = call; final String predefinedId = callDetails.getIntentExtras() != null - ? callDetails.getIntentExtras().getString(EXTRA_CALL_ID) : null; - mId = predefinedId != null ? predefinedId : UUID.randomUUID().toString(); + ? callDetails.getIntentExtras().getString(CrossDeviceSyncController.EXTRA_CALL_ID) + : null; + final String generatedId = UUID.randomUUID().toString(); + mId = predefinedId != null ? (generatedId + predefinedId) : generatedId; + if (call != null) { + call.putExtra(CrossDeviceSyncController.EXTRA_CALL_ID, mId); + } + mIsCallPlacedByContextSync = + new ComponentName(context, CallMetadataSyncConnectionService.class) + .equals(callDetails.getAccountHandle().getComponentName()); mCallingAppPackageName = callDetails.getAccountHandle().getComponentName().getPackageName(); - mIsOtt = (callDetails.getCallCapabilities() & Call.Details.PROPERTY_SELF_MANAGED) - == Call.Details.PROPERTY_SELF_MANAGED; mIsEnterprise = (callDetails.getCallProperties() & Call.Details.PROPERTY_ENTERPRISE_CALL) == Call.Details.PROPERTY_ENTERPRISE_CALL; + final PackageManager packageManager = context.getPackageManager(); try { final ApplicationInfo applicationInfo = packageManager .getApplicationInfo(mCallingAppPackageName, PackageManager.ApplicationInfoFlags.of(0)); mCallingAppName = packageManager.getApplicationLabel(applicationInfo).toString(); - mCallingAppIcon = renderDrawableToByteArray( + mCallingAppIcon = BitmapUtils.renderDrawableToByteArray( packageManager.getApplicationIcon(applicationInfo)); } catch (PackageManager.NameNotFoundException e) { Slog.e(TAG, "Could not get application info for package " + mCallingAppPackageName, e); @@ -89,40 +99,6 @@ public class CrossDeviceCall { updateCallDetails(callDetails); } - private byte[] renderDrawableToByteArray(Drawable drawable) { - if (drawable instanceof BitmapDrawable) { - // Can't recycle the drawable's bitmap, so handle separately - final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); - if (bitmap.getWidth() > APP_ICON_BITMAP_DIMENSION - || bitmap.getHeight() > APP_ICON_BITMAP_DIMENSION) { - // Downscale, as the original drawable bitmap is too large. - final Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, - APP_ICON_BITMAP_DIMENSION, APP_ICON_BITMAP_DIMENSION, /* filter= */ true); - final byte[] renderedBitmap = renderBitmapToByteArray(scaledBitmap); - scaledBitmap.recycle(); - return renderedBitmap; - } - return renderBitmapToByteArray(bitmap); - } - final Bitmap bitmap = Bitmap.createBitmap(APP_ICON_BITMAP_DIMENSION, - APP_ICON_BITMAP_DIMENSION, - Bitmap.Config.ARGB_8888); - try { - final Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); - drawable.draw(canvas); - return renderBitmapToByteArray(bitmap); - } finally { - bitmap.recycle(); - } - } - - private byte[] renderBitmapToByteArray(Bitmap bitmap) { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(bitmap.getByteCount()); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); - return baos.toByteArray(); - } - /** * Update the mute state of this call. No-op if the call is not capable of being muted. * @@ -148,7 +124,10 @@ public class CrossDeviceCall { @VisibleForTesting void updateCallDetails(Call.Details callDetails) { mCallerDisplayName = callDetails.getCallerDisplayName(); + mCallerDisplayNamePresentation = callDetails.getCallerDisplayNamePresentation(); mContactDisplayName = callDetails.getContactDisplayName(); + mHandle = callDetails.getHandle(); + mHandlePresentation = callDetails.getHandlePresentation(); mStatus = convertStateToStatus(callDetails.getState()); mControls.clear(); if (mStatus == android.companion.Telecom.Call.RINGING @@ -185,7 +164,14 @@ public class CrossDeviceCall { return android.companion.Telecom.Call.ONGOING; case Call.STATE_RINGING: return android.companion.Telecom.Call.RINGING; + case Call.STATE_AUDIO_PROCESSING: + return android.companion.Telecom.Call.AUDIO_PROCESSING; + case Call.STATE_SIMULATED_RINGING: + return android.companion.Telecom.Call.RINGING_SIMULATED; + case Call.STATE_DISCONNECTED: + return android.companion.Telecom.Call.DISCONNECTED; default: + Slog.e(TAG, "Couldn't resolve state to status: " + callState); return android.companion.Telecom.Call.UNKNOWN_STATUS; } } @@ -203,6 +189,12 @@ public class CrossDeviceCall { case android.companion.Telecom.Call.RINGING: case android.companion.Telecom.Call.RINGING_SILENCED: return Call.STATE_RINGING; + case android.companion.Telecom.Call.AUDIO_PROCESSING: + return Call.STATE_AUDIO_PROCESSING; + case android.companion.Telecom.Call.RINGING_SIMULATED: + return Call.STATE_SIMULATED_RINGING; + case android.companion.Telecom.Call.DISCONNECTED: + return Call.STATE_DISCONNECTED; case android.companion.Telecom.Call.UNKNOWN_STATUS: default: return Call.STATE_NEW; @@ -235,10 +227,23 @@ public class CrossDeviceCall { * @param isAdminBlocked whether there is an admin that has blocked contacts over Bluetooth */ public String getReadableCallerId(boolean isAdminBlocked) { - if (mIsOtt) { + if (mIsEnterprise && isAdminBlocked) { + // Cannot use any contact information. + return getNonContactString(); + } + return mContactDisplayName != null ? mContactDisplayName : getNonContactString(); + } + + private String getNonContactString() { + if (mCallerDisplayName != null + && mCallerDisplayNamePresentation == TelecomManager.PRESENTATION_ALLOWED) { return mCallerDisplayName; } - return mIsEnterprise && isAdminBlocked ? mCallerDisplayName : mContactDisplayName; + if (mHandle != null && mHandle.getSchemeSpecificPart() != null + && mHandlePresentation == TelecomManager.PRESENTATION_ALLOWED) { + return mHandle.getSchemeSpecificPart(); + } + return null; } public int getStatus() { @@ -249,6 +254,10 @@ public class CrossDeviceCall { return mControls; } + public boolean isCallPlacedByContextSync() { + return mIsCallPlacedByContextSync; + } + void doAccept() { mCall.answer(VideoProfile.STATE_AUDIO_ONLY); } diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java index 937d7fed3542..bf82f3f4a352 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java @@ -20,6 +20,7 @@ import static com.android.server.companion.transport.Transport.MESSAGE_REQUEST_C import android.app.admin.DevicePolicyManager; import android.companion.AssociationInfo; +import android.companion.CompanionDeviceManager; import android.companion.ContextSyncMessage; import android.companion.IOnMessageReceivedListener; import android.companion.IOnTransportsChangedListener; @@ -44,8 +45,10 @@ import com.android.server.companion.CompanionDeviceConfig; import com.android.server.companion.transport.CompanionTransportManager; import java.io.IOException; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -54,6 +57,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; /** * Monitors connections and sending / receiving of synced data. @@ -62,6 +66,13 @@ public class CrossDeviceSyncController { private static final String TAG = "CrossDeviceSyncController"; + public static final String EXTRA_CALL_ID = + "com.android.companion.datatransfer.contextsync.extra.CALL_ID"; + static final String EXTRA_FACILITATOR_ICON = + "com.android.companion.datatransfer.contextsync.extra.FACILITATOR_ICON"; + static final String EXTRA_IS_REMOTE_ORIGIN = + "com.android.companion.datatransfer.contextsync.extra.IS_REMOTE_ORIGIN"; + static final String EXTRA_ASSOCIATION_ID = "com.android.server.companion.datatransfer.contextsync.extra.ASSOCIATION_ID"; static final String EXTRA_CALL = @@ -78,11 +89,13 @@ public class CrossDeviceSyncController { private final Context mContext; private final CompanionTransportManager mCompanionTransportManager; private final PhoneAccountManager mPhoneAccountManager; + private final CallManager mCallManager; private final List<AssociationInfo> mConnectedAssociations = new ArrayList<>(); private final Set<Integer> mBlocklist = new HashSet<>(); private final List<CallMetadataSyncData.CallFacilitator> mCallFacilitators = new ArrayList<>(); - private CrossDeviceSyncControllerCallback mCrossDeviceSyncControllerCallback; + private WeakReference<CrossDeviceSyncControllerCallback> mInCallServiceCallbackRef; + private WeakReference<CrossDeviceSyncControllerCallback> mConnectionServiceCallbackRef; public CrossDeviceSyncController(Context context, CompanionTransportManager companionTransportManager) { @@ -104,25 +117,77 @@ public class CrossDeviceSyncController { mConnectedAssociations); mConnectedAssociations.clear(); mConnectedAssociations.addAll(newAssociations); - if (mCrossDeviceSyncControllerCallback == null) { - Slog.w(TAG, "No callback to report transports changed"); - return; - } for (AssociationInfo associationInfo : newAssociations) { - if (!existingAssociations.contains(associationInfo) - && !isAssociationBlocked(associationInfo.getId())) { - mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( - associationInfo.getUserId(), /* added= */ true); - mCrossDeviceSyncControllerCallback.requestCrossDeviceSync(associationInfo); + if (!existingAssociations.contains(associationInfo)) { + // New association. + if (!isAssociationBlocked(associationInfo)) { + final CrossDeviceSyncControllerCallback callback = + mInCallServiceCallbackRef != null + ? mInCallServiceCallbackRef.get() : null; + if (callback != null) { + callback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ true); + callback.requestCrossDeviceSync(associationInfo); + } else { + Slog.w(TAG, "No callback to report new transport"); + syncMessageToDevice(associationInfo.getId(), + createFacilitatorMessage()); + } + } else { + mBlocklist.add(associationInfo.getId()); + Slog.i(TAG, "New association was blocked from context syncing"); + } } } for (AssociationInfo associationInfo : existingAssociations) { if (!newAssociations.contains(associationInfo)) { - if (isAssociationBlocked(associationInfo.getId())) { - mBlocklist.remove(associationInfo.getId()); - } else { - mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( - associationInfo.getUserId(), /* added= */ false); + // Removed association! + mBlocklist.remove(associationInfo.getId()); + if (!isAssociationBlockedLocal(associationInfo.getId())) { + final CrossDeviceSyncControllerCallback callback = + mInCallServiceCallbackRef != null + ? mInCallServiceCallbackRef.get() : null; + if (callback != null) { + callback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ false); + } else { + Slog.w(TAG, "No callback to report removed transport"); + } + } + } else { + // Stable association! + final boolean systemBlocked = isAssociationBlocked(associationInfo); + if (isAssociationBlockedLocal(associationInfo.getId()) != systemBlocked) { + // Block state has changed. + final CrossDeviceSyncControllerCallback callback = + mInCallServiceCallbackRef != null + ? mInCallServiceCallbackRef.get() : null; + if (!systemBlocked) { + Slog.i(TAG, "Unblocking existing association for context sync"); + mBlocklist.remove(associationInfo.getId()); + if (callback != null) { + callback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ true); + callback.requestCrossDeviceSync(associationInfo); + } else { + Slog.w(TAG, "No callback to report changed transport"); + syncMessageToDevice(associationInfo.getId(), + createFacilitatorMessage()); + } + } else { + Slog.i(TAG, "Blocking existing association for context sync"); + mBlocklist.add(associationInfo.getId()); + if (callback != null) { + callback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ false); + } else { + Slog.w(TAG, "No callback to report changed transport"); + } + // Send empty message to device to clear its data (otherwise it + // will get stale) + syncMessageToDevice(associationInfo.getId(), + createEmptyMessage()); + } } } } @@ -132,22 +197,76 @@ public class CrossDeviceSyncController { new IOnMessageReceivedListener.Stub() { @Override public void onMessageReceived(int associationId, byte[] data) { + if (isAssociationBlockedLocal(associationId)) { + return; + } final CallMetadataSyncData processedData = processTelecomDataFromSync(data); mPhoneAccountManager.updateFacilitators(associationId, processedData); - processCallCreateRequests(associationId, processedData); - if (mCrossDeviceSyncControllerCallback == null) { + mCallManager.updateCalls(associationId, processedData); + processCallCreateRequests(processedData); + if (mInCallServiceCallbackRef == null + && mConnectionServiceCallbackRef == null) { Slog.w(TAG, "No callback to process context sync message"); return; } - mCrossDeviceSyncControllerCallback.processContextSyncMessage(associationId, - processedData); + final CrossDeviceSyncControllerCallback inCallServiceCallback = + mInCallServiceCallbackRef != null ? mInCallServiceCallbackRef.get() + : null; + if (inCallServiceCallback != null) { + inCallServiceCallback.processContextSyncMessage(associationId, + processedData); + } else { + // This is dead; get rid of it lazily + mInCallServiceCallbackRef = null; + } + + final CrossDeviceSyncControllerCallback connectionServiceCallback = + mConnectionServiceCallbackRef != null + ? mConnectionServiceCallbackRef.get() : null; + if (connectionServiceCallback != null) { + connectionServiceCallback.processContextSyncMessage(associationId, + processedData); + } else { + // This is dead; get rid of it lazily + mConnectionServiceCallbackRef = null; + } } }); mPhoneAccountManager = new PhoneAccountManager(mContext); + mCallManager = new CallManager(mContext, mPhoneAccountManager); + } + + private static boolean isAssociationBlocked(AssociationInfo info) { + return (info.getSystemDataSyncFlags() & CompanionDeviceManager.FLAG_CALL_METADATA) + != CompanionDeviceManager.FLAG_CALL_METADATA; + } + + /** Invoke set-up tasks that happen when boot is completed. */ + public void onBootCompleted() { + if (!CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + return; + } + + mPhoneAccountManager.onBootCompleted(); + + final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class); + if (telecomManager != null && telecomManager.getCallCapablePhoneAccounts().size() != 0) { + final PhoneAccountHandle defaultOutgoingTelAccountHandle = + telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL); + if (defaultOutgoingTelAccountHandle != null) { + final PhoneAccount defaultOutgoingTelAccount = telecomManager.getPhoneAccount( + defaultOutgoingTelAccountHandle); + if (defaultOutgoingTelAccount != null) { + mCallFacilitators.add( + new CallMetadataSyncData.CallFacilitator( + defaultOutgoingTelAccount.getLabel().toString(), + FACILITATOR_ID_SYSTEM)); + } + } + } } - private void processCallCreateRequests(int associationId, - CallMetadataSyncData callMetadataSyncData) { + private void processCallCreateRequests(CallMetadataSyncData callMetadataSyncData) { final Iterator<CallMetadataSyncData.CallCreateRequest> iterator = callMetadataSyncData.getCallCreateRequests().iterator(); while (iterator.hasNext()) { @@ -159,7 +278,7 @@ public class CrossDeviceSyncController { final Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, request.getAddress().replaceAll("\\D+", ""), /* fragment= */ null); final Bundle extras = new Bundle(); - extras.putString(CrossDeviceCall.EXTRA_CALL_ID, request.getId()); + extras.putString(EXTRA_CALL_ID, request.getId()); final Bundle outerExtras = new Bundle(); outerExtras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras); mContext.getSystemService(TelecomManager.class).placeCall(uri, outerExtras); @@ -171,39 +290,33 @@ public class CrossDeviceSyncController { } } - private boolean isAssociationBlocked(int associationId) { + /** + * This keeps track of "previous" state to calculate deltas. Use {@link #isAssociationBlocked} + * for all other use cases. + */ + private boolean isAssociationBlockedLocal(int associationId) { return mBlocklist.contains(associationId); } /** Registers the call metadata callback. */ - public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback) { - mCrossDeviceSyncControllerCallback = callback; - for (AssociationInfo associationInfo : mConnectedAssociations) { - if (!isAssociationBlocked(associationInfo.getId())) { - mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( - associationInfo.getUserId(), /* added= */ true); - mCrossDeviceSyncControllerCallback.requestCrossDeviceSync(associationInfo); - } - } - } - - /** Allow specific associated devices to enable / disable syncing. */ - public void setSyncEnabled(AssociationInfo associationInfo, boolean enabled) { - if (enabled) { - if (isAssociationBlocked(associationInfo.getId())) { - mBlocklist.remove(associationInfo.getId()); - mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( - associationInfo.getUserId(), /* added= */ true); - mCrossDeviceSyncControllerCallback.requestCrossDeviceSync(associationInfo); + public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback, + @CrossDeviceSyncControllerCallback.Type int type) { + if (type == CrossDeviceSyncControllerCallback.TYPE_IN_CALL_SERVICE) { + mInCallServiceCallbackRef = new WeakReference<>(callback); + for (AssociationInfo associationInfo : mConnectedAssociations) { + if (!isAssociationBlocked(associationInfo)) { + mBlocklist.remove(associationInfo.getId()); + callback.updateNumberOfActiveSyncAssociations(associationInfo.getUserId(), + /* added= */ true); + callback.requestCrossDeviceSync(associationInfo); + } else { + mBlocklist.add(associationInfo.getId()); + } } + } else if (type == CrossDeviceSyncControllerCallback.TYPE_CONNECTION_SERVICE) { + mConnectionServiceCallbackRef = new WeakReference<>(callback); } else { - if (!isAssociationBlocked(associationInfo.getId())) { - mBlocklist.add(associationInfo.getId()); - mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( - associationInfo.getUserId(), /* added= */ false); - // Send empty message to device to clear its data (otherwise it will get stale) - syncMessageToDevice(associationInfo.getId(), createEmptyMessage()); - } + Slog.e(TAG, "Cannot register callback of unknown type: " + type); } } @@ -221,8 +334,7 @@ public class CrossDeviceSyncController { public void syncToAllDevicesForUserId(int userId, Collection<CrossDeviceCall> calls) { final Set<Integer> associationIds = new HashSet<>(); for (AssociationInfo associationInfo : mConnectedAssociations) { - if (associationInfo.getUserId() == userId && !isAssociationBlocked( - associationInfo.getId())) { + if (associationInfo.getUserId() == userId && !isAssociationBlocked(associationInfo)) { associationIds.add(associationInfo.getId()); } } @@ -244,7 +356,7 @@ public class CrossDeviceSyncController { */ public void syncToSingleDevice(AssociationInfo associationInfo, Collection<CrossDeviceCall> calls) { - if (isAssociationBlocked(associationInfo.getId())) { + if (isAssociationBlocked(associationInfo)) { Slog.e(TAG, "Cannot sync to requested device; connection is blocked"); return; } @@ -261,7 +373,7 @@ public class CrossDeviceSyncController { * @param message The message to sync. */ public void syncMessageToDevice(int associationId, byte[] message) { - if (isAssociationBlocked(associationId)) { + if (isAssociationBlockedLocal(associationId)) { Slog.e(TAG, "Cannot sync to requested device; connection is blocked"); return; } @@ -466,6 +578,10 @@ public class CrossDeviceSyncController { pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION); final long telecomToken = pos.start(ContextSyncMessage.TELECOM); for (CrossDeviceCall call : calls) { + if (call.isCallPlacedByContextSync()) { + // Do not sync any calls which our "ours" as that would be duplicative. + continue; + } final long callsToken = pos.start(Telecom.CALLS); pos.write(Telecom.Call.ID, call.getId()); final long originToken = pos.start(Telecom.Call.ORIGIN); @@ -534,6 +650,50 @@ public class CrossDeviceSyncController { return pos.getBytes(); } + /** Create a facilitator-only message, used before any calls are available as a call intake. */ + private byte[] createFacilitatorMessage() { + return createCallUpdateMessage(Collections.emptyList(), -1); + } + + @VisibleForTesting + static class CallManager { + + @VisibleForTesting final Map<Integer, Set<String>> mCallIds = new HashMap<>(); + private final TelecomManager mTelecomManager; + private final PhoneAccountManager mPhoneAccountManager; + + CallManager(Context context, PhoneAccountManager phoneAccountManager) { + mTelecomManager = context.getSystemService(TelecomManager.class); + mPhoneAccountManager = phoneAccountManager; + } + + /** Add any new calls to Telecom. The ConnectionService will handle everything else. */ + void updateCalls(int associationId, CallMetadataSyncData data) { + final Set<String> oldCallIds = mCallIds.getOrDefault(associationId, new HashSet<>()); + final Set<String> newCallIds = data.getCalls().stream().map( + CallMetadataSyncData.Call::getId).collect(Collectors.toSet()); + if (oldCallIds.equals(newCallIds)) { + return; + } + + for (CallMetadataSyncData.Call currentCall : data.getCalls()) { + if (!oldCallIds.contains(currentCall.getId()) + && currentCall.getFacilitator() != null) { + final Bundle extras = new Bundle(); + extras.putInt(EXTRA_ASSOCIATION_ID, associationId); + extras.putBoolean(EXTRA_IS_REMOTE_ORIGIN, true); + extras.putParcelable(EXTRA_CALL, currentCall); + extras.putString(EXTRA_CALL_ID, currentCall.getId()); + extras.putByteArray(EXTRA_FACILITATOR_ICON, currentCall.getAppIcon()); + final PhoneAccountHandle handle = mPhoneAccountManager.getPhoneAccountHandle( + associationId, currentCall.getFacilitator().getIdentifier()); + mTelecomManager.addNewIncomingCall(handle, extras); + } + } + mCallIds.put(associationId, newCallIds); + } + } + static class PhoneAccountManager { private final Map<PhoneAccountHandleIdentifier, PhoneAccountHandle> mPhoneAccountHandles = new HashMap<>(); @@ -546,6 +706,10 @@ public class CrossDeviceSyncController { CallMetadataSyncConnectionService.class); } + void onBootCompleted() { + mTelecomManager.clearPhoneAccounts(); + } + PhoneAccountHandle getPhoneAccountHandle(int associationId, String appIdentifier) { return mPhoneAccountHandles.get( new PhoneAccountHandleIdentifier(associationId, appIdentifier)); diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerCallback.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerCallback.java index 31e10a814568..8a0ba27b7526 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerCallback.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerCallback.java @@ -16,11 +16,25 @@ package com.android.server.companion.datatransfer.contextsync; +import android.annotation.IntDef; import android.companion.AssociationInfo; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** Callback for call metadata syncing. */ public abstract class CrossDeviceSyncControllerCallback { + static final int TYPE_CONNECTION_SERVICE = 1; + static final int TYPE_IN_CALL_SERVICE = 2; + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_CONNECTION_SERVICE, + TYPE_IN_CALL_SERVICE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type { + } + void processContextSyncMessage(int associationId, CallMetadataSyncData callMetadataSyncData) {} void requestCrossDeviceSync(AssociationInfo associationInfo) {} diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java index 0457e9aa345d..5a3db4b18a1a 100644 --- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java +++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java @@ -53,8 +53,6 @@ public class SecureChannel { private static final int VERSION = 1; private static final int HEADER_LENGTH = 6; - private static final String HANDSHAKE_PROTOCOL = "AES_256_CBC-HMAC_SHA256"; - private final InputStream mInput; private final OutputStream mOutput; private final Callback mCallback; @@ -62,14 +60,16 @@ public class SecureChannel { private final AttestationVerifier mVerifier; private volatile boolean mStopped; - private boolean mInProgress; + private volatile boolean mInProgress; private Role mRole; + private byte[] mClientInit; private D2DHandshakeContext mHandshakeContext; private D2DConnectionContextV1 mConnectionContext; private String mAlias; private int mVerificationResult; + private boolean mPskVerified; /** @@ -202,8 +202,8 @@ public class SecureChannel { } try { - initiateHandshake(); mInProgress = true; + initiateHandshake(); } catch (BadHandleException e) { throw new SecureChannelException("Failed to initiate handshake protocol.", e); } @@ -329,12 +329,56 @@ public class SecureChannel { mRole = Role.Initiator; mHandshakeContext = D2DHandshakeContext.forInitiator(); + mClientInit = mHandshakeContext.getNextHandshakeMessage(); // Send Client Init if (DEBUG) { Slog.d(TAG, "Sending Ukey2 Client Init message"); } - sendMessage(MessageType.HANDSHAKE_INIT, mHandshakeContext.getNextHandshakeMessage()); + sendMessage(MessageType.HANDSHAKE_INIT, constructHandshakeInitMessage(mClientInit)); + } + + // In an occasion where both participants try to initiate a handshake, resolve the conflict + // with a dice roll simulated by the message byte content comparison. + // The higher value wins! (a.k.a. gets to be the initiator) + private byte[] handleHandshakeCollision(byte[] handshakeInitMessage) + throws IOException, HandshakeException, BadHandleException, CryptoException { + + // First byte indicates message type; 0 = CLIENT INIT, 1 = SERVER INIT + ByteBuffer buffer = ByteBuffer.wrap(handshakeInitMessage); + boolean isClientInit = buffer.get() == 0; + byte[] handshakeMessage = new byte[buffer.remaining()]; + buffer.get(handshakeMessage); + + // If received message is Server Init or current role is Responder, then there was + // no collision. Return extracted handshake message. + if (mHandshakeContext == null || !isClientInit) { + return handshakeMessage; + } + + Slog.w(TAG, "Detected a Ukey2 handshake role collision. Negotiating a role."); + + // if received message is "larger" than the sent message, then reset the handshake context. + if (compareByteArray(mClientInit, handshakeMessage) < 0) { + Slog.d(TAG, "Assigned: Responder"); + mHandshakeContext = null; + return handshakeMessage; + } else { + Slog.d(TAG, "Assigned: Initiator; Discarding received Client Init"); + + // Wait for another init message after discarding the client init + ByteBuffer nextInitMessage = ByteBuffer.wrap(readMessage(MessageType.HANDSHAKE_INIT)); + + // Throw if this message is a Client Init again; 0 = CLIENT INIT, 1 = SERVER INIT + if (nextInitMessage.get() == 0) { + // This should never happen! + throw new HandshakeException("Failed to resolve Ukey2 handshake role collision."); + } + byte[] nextHandshakeMessage = new byte[nextInitMessage.remaining()]; + nextInitMessage.get(nextHandshakeMessage); + + return nextHandshakeMessage; + } } private void exchangeHandshake() @@ -345,8 +389,15 @@ public class SecureChannel { } // Waiting for message - byte[] handshakeMessage = readMessage(MessageType.HANDSHAKE_INIT); + byte[] handshakeInitMessage = readMessage(MessageType.HANDSHAKE_INIT); + + // Mark "in-progress" upon receiving the first message + mInProgress = true; + // Handle a potential collision where both devices tried to initiate a connection + byte[] handshakeMessage = handleHandshakeCollision(handshakeInitMessage); + + // Proceed with the rest of Ukey2 handshake if (mHandshakeContext == null) { // Server-side logic mRole = Role.Responder; mHandshakeContext = D2DHandshakeContext.forResponder(); @@ -361,7 +412,8 @@ public class SecureChannel { if (DEBUG) { Slog.d(TAG, "Sending Ukey2 Server Init message"); } - sendMessage(MessageType.HANDSHAKE_INIT, mHandshakeContext.getNextHandshakeMessage()); + sendMessage(MessageType.HANDSHAKE_INIT, + constructHandshakeInitMessage(mHandshakeContext.getNextHandshakeMessage())); // Receive Client Finish if (DEBUG) { @@ -418,9 +470,9 @@ public class SecureChannel { ? Role.Responder : Role.Initiator, mPreSharedKey); - boolean authenticated = Arrays.equals(receivedAuthToken, expectedAuthToken); + mPskVerified = Arrays.equals(receivedAuthToken, expectedAuthToken); - if (!authenticated) { + if (!mPskVerified) { throw new SecureChannelException("Failed to verify the hash of pre-shared key."); } @@ -477,10 +529,21 @@ public class SecureChannel { } private boolean isSecured() { + // Is ukey-2 encrypted if (mConnectionContext == null) { return false; } - return mVerifier == null || mVerificationResult == RESULT_SUCCESS; + // Is authenticated + return mPskVerified || mVerificationResult == RESULT_SUCCESS; + } + + // First byte indicates message type; 0 = CLIENT INIT, 1 = SERVER INIT + // This information is needed to help resolve potential role collision. + private byte[] constructHandshakeInitMessage(byte[] message) { + return ByteBuffer.allocate(1 + message.length) + .put((byte) (Role.Initiator.equals(mRole) ? 0 : 1)) + .put(message) + .array(); } private byte[] constructToken(D2DHandshakeContext.Role role, byte[] authValue) @@ -494,6 +557,22 @@ public class SecureChannel { .array()); } + // Arbitrary comparator + private int compareByteArray(byte[] a, byte[] b) { + if (a == b) { + return 0; + } + if (a.length != b.length) { + return a.length - b.length; + } + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return a[i] - b[i]; + } + } + return 0; + } + private String generateAlias() { String alias; do { 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 949f39ae1609..2d856b9614cb 100644 --- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java +++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java @@ -29,7 +29,6 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Future; class SecureTransport extends Transport implements SecureChannel.Callback { private final SecureChannel mSecureChannel; @@ -68,24 +67,13 @@ class SecureTransport extends Transport implements SecureChannel.Callback { } @Override - public Future<byte[]> requestForResponse(int message, byte[] data) { - // Check if channel is secured and start securing + protected void sendMessage(int message, int sequence, @NonNull byte[] data) + throws IOException { + // Check if channel is secured; otherwise start securing if (!mShouldProcessRequests) { - Slog.d(TAG, "Establishing secure connection."); - try { - mSecureChannel.establishSecureConnection(); - } catch (Exception e) { - Slog.w(TAG, "Failed to initiate secure channel handshake.", e); - onError(e); - } + establishSecureConnection(); } - return super.requestForResponse(message, data); - } - - @Override - protected void sendMessage(int message, int sequence, @NonNull byte[] data) - throws IOException { if (DEBUG) { Slog.d(TAG, "Queueing message 0x" + Integer.toHexString(message) + " sequence " + sequence + " length " + data.length @@ -103,6 +91,16 @@ class SecureTransport extends Transport implements SecureChannel.Callback { } } + private void establishSecureConnection() { + Slog.d(TAG, "Establishing secure connection."); + try { + mSecureChannel.establishSecureConnection(); + } catch (Exception e) { + Slog.w(TAG, "Failed to initiate secure channel handshake.", e); + onError(e); + } + } + @Override public void onSecureConnection() { mShouldProcessRequests = true; diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index ad4c0bf26d62..e9b9980b93eb 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -55,6 +55,7 @@ import android.util.SparseArray; import android.view.Display; import android.widget.Toast; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; @@ -156,7 +157,7 @@ public class VirtualDeviceManagerService extends SystemService { VirtualDeviceImpl virtualDevice = virtualDevicesSnapshot.get(i); virtualDevice.showToastWhereUidIsRunning(appUid, getContext().getString( - com.android.internal.R.string.vdm_camera_access_denied, + R.string.vdm_camera_access_denied, virtualDevice.getDisplayName()), Toast.LENGTH_LONG, Looper.myLooper()); } @@ -623,6 +624,18 @@ public class VirtualDeviceManagerService extends SystemService { } @Override + public void onAuthenticationPrompt(int uid) { + synchronized (mVirtualDeviceManagerLock) { + for (int i = 0; i < mVirtualDevices.size(); i++) { + VirtualDeviceImpl device = mVirtualDevices.valueAt(i); + device.showToastWhereUidIsRunning(uid, + R.string.app_streaming_blocked_message_for_fingerprint_dialog, + Toast.LENGTH_LONG, Looper.getMainLooper()); + } + } + } + + @Override public int getBaseVirtualDisplayFlags(IVirtualDevice virtualDevice) { return ((VirtualDeviceImpl) virtualDevice).getBaseVirtualDisplayFlags(); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 96766a20c803..0da25be8c8cc 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -262,8 +262,7 @@ public final class ActiveServices { private static final boolean DEBUG_DELAYED_SERVICE = DEBUG_SERVICE; private static final boolean DEBUG_DELAYED_STARTS = DEBUG_DELAYED_SERVICE; - // STOPSHIP(b/260012573) turn it off. - private static final boolean DEBUG_SHORT_SERVICE = true; // DEBUG_SERVICE; + private static final boolean DEBUG_SHORT_SERVICE = DEBUG_SERVICE; private static final boolean LOG_SERVICE_START_STOP = DEBUG_SERVICE; @@ -2215,6 +2214,8 @@ public final class ActiveServices { } } + boolean resetNeededForLogging = false; + // Re-evaluate mAllowWhileInUsePermissionInFgs and mAllowStartForeground // (i.e. while-in-use and BFSL flags) if needed. // @@ -2297,8 +2298,22 @@ public final class ActiveServices { !r.isForeground && delayMs > mAm.mConstants.mFgsStartForegroundTimeoutMs; if (resetNeeded) { - resetFgsRestrictionLocked(r); + // We don't want to reset mDebugWhileInUseReasonInBindService here -- + // we'll instead reset it in the following code, using the simulated + // legacy logic. + resetFgsRestrictionLocked(r, + /*resetDebugWhileInUseReasonInBindService=*/ false); + } + + // Simulate the reset flow in the legacy logic to reset + // mDebugWhileInUseReasonInBindService. + // (Which is only used to compare to the old logic.) + final long legacyDelayMs = SystemClock.elapsedRealtime() - r.createRealTime; + if ((r.mStartForegroundCount == 0) + && (legacyDelayMs > mAm.mConstants.mFgsStartForegroundTimeoutMs)) { + r.mDebugWhileInUseReasonInBindService = REASON_DENIED; } + setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(), r.appInfo.uid, r.intent.getIntent(), r, r.userId, BackgroundStartPrivileges.NONE, @@ -2315,12 +2330,24 @@ public final class ActiveServices { r.mInfoAllowStartForeground = temp; } r.mLoggedInfoAllowStartForeground = false; + + resetNeededForLogging = resetNeeded; } // If the service has any bindings and it's not yet a FGS // we compare the new and old while-in-use logics. // (If it's not the first startForeground() call, we already reset the // while-in-use and BFSL flags, so the logic change wouldn't matter.) + // + // Note, mDebugWhileInUseReasonInBindService does *not* fully simulate the + // legacy logic, because we'll only set it in bindService(), but the actual + // mAllowWhileInUsePermissionInFgsReason can change afterwards, in a subsequent + // Service.startForeground(). This check will only provide "rough" check. + // But if mDebugWhileInUseReasonInBindService is _not_ DENIED, and + // mDebugWhileInUseReasonInStartForeground _is_ DENIED, then that means we'd + // now detected a behavior change. + // OTOH, if it's changing from non-DENIED to another non-DENIED, that may + // not be a problem. if (enableFgsWhileInUseFix && !r.isForeground && (r.getConnections().size() > 0) @@ -2330,6 +2357,10 @@ public final class ActiveServices { + reasonCodeToString(r.mDebugWhileInUseReasonInBindService) + " new=" + reasonCodeToString(r.mDebugWhileInUseReasonInStartForeground) + + " startForegroundCount=" + r.mStartForegroundCount + + " started=" + r.startRequested + + " num_bindings=" + r.getConnections().size() + + " resetNeeded=" + resetNeededForLogging + " " + r.shortInstanceName); } @@ -2668,7 +2699,10 @@ public final class ActiveServices { + " code=" + code + " callerApp=" + r.app + " targetSDK=" + r.app.info.targetSdkVersion - + " requiredPermissions=" + policyInfo.toPermissionString(); + + " requiredPermissions=" + policyInfo.toPermissionString() + + (policyInfo.hasForegroundOnlyPermission() + ? " and the app must be in the eligible state/exemptions" + + " to access the foreground only permission" : ""); Slog.wtfQuiet(TAG, msg); Slog.w(TAG, msg); } break; @@ -2678,7 +2712,10 @@ public final class ActiveServices { + " callerApp=" + r.app + " targetSDK=" + r.app.info.targetSdkVersion + " requires permissions: " - + policyInfo.toPermissionString()); + + policyInfo.toPermissionString() + + (policyInfo.hasForegroundOnlyPermission() + ? " and the app must be in the eligible state/exemptions" + + " to access the foreground only permission" : "")); } break; case FGS_TYPE_POLICY_CHECK_OK: default: @@ -4504,10 +4541,11 @@ public final class ActiveServices { + ", uid=" + callingUid + " requires " + r.permission); return new ServiceLookupResult(r.permission); - } else if (Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE.equals(r.permission) + } else if ((Manifest.permission.BIND_HOTWORD_DETECTION_SERVICE.equals(r.permission) + || Manifest.permission.BIND_VISUAL_QUERY_DETECTION_SERVICE.equals(r.permission)) && callingUid != Process.SYSTEM_UID) { - // Hotword detection must run in its own sandbox, and we don't even trust - // its enclosing application to bind to it - only the system. + // Hotword detection and visual query detection must run in its own sandbox, and we + // don't even trust its enclosing application to bind to it - only the system. // TODO(b/185746653) remove this special case and generalize Slog.w(TAG, "Permission Denial: Accessing service " + r.shortInstanceName + " from pid=" + callingPid @@ -5243,6 +5281,7 @@ public final class ActiveServices { final IApplicationThread thread = app.getThread(); final int pid = app.getPid(); final UidRecord uidRecord = app.getUidRecord(); + r.isolationHostProc = app; if (thread != null) { try { if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { @@ -7567,15 +7606,27 @@ public final class ActiveServices { } } + /** + * Reset various while-in-use and BFSL related information. + */ void resetFgsRestrictionLocked(ServiceRecord r) { + resetFgsRestrictionLocked(r, /*resetDebugWhileInUseReasonInBindService=*/ true); + } + + /** + * Reset various while-in-use and BFSL related information. + */ + void resetFgsRestrictionLocked(ServiceRecord r, + boolean resetDebugWhileInUseReasonInBindService) { r.mAllowWhileInUsePermissionInFgs = false; r.mAllowWhileInUsePermissionInFgsReason = REASON_DENIED; r.mDebugWhileInUseReasonInStartForeground = REASON_DENIED; - // We don't reset mWhileInUseReasonInBindService here, because if we do this, we would - // lose it in the "reevaluation" case in startForeground(), where we call - // resetFgsRestrictionLocked(). - // Not resetting this is fine because it's only used in the first Service.startForeground() - // case, and there's no situations where we call resetFgsRestrictionLocked() before that. + + // In Service.startForeground(), we reset this field using a legacy logic, + // so resetting this field is optional. + if (resetDebugWhileInUseReasonInBindService) { + r.mDebugWhileInUseReasonInBindService = REASON_DENIED; + } r.mAllowStartForeground = REASON_DENIED; r.mInfoAllowStartForeground = null; r.mInfoTempFgsAllowListReason = null; @@ -8246,12 +8297,14 @@ public final class ActiveServices { r.mFgsDelegation != null ? r.mFgsDelegation.mOptions.mDelegationService : ForegroundServiceDelegationOptions.DELEGATION_SERVICE_DEFAULT, 0 /* api_sate */, - null /* api_type */, - null /* api_timestamp */, + 0 /* api_type */, + 0 /* api_timestamp */, mAm.getUidStateLocked(r.appInfo.uid), mAm.getUidProcessCapabilityLocked(r.appInfo.uid), mAm.getUidStateLocked(r.mRecentCallingUid), - mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid)); + mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid), + 0, + 0); int event = 0; if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) { @@ -8426,6 +8479,9 @@ public final class ActiveServices { true, false, null, false, AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); registerAppOpCallbackLocked(r); + synchronized (mFGSLogger) { + mFGSLogger.logForegroundServiceStart(r.appInfo.uid, 0, r); + } logFGSStateChangeLocked(r, FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER, 0, FGS_STOP_REASON_UNKNOWN, FGS_TYPE_POLICY_CHECK_UNKNOWN); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 658e6649b46d..bfa397fa2b03 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1175,17 +1175,21 @@ public class ActivityManagerService extends IActivityManager.Stub static final class StickyBroadcast { public Intent intent; public boolean deferUntilActive; + public int originalCallingUid; - public static StickyBroadcast create(Intent intent, boolean deferUntilActive) { + public static StickyBroadcast create(Intent intent, boolean deferUntilActive, + int originalCallingUid) { final StickyBroadcast b = new StickyBroadcast(); b.intent = intent; b.deferUntilActive = deferUntilActive; + b.originalCallingUid = originalCallingUid; return b; } @Override public String toString() { - return "{intent=" + intent + ", defer=" + deferUntilActive + "}"; + return "{intent=" + intent + ", defer=" + deferUntilActive + ", originalCallingUid=" + + originalCallingUid + "}"; } } @@ -11119,6 +11123,9 @@ public class ActivityManagerService extends IActivityManager.Stub pw.print(" [D]"); } pw.println(); + pw.print(" originalCallingUid: "); + pw.println(broadcasts.get(i).originalCallingUid); + pw.println(); Bundle bundle = intent.getExtras(); if (bundle != null) { pw.print(" extras: "); @@ -14008,16 +14015,25 @@ public class ActivityManagerService extends IActivityManager.Stub if (allSticky != null) { ArrayList receivers = new ArrayList(); receivers.add(bf); + sticky = null; final int stickyCount = allSticky.size(); for (int i = 0; i < stickyCount; i++) { final StickyBroadcast broadcast = allSticky.get(i); + final int originalStickyCallingUid = allSticky.get(i).originalCallingUid; + // TODO(b/281889567): consider using checkComponentPermission instead of + // canAccessUnexportedComponents + if (sticky == null && (exported || originalStickyCallingUid == callingUid + || ActivityManager.canAccessUnexportedComponents( + originalStickyCallingUid))) { + sticky = broadcast.intent; + } BroadcastQueue queue = broadcastQueueForIntent(broadcast.intent); BroadcastRecord r = new BroadcastRecord(queue, broadcast.intent, null, null, null, -1, -1, false, null, null, null, null, OP_NONE, BroadcastOptions.makeWithDeferUntilActive(broadcast.deferUntilActive), receivers, null, null, 0, null, null, false, true, true, -1, - BackgroundStartPrivileges.NONE, + originalStickyCallingUid, BackgroundStartPrivileges.NONE, false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */, null /* filterExtrasForReceiver */); queue.enqueueBroadcastLocked(r); @@ -14895,12 +14911,13 @@ public class ActivityManagerService extends IActivityManager.Stub for (i = 0; i < stickiesCount; i++) { if (intent.filterEquals(list.get(i).intent)) { // This sticky already exists, replace it. - list.set(i, StickyBroadcast.create(new Intent(intent), deferUntilActive)); + list.set(i, StickyBroadcast.create(new Intent(intent), deferUntilActive, + callingUid)); break; } } if (i >= stickiesCount) { - list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive)); + list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive, callingUid)); } } @@ -18877,12 +18894,14 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void waitForBroadcastIdle() { - waitForBroadcastIdle(LOG_WRITER_INFO); + waitForBroadcastIdle(LOG_WRITER_INFO, false); } - public void waitForBroadcastIdle(@NonNull PrintWriter pw) { + void waitForBroadcastIdle(@NonNull PrintWriter pw, boolean flushBroadcastLoopers) { enforceCallingPermission(permission.DUMP, "waitForBroadcastIdle()"); - BroadcastLoopers.waitForIdle(pw); + if (flushBroadcastLoopers) { + BroadcastLoopers.waitForIdle(pw); + } for (BroadcastQueue queue : mBroadcastQueues) { queue.waitForIdle(pw); } @@ -18895,7 +18914,7 @@ public class ActivityManagerService extends IActivityManager.Stub waitForBroadcastBarrier(LOG_WRITER_INFO, false, false); } - public void waitForBroadcastBarrier(@NonNull PrintWriter pw, + void waitForBroadcastBarrier(@NonNull PrintWriter pw, boolean flushBroadcastLoopers, boolean flushApplicationThreads) { enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()"); if (flushBroadcastLoopers) { @@ -18913,7 +18932,7 @@ public class ActivityManagerService extends IActivityManager.Stub * Wait for all pending {@link IApplicationThread} events to be processed in * all currently running apps. */ - public void waitForApplicationBarrier(@NonNull PrintWriter pw) { + void waitForApplicationBarrier(@NonNull PrintWriter pw) { final CountDownLatch finishedLatch = new CountDownLatch(1); final AtomicInteger pingCount = new AtomicInteger(0); final AtomicInteger pongCount = new AtomicInteger(0); diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 8759e3f207c4..add22bd6009e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -918,6 +918,7 @@ final class ActivityManagerShellCommand extends ShellCommand { } int runSendBroadcast(PrintWriter pw) throws RemoteException { + pw = new PrintWriter(new TeeWriter(LOG_WRITER_INFO, pw)); Intent intent; try { intent = makeIntent(UserHandle.USER_CURRENT); @@ -931,9 +932,10 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println("Broadcasting: " + intent); pw.flush(); Bundle bundle = mBroadcastOptions == null ? null : mBroadcastOptions.toBundle(); - mInterface.broadcastIntentWithFeature(null, null, intent, null, receiver, 0, null, null, - requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE, bundle, true, - false, mUserId); + final int result = mInterface.broadcastIntentWithFeature(null, null, intent, null, + receiver, 0, null, null, requiredPermissions, null, null, + android.app.AppOpsManager.OP_NONE, bundle, true, false, mUserId); + Slogf.i(TAG, "Broadcasted %s: " + result, intent); if (!mAsync) { receiver.waitForFinish(); } @@ -3445,7 +3447,17 @@ final class ActivityManagerShellCommand extends ShellCommand { int runWaitForBroadcastIdle(PrintWriter pw) throws RemoteException { pw = new PrintWriter(new TeeWriter(LOG_WRITER_INFO, pw)); - mInternal.waitForBroadcastIdle(pw); + boolean flushBroadcastLoopers = false; + String opt; + while ((opt = getNextOption()) != null) { + if (opt.equals("--flush-broadcast-loopers")) { + flushBroadcastLoopers = true; + } else { + getErrPrintWriter().println("Error: Unknown option: " + opt); + return -1; + } + } + mInternal.waitForBroadcastIdle(pw, flushBroadcastLoopers); return 0; } diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 36da888dbc2a..dc6f8584006e 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -2287,34 +2287,6 @@ public final class BatteryStatsService extends IBatteryStats.Stub } } - /** - * Bluetooth on stat logging - */ - @Override - @EnforcePermission(BLUETOOTH_CONNECT) - public void noteBluetoothOn(int uid, int reason, String packageName) { - super.noteBluetoothOn_enforcePermission(); - - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED, - Binder.getCallingUid(), null, - FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED__STATE__ENABLED, - reason, packageName); - } - - /** - * Bluetooth off stat logging - */ - @Override - @EnforcePermission(BLUETOOTH_CONNECT) - public void noteBluetoothOff(int uid, int reason, String packageName) { - super.noteBluetoothOff_enforcePermission(); - - FrameworkStatsLog.write_non_chained(FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED, - Binder.getCallingUid(), null, - FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED__STATE__DISABLED, - reason, packageName); - } - @Override @EnforcePermission(UPDATE_DEVICE_STATS) public void noteBleScanStarted(final WorkSource ws, final boolean isUnoptimized) { diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index 87214decfe2e..030d596a1676 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -247,6 +247,26 @@ public class BroadcastConstants { private static final long DEFAULT_DELAY_URGENT_MILLIS = -120_000; /** + * For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts to + * foreground processes, typically a negative value to indicate they should be + * executed before most other pending broadcasts. + */ + public long DELAY_FOREGROUND_PROC_MILLIS = DEFAULT_DELAY_FOREGROUND_PROC_MILLIS; + private static final String KEY_DELAY_FOREGROUND_PROC_MILLIS = + "bcast_delay_foreground_proc_millis"; + private static final long DEFAULT_DELAY_FOREGROUND_PROC_MILLIS = -120_000; + + /** + * For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts to + * persistent processes, typically a negative value to indicate they should be + * executed before most other pending broadcasts. + */ + public long DELAY_PERSISTENT_PROC_MILLIS = DEFAULT_DELAY_FOREGROUND_PROC_MILLIS; + private static final String KEY_DELAY_PERSISTENT_PROC_MILLIS = + "bcast_delay_persistent_proc_millis"; + private static final long DEFAULT_DELAY_PERSISTENT_PROC_MILLIS = -120_000; + + /** * For {@link BroadcastQueueModernImpl}: Maximum number of complete * historical broadcasts to retain for debugging purposes. */ @@ -411,6 +431,10 @@ public class BroadcastConstants { DEFAULT_DELAY_CACHED_MILLIS); DELAY_URGENT_MILLIS = getDeviceConfigLong(KEY_DELAY_URGENT_MILLIS, DEFAULT_DELAY_URGENT_MILLIS); + DELAY_FOREGROUND_PROC_MILLIS = getDeviceConfigLong(KEY_DELAY_FOREGROUND_PROC_MILLIS, + DEFAULT_DELAY_FOREGROUND_PROC_MILLIS); + DELAY_PERSISTENT_PROC_MILLIS = getDeviceConfigLong(KEY_DELAY_PERSISTENT_PROC_MILLIS, + DEFAULT_DELAY_PERSISTENT_PROC_MILLIS); MAX_HISTORY_COMPLETE_SIZE = getDeviceConfigInt(KEY_MAX_HISTORY_COMPLETE_SIZE, DEFAULT_MAX_HISTORY_COMPLETE_SIZE); MAX_HISTORY_SUMMARY_SIZE = getDeviceConfigInt(KEY_MAX_HISTORY_SUMMARY_SIZE, @@ -463,6 +487,10 @@ public class BroadcastConstants { TimeUtils.formatDuration(DELAY_CACHED_MILLIS)).println(); pw.print(KEY_DELAY_URGENT_MILLIS, TimeUtils.formatDuration(DELAY_URGENT_MILLIS)).println(); + pw.print(KEY_DELAY_FOREGROUND_PROC_MILLIS, + TimeUtils.formatDuration(DELAY_FOREGROUND_PROC_MILLIS)).println(); + pw.print(KEY_DELAY_PERSISTENT_PROC_MILLIS, + TimeUtils.formatDuration(DELAY_PERSISTENT_PROC_MILLIS)).println(); pw.print(KEY_MAX_HISTORY_COMPLETE_SIZE, MAX_HISTORY_COMPLETE_SIZE).println(); pw.print(KEY_MAX_HISTORY_SUMMARY_SIZE, MAX_HISTORY_SUMMARY_SIZE).println(); pw.print(KEY_MAX_CONSECUTIVE_URGENT_DISPATCHES, diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 2803b4b66615..3ac2b2bd9fd1 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -1098,8 +1098,11 @@ class BroadcastProcessQueue { mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS; mRunnableAtReason = REASON_INSTRUMENTED; } else if (mUidForeground) { - mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS; + mRunnableAt = runnableAt + constants.DELAY_FOREGROUND_PROC_MILLIS; mRunnableAtReason = REASON_FOREGROUND; + } else if (mProcessPersistent) { + mRunnableAt = runnableAt + constants.DELAY_PERSISTENT_PROC_MILLIS; + mRunnableAtReason = REASON_PERSISTENT; } else if (mCountOrdered > 0) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_ORDERED; @@ -1112,9 +1115,6 @@ class BroadcastProcessQueue { } else if (mCountManifest > 0) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_MANIFEST; - } else if (mProcessPersistent) { - mRunnableAt = runnableAt; - mRunnableAtReason = REASON_PERSISTENT; } else if (mUidCached) { if (r.deferUntilActive) { // All enqueued broadcasts are deferrable, defer diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index d9b315794ea3..d6e692cfdc00 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -62,6 +62,7 @@ import android.os.Bundle; import android.os.BundleMerger; import android.os.Handler; import android.os.Message; +import android.os.PowerExemptionManager; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; @@ -878,12 +879,20 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mLocalHandler.sendMessageDelayed( Message.obtain(mLocalHandler, MSG_BG_ACTIVITY_START_TIMEOUT, args), timeout); } - if (r.options != null && r.options.getTemporaryAppAllowlistDuration() > 0) { - mService.tempAllowlistUidLocked(queue.uid, - r.options.getTemporaryAppAllowlistDuration(), - r.options.getTemporaryAppAllowlistReasonCode(), r.toShortString(), - r.options.getTemporaryAppAllowlistType(), r.callingUid); + if (r.options.getTemporaryAppAllowlistType() + == PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_APP_FREEZING_DELAYED) { + // Only delay freezer, don't add to any temp allowlist + // TODO: Add a unit test + mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(app, + CachedAppOptimizer.UNFREEZE_REASON_START_RECEIVER, + r.options.getTemporaryAppAllowlistDuration()); + } else { + mService.tempAllowlistUidLocked(queue.uid, + r.options.getTemporaryAppAllowlistDuration(), + r.options.getTemporaryAppAllowlistReasonCode(), r.toShortString(), + r.options.getTemporaryAppAllowlistType(), r.callingUid); + } } if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app); diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index a92744086f70..a402db945321 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -79,6 +79,9 @@ final class BroadcastRecord extends Binder { final @Nullable String callerFeatureId; // which feature in the package sent this final int callingPid; // the pid of who sent this final int callingUid; // the uid of who sent this + + final int originalStickyCallingUid; + // if this is a sticky broadcast, the Uid of the original sender final boolean callerInstantApp; // caller is an Instant App? final boolean callerInstrumented; // caller is being instrumented? final boolean ordered; // serialize the send to receivers? @@ -330,7 +333,8 @@ final class BroadcastRecord extends Binder { pw.print(prefix); pw.print("resultAbort="); pw.print(resultAbort); pw.print(" ordered="); pw.print(ordered); pw.print(" sticky="); pw.print(sticky); - pw.print(" initialSticky="); pw.println(initialSticky); + pw.print(" initialSticky="); pw.print(initialSticky); + pw.print(" originalStickyCallingUid="); pw.println(originalStickyCallingUid); } if (nextReceiver != 0) { pw.print(prefix); pw.print("nextReceiver="); pw.println(nextReceiver); @@ -399,6 +403,27 @@ final class BroadcastRecord extends Binder { } } + BroadcastRecord(BroadcastQueue queue, + Intent intent, ProcessRecord callerApp, String callerPackage, + @Nullable String callerFeatureId, int callingPid, int callingUid, + boolean callerInstantApp, String resolvedType, + String[] requiredPermissions, String[] excludedPermissions, + String[] excludedPackages, int appOp, + BroadcastOptions options, List receivers, + ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, + String resultData, Bundle resultExtras, boolean serialized, boolean sticky, + boolean initialSticky, int userId, + @NonNull BackgroundStartPrivileges backgroundStartPrivileges, + boolean timeoutExempt, + @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) { + this(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid, + callingUid, callerInstantApp, resolvedType, requiredPermissions, + excludedPermissions, excludedPackages, appOp, options, receivers, resultToApp, + resultTo, resultCode, resultData, resultExtras, serialized, sticky, + initialSticky, userId, -1, backgroundStartPrivileges, timeoutExempt, + filterExtrasForReceiver); + } + BroadcastRecord(BroadcastQueue _queue, Intent _intent, ProcessRecord _callerApp, String _callerPackage, @Nullable String _callerFeatureId, int _callingPid, int _callingUid, @@ -408,7 +433,7 @@ final class BroadcastRecord extends Binder { BroadcastOptions _options, List _receivers, ProcessRecord _resultToApp, IIntentReceiver _resultTo, int _resultCode, String _resultData, Bundle _resultExtras, boolean _serialized, boolean _sticky, - boolean _initialSticky, int _userId, + boolean _initialSticky, int _userId, int originalStickyCallingUid, @NonNull BackgroundStartPrivileges backgroundStartPrivileges, boolean timeoutExempt, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) { @@ -460,6 +485,7 @@ final class BroadcastRecord extends Binder { interactive = options != null && options.isInteractive(); shareIdentity = options != null && options.isShareIdentityEnabled(); this.filterExtrasForReceiver = filterExtrasForReceiver; + this.originalStickyCallingUid = originalStickyCallingUid; } /** @@ -524,6 +550,7 @@ final class BroadcastRecord extends Binder { shareIdentity = from.shareIdentity; urgent = from.urgent; filterExtrasForReceiver = from.filterExtrasForReceiver; + originalStickyCallingUid = from.originalStickyCallingUid; } /** diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 6718319c7ac6..5f918cfa3478 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -563,14 +563,15 @@ public class BroadcastSkipPolicy { // Ensure that broadcasts are only sent to other apps if they are explicitly marked as // exported, or are System level broadcasts + final int originalCallingUid = r.sticky ? r.originalStickyCallingUid : r.callingUid; if (!filter.exported && checkComponentPermission(null, r.callingPid, - r.callingUid, filter.receiverList.uid, filter.exported) + originalCallingUid, filter.receiverList.uid, filter.exported) != PackageManager.PERMISSION_GRANTED) { return "Exported Denial: sending " + r.intent.toString() + ", action: " + r.intent.getAction() + " from " + r.callerPackage - + " (uid=" + r.callingUid + ")" + + " (uid=" + originalCallingUid + ")" + " due to receiver " + filter.receiverList.app + " (uid " + filter.receiverList.uid + ")" + " not specifying RECEIVER_EXPORTED"; diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 3e82d557c01a..7773190d22b6 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -47,6 +47,7 @@ import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FREEZER; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import android.annotation.IntDef; +import android.annotation.UptimeMillisLong; import android.app.ActivityManager; import android.app.ActivityManagerInternal.OomAdjReason; import android.app.ActivityThread; @@ -1278,14 +1279,35 @@ public final class CachedAppOptimizer { return true; } + /** + * Returns the earliest time (relative) from now that the app can be frozen. + * @param app The app to update + * @param delayMillis How much to delay freezing by + */ + @GuardedBy("mProcLock") + private long updateEarliestFreezableTime(ProcessRecord app, long delayMillis) { + final long now = SystemClock.uptimeMillis(); + app.mOptRecord.setEarliestFreezableTime( + Math.max(app.mOptRecord.getEarliestFreezableTime(), now + delayMillis)); + return app.mOptRecord.getEarliestFreezableTime() - now; + } + // This will ensure app will be out of the freezer for at least mFreezerDebounceTimeout. @GuardedBy("mAm") void unfreezeTemporarily(ProcessRecord app, @UnfreezeReason int reason) { + unfreezeTemporarily(app, reason, mFreezerDebounceTimeout); + } + + // This will ensure app will be out of the freezer for at least mFreezerDebounceTimeout. + @GuardedBy("mAm") + void unfreezeTemporarily(ProcessRecord app, @UnfreezeReason int reason, long delayMillis) { if (mUseFreezer) { synchronized (mProcLock) { + // Move the earliest freezable time further, if necessary + final long delay = updateEarliestFreezableTime(app, delayMillis); if (app.mOptRecord.isFrozen() || app.mOptRecord.isPendingFreeze()) { unfreezeAppLSP(app, reason); - freezeAppAsyncLSP(app); + freezeAppAsyncLSP(app, delay); } } } @@ -1293,11 +1315,17 @@ public final class CachedAppOptimizer { @GuardedBy({"mAm", "mProcLock"}) void freezeAppAsyncLSP(ProcessRecord app) { - freezeAppAsyncInternalLSP(app, mFreezerDebounceTimeout, false); + freezeAppAsyncLSP(app, updateEarliestFreezableTime(app, mFreezerDebounceTimeout)); + } + + @GuardedBy({"mAm", "mProcLock"}) + private void freezeAppAsyncLSP(ProcessRecord app, @UptimeMillisLong long delayMillis) { + freezeAppAsyncInternalLSP(app, delayMillis, false); } @GuardedBy({"mAm", "mProcLock"}) - void freezeAppAsyncInternalLSP(ProcessRecord app, long delayMillis, boolean force) { + void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis, + boolean force) { final ProcessCachedOptimizerRecord opt = app.mOptRecord; if (opt.isPendingFreeze()) { // Skip redundant DO_FREEZE message diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java index 79089074dd1c..daa4ba4f1e16 100644 --- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java +++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java @@ -105,6 +105,11 @@ public class ForegroundServiceTypeLoggerModule { // to another ordered map, keyed by the component name // to facilitate removing the record from the structure final SparseArray<ArrayMap<ComponentName, ServiceRecord>> mRunningFgs = new SparseArray<>(); + + // A map of API types to last FGS stop call timestamps + // We use this to get the duration an API was active after + // the stop call. + final SparseArray<Long> mLastFgsTimeStamp = new SparseArray<>(); } // SparseArray that tracks all UIDs that have made various @@ -167,17 +172,13 @@ public class ForegroundServiceTypeLoggerModule { } if (!apiTypesFound.isEmpty()) { // log a state change - int[] types = new int[apiTypesFound.size()]; - long[] timestamps = new long[apiTypesFound.size()]; for (int i = 0, size = apiTypesFound.size(); i < size; i++) { - types[i] = apiTypesFound.get(i); - timestamps[i] = timestampsFound.get(i); + logFgsApiEvent(record, + FGS_STATE_CHANGED_API_CALL, + FGS_API_BEGIN_WITH_FGS, + apiTypesFound.get(i), + timestampsFound.get(i)); } - logFgsApiEvent(record, - FGS_STATE_CHANGED_API_CALL, - FGS_API_BEGIN_WITH_FGS, - types, - timestamps); } } @@ -192,7 +193,7 @@ public class ForegroundServiceTypeLoggerModule { final ArrayList<Integer> apiTypes = convertFgsTypeToApiTypes(record.foregroundServiceType); final UidState uidState = mUids.get(uid); if (uidState == null) { - Slog.wtfStack(TAG, "FGS stop call being logged with no start call for UID for UID " + Slog.w(TAG, "FGS stop call being logged with no start call for UID for UID " + uid + " in package " + record.packageName); return; @@ -202,7 +203,7 @@ public class ForegroundServiceTypeLoggerModule { for (int i = 0, size = apiTypes.size(); i < size; i++) { final int apiType = apiTypes.get(i); if (!uidState.mOpenWithFgsCount.contains(apiType)) { - Slog.wtfStack(TAG, "Logger should be tracking FGS types correctly for UID " + uid + Slog.w(TAG, "Logger should be tracking FGS types correctly for UID " + uid + " in package " + record.packageName); continue; } @@ -231,19 +232,17 @@ public class ForegroundServiceTypeLoggerModule { if (runningFgsOfType.size() == 0) { // there's no more FGS running for this type, just get rid of it uidState.mRunningFgs.remove(apiType); + // but we need to keep track of the timestamp in case an API stops + uidState.mLastFgsTimeStamp.put(apiType, record.mFgsExitTime); } } if (!apisFound.isEmpty()) { // time to log the call - int[] types = new int[apisFound.size()]; - long[] timestamps = new long[apisFound.size()]; for (int i = 0; i < apisFound.size(); i++) { - types[i] = apisFound.get(i); - timestamps[i] = timestampsFound.get(i); + logFgsApiEvent(record, + FGS_STATE_CHANGED_API_CALL, + FGS_API_END_WITH_FGS, apisFound.get(i), timestampsFound.get(i)); } - logFgsApiEvent(record, - FGS_STATE_CHANGED_API_CALL, - FGS_API_END_WITH_FGS, types, timestamps); } } @@ -303,8 +302,8 @@ public class ForegroundServiceTypeLoggerModule { final ArrayMap<ComponentName, ServiceRecord> fgsListMap = uidState.mRunningFgs.get(apiType); // now we get the relevant FGS to log with - final int[] apiTypes = {apiType}; - final long[] timestamps = {callStart.mTimeStart}; + final int apiTypes = apiType; + final long timestamps = callStart.mTimeStart; if (uidState.mOpenWithFgsCount.valueAt(openWithFgsIndex) == 1) { for (ServiceRecord record : fgsListMap.values()) { logFgsApiEvent(record, @@ -347,13 +346,13 @@ public class ForegroundServiceTypeLoggerModule { // we just log that an event happened w/ no // FGS associated. This is to avoid dangling // events - final long[] timestamp = {System.currentTimeMillis()}; - final int[] apiTypes = {apiType}; + final long timestamp = System.currentTimeMillis(); + final int apiTypes = apiType; logFgsApiEventWithNoFgs(uid, FGS_API_END_WITHOUT_FGS, apiTypes, timestamp); // we should now remove the count, so as to signal that // there was never an FGS called that can be associated uidState.mOpenWithFgsCount.remove(apiType); - return timestamp[0]; + return timestamp; } } // we know now that this call is not coming from an @@ -392,8 +391,8 @@ public class ForegroundServiceTypeLoggerModule { return; } final ArrayMap<ComponentName, ServiceRecord> fgsRecords = uidState.mRunningFgs.get(apiType); - final int[] apiTypes = {apiType}; - final long[] timestamp = {System.currentTimeMillis()}; + final int apiTypes = apiType; + final long timestamp = System.currentTimeMillis(); for (ServiceRecord record : fgsRecords.values()) { logFgsApiEvent(record, FGS_STATE_CHANGED_API_CALL, @@ -449,7 +448,9 @@ public class ForegroundServiceTypeLoggerModule { @VisibleForTesting public void logFgsApiEvent(ServiceRecord r, int fgsState, @FgsApiState int apiState, - @ForegroundServiceApiType int[] apiType, long[] timestamp) { + @ForegroundServiceApiType int apiType, long timestamp) { + final long apiDurationBeforeFgsStart = r.mFgsEnterTime - timestamp; + final long apiDurationAfterFgsEnd = timestamp - r.mFgsExitTime; FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED, r.appInfo.uid, r.shortInstanceName, @@ -479,7 +480,9 @@ public class ForegroundServiceTypeLoggerModule { ActivityManager.PROCESS_STATE_UNKNOWN, ActivityManager.PROCESS_CAPABILITY_NONE, ActivityManager.PROCESS_STATE_UNKNOWN, - ActivityManager.PROCESS_CAPABILITY_NONE); + ActivityManager.PROCESS_CAPABILITY_NONE, + apiDurationBeforeFgsStart, + apiDurationAfterFgsEnd); } /** @@ -489,7 +492,14 @@ public class ForegroundServiceTypeLoggerModule { @VisibleForTesting public void logFgsApiEventWithNoFgs(int uid, @FgsApiState int apiState, - @ForegroundServiceApiType int[] apiType, long[] timestamp) { + @ForegroundServiceApiType int apiType, long timestamp) { + long apiDurationAfterFgsEnd = 0; + UidState uidState = mUids.get(uid); + if (uidState != null) { + if (uidState.mLastFgsTimeStamp.contains(apiType)) { + apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType); + } + } FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED, uid, null, @@ -517,7 +527,9 @@ public class ForegroundServiceTypeLoggerModule { ActivityManager.PROCESS_STATE_UNKNOWN, ActivityManager.PROCESS_CAPABILITY_NONE, ActivityManager.PROCESS_STATE_UNKNOWN, - ActivityManager.PROCESS_CAPABILITY_NONE); + ActivityManager.PROCESS_CAPABILITY_NONE, + 0, + apiDurationAfterFgsEnd); } /** diff --git a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java index e8c8f6dd5462..7841b699ec98 100644 --- a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java +++ b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java @@ -16,6 +16,7 @@ package com.android.server.am; +import android.annotation.UptimeMillisLong; import android.app.ActivityManagerInternal.OomAdjReason; import com.android.internal.annotations.GuardedBy; @@ -119,6 +120,12 @@ final class ProcessCachedOptimizerRecord { @GuardedBy("mProcLock") private boolean mPendingFreeze; + /** + * This is the soonest the process can be allowed to freeze, in uptime millis + */ + @GuardedBy("mProcLock") + private @UptimeMillisLong long mEarliestFreezableTimeMillis; + @GuardedBy("mProcLock") long getLastCompactTime() { return mLastCompactTime; @@ -264,6 +271,16 @@ final class ProcessCachedOptimizerRecord { } @GuardedBy("mProcLock") + @UptimeMillisLong long getEarliestFreezableTime() { + return mEarliestFreezableTimeMillis; + } + + @GuardedBy("mProcLock") + void setEarliestFreezableTime(@UptimeMillisLong long earliestFreezableTimeMillis) { + mEarliestFreezableTimeMillis = earliestFreezableTimeMillis; + } + + @GuardedBy("mProcLock") boolean isFreezeExempt() { return mFreezeExempt; } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index c393213c5e85..fbe7e704a653 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -107,6 +107,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.os.storage.StorageManagerInternal; +import android.provider.DeviceConfig; import android.system.Os; import android.system.OsConstants; import android.text.TextUtils; @@ -181,6 +182,8 @@ public final class ProcessList { static final String ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY = "persist.sys.vold_app_data_isolation_enabled"; + private static final String APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = ":isSdkSandboxNext"; + // OOM adjustments for processes in various states: // Uninitialized value for any major or minor adj fields @@ -538,6 +541,78 @@ public final class ProcessList { ActivityManagerGlobalLock mProcLock; + private static final String PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = + "apply_sdk_sandbox_next_restrictions"; + private static final boolean DEFAULT_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = false; + + @GuardedBy("mService") + private ProcessListSettingsListener mProcessListSettingsListener; + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + ProcessListSettingsListener getProcessListSettingsListener() { + synchronized (mService) { + if (mProcessListSettingsListener == null) { + mProcessListSettingsListener = new ProcessListSettingsListener(mService.mContext); + mProcessListSettingsListener.registerObserver(); + } + return mProcessListSettingsListener; + } + } + + static class ProcessListSettingsListener implements DeviceConfig.OnPropertiesChangedListener { + + private final Context mContext; + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private boolean mSdkSandboxApplyRestrictionsNext = + DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ADSERVICES, + PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, + DEFAULT_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS); + + ProcessListSettingsListener(Context context) { + mContext = context; + } + + private void registerObserver() { + DeviceConfig.addOnPropertiesChangedListener( + DeviceConfig.NAMESPACE_ADSERVICES, mContext.getMainExecutor(), this); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + void unregisterObserver() { + DeviceConfig.removeOnPropertiesChangedListener(this); + } + + boolean applySdkSandboxRestrictionsNext() { + synchronized (mLock) { + return mSdkSandboxApplyRestrictionsNext; + } + } + + @Override + public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) { + synchronized (mLock) { + for (String name : properties.getKeyset()) { + if (name == null) { + continue; + } + + switch (name) { + case PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS: + mSdkSandboxApplyRestrictionsNext = + properties.getBoolean( + PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, + DEFAULT_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS); + break; + default: + } + } + } + } + } + final class IsolatedUidRange { @VisibleForTesting public final int mFirstUid; @@ -1883,8 +1958,9 @@ public final class ProcessList { new IllegalStateException("SELinux tag not defined for " + app.info.packageName + " (uid " + app.uid + ")")); } - final String seInfo = app.info.seInfo - + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser); + + String seInfo = updateSeInfo(app); + // Start the process. It will either succeed and return a result containing // the PID of the new process, or else throw a RuntimeException. final String entryPoint = "android.app.ActivityThread"; @@ -1907,6 +1983,21 @@ public final class ProcessList { } } + @VisibleForTesting + @GuardedBy("mService") + String updateSeInfo(ProcessRecord app) { + String extraInfo = ""; + // By the time the first the SDK sandbox process is started, device config service + // should be available. + if (app.isSdkSandbox + && getProcessListSettingsListener().applySdkSandboxRestrictionsNext()) { + extraInfo = APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS; + } + + return app.info.seInfo + + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser) + extraInfo; + } + @GuardedBy("mService") boolean startProcessLocked(HostingRecord hostingRecord, String entryPoint, ProcessRecord app, diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index b22ece30c386..1f39d1b5d7f8 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -175,7 +175,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN // while-in-use permissions in FGS started from background might be restricted. boolean mAllowWhileInUsePermissionInFgs; @PowerExemptionManager.ReasonCode - int mAllowWhileInUsePermissionInFgsReason; + int mAllowWhileInUsePermissionInFgsReason = REASON_DENIED; // Integer version of mAllowWhileInUsePermissionInFgs that we keep track to compare // the old and new logics. @@ -622,12 +622,13 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN pw.println(mBackgroundStartPrivilegesByStartMerged); } pw.print(prefix); pw.print("mAllowWhileInUsePermissionInFgsReason="); - pw.println(mAllowWhileInUsePermissionInFgsReason); + pw.println(PowerExemptionManager.reasonCodeToString(mAllowWhileInUsePermissionInFgsReason)); pw.print(prefix); pw.print("debugWhileInUseReasonInStartForeground="); - pw.println(mDebugWhileInUseReasonInStartForeground); + pw.println(PowerExemptionManager.reasonCodeToString( + mDebugWhileInUseReasonInStartForeground)); pw.print(prefix); pw.print("debugWhileInUseReasonInBindService="); - pw.println(mDebugWhileInUseReasonInBindService); + pw.println(PowerExemptionManager.reasonCodeToString(mDebugWhileInUseReasonInBindService)); pw.print(prefix); pw.print("allowUiJobScheduling="); pw.println(mAllowUiJobScheduling); pw.print(prefix); pw.print("recentCallingPackage="); diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java index f520f6a9ef49..4dfd9b076354 100644 --- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java +++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java @@ -19,6 +19,7 @@ package com.android.server.appop; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.OP_NONE; import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM; +import static android.app.AppOpsManager.OP_USE_FULL_SCREEN_INTENT; import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES; import static android.app.AppOpsManager.opRestrictsRead; import static android.app.AppOpsManager.opToDefaultMode; @@ -41,6 +42,7 @@ import android.os.Binder; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; +import android.permission.PermissionManager; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; @@ -107,7 +109,7 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface * {@link #upgradeLocked(int)} below. The first version was 1. */ @VisibleForTesting - static final int CURRENT_VERSION = 3; + static final int CURRENT_VERSION = 4; /** * This stores the version of appops.xml seen at boot. If this is smaller than @@ -1074,7 +1076,12 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface upgradeScheduleExactAlarmLocked(); // fall through case 2: - // for future upgrades + // split the appops.xml into appops.xml to store appop state and apppops_access.xml + // to store app-op access. + // fall through + case 3: + resetUseFullScreenIntentLocked(); + // fall through } scheduleFastWriteLocked(); } @@ -1145,6 +1152,38 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface } } + /** + * A cleanup step for U Beta 2 that reverts the OP_USE_FULL_SCREEN_INTENT's mode to MODE_DEFAULT + * if the permission flags for the USE_FULL_SCREEN_INTENT permission does not have USER_SET. + */ + @VisibleForTesting + @GuardedBy("mLock") + void resetUseFullScreenIntentLocked() { + final PermissionManagerServiceInternal pmsi = LocalServices.getService( + PermissionManagerServiceInternal.class); + final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class); + final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + final PermissionManager permissionManager = + mContext.getSystemService(PermissionManager.class); + + final String permissionName = AppOpsManager.opToPermission(OP_USE_FULL_SCREEN_INTENT); + final String[] packagesDeclaringPermission = + pmsi.getAppOpPermissionPackages(permissionName); + final int[] userIds = umi.getUserIds(); + + for (final String pkg : packagesDeclaringPermission) { + for (int userId : userIds) { + final int uid = pmi.getPackageUid(pkg, 0, userId); + final int flags = permissionManager.getPermissionFlags(pkg, permissionName, + UserHandle.of(userId)); + if ((flags & PackageManager.FLAG_PERMISSION_USER_SET) == 0) { + setUidMode(uid, OP_USE_FULL_SCREEN_INTENT, + AppOpsManager.opToDefaultMode(OP_USE_FULL_SCREEN_INTENT)); + } + } + } + } + @VisibleForTesting List<Integer> getUidsWithNonDefaultModes() { List<Integer> result = new ArrayList<>(); diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index bc4e8df2a4ad..f4c9d05ee0ef 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -865,43 +865,49 @@ import java.util.concurrent.atomic.AtomicBoolean; } } + // Lock protecting state variable related to Bluetooth audio state + private final Object mBluetoothAudioStateLock = new Object(); + // Current Bluetooth SCO audio active state indicated by BtHelper via setBluetoothScoOn(). - @GuardedBy("mDeviceStateLock") + @GuardedBy("mBluetoothAudioStateLock") private boolean mBluetoothScoOn; // value of BT_SCO parameter currently applied to audio HAL. - @GuardedBy("mDeviceStateLock") + @GuardedBy("mBluetoothAudioStateLock") private boolean mBluetoothScoOnApplied; // A2DP suspend state requested by AudioManager.setA2dpSuspended() API. - @GuardedBy("mDeviceStateLock") + @GuardedBy("mBluetoothAudioStateLock") private boolean mBluetoothA2dpSuspendedExt; // A2DP suspend state requested by AudioDeviceInventory. - @GuardedBy("mDeviceStateLock") + @GuardedBy("mBluetoothAudioStateLock") private boolean mBluetoothA2dpSuspendedInt; // value of BT_A2dpSuspendedSCO parameter currently applied to audio HAL. - @GuardedBy("mDeviceStateLock") + + @GuardedBy("mBluetoothAudioStateLock") private boolean mBluetoothA2dpSuspendedApplied; // LE Audio suspend state requested by AudioManager.setLeAudioSuspended() API. - @GuardedBy("mDeviceStateLock") + @GuardedBy("mBluetoothAudioStateLock") private boolean mBluetoothLeSuspendedExt; // LE Audio suspend state requested by AudioDeviceInventory. - @GuardedBy("mDeviceStateLock") + @GuardedBy("mBluetoothAudioStateLock") private boolean mBluetoothLeSuspendedInt; // value of LeAudioSuspended parameter currently applied to audio HAL. - @GuardedBy("mDeviceStateLock") + @GuardedBy("mBluetoothAudioStateLock") private boolean mBluetoothLeSuspendedApplied; private void initAudioHalBluetoothState() { - mBluetoothScoOnApplied = false; - AudioSystem.setParameters("BT_SCO=off"); - mBluetoothA2dpSuspendedApplied = false; - AudioSystem.setParameters("A2dpSuspended=false"); - mBluetoothLeSuspendedApplied = false; - AudioSystem.setParameters("LeAudioSuspended=false"); + synchronized (mBluetoothAudioStateLock) { + mBluetoothScoOnApplied = false; + AudioSystem.setParameters("BT_SCO=off"); + mBluetoothA2dpSuspendedApplied = false; + AudioSystem.setParameters("A2dpSuspended=false"); + mBluetoothLeSuspendedApplied = false; + AudioSystem.setParameters("LeAudioSuspended=false"); + } } - @GuardedBy("mDeviceStateLock") + @GuardedBy("mBluetoothAudioStateLock") private void updateAudioHalBluetoothState() { if (mBluetoothScoOn != mBluetoothScoOnApplied) { if (AudioService.DEBUG_COMM_RTE) { @@ -964,25 +970,21 @@ import java.util.concurrent.atomic.AtomicBoolean; if (AudioService.DEBUG_COMM_RTE) { Log.v(TAG, "setBluetoothScoOn: " + on + " " + eventSource); } - synchronized (mDeviceStateLock) { + synchronized (mBluetoothAudioStateLock) { mBluetoothScoOn = on; updateAudioHalBluetoothState(); postUpdateCommunicationRouteClient(eventSource); } } - /*package*/ void postSetA2dpSuspended(boolean enable, String eventSource) { - sendILMsgNoDelay(MSG_IL_SET_A2DP_SUSPENDED, SENDMSG_QUEUE, (enable ? 1 : 0), eventSource); - } - /*package*/ void setA2dpSuspended(boolean enable, boolean internal, String eventSource) { - if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "setA2dpSuspended source: " + eventSource + ", enable: " - + enable + ", internal: " + internal - + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt - + ", mBluetoothA2dpSuspendedExt: " + mBluetoothA2dpSuspendedExt); - } - synchronized (mDeviceStateLock) { + synchronized (mBluetoothAudioStateLock) { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "setA2dpSuspended source: " + eventSource + ", enable: " + + enable + ", internal: " + internal + + ", mBluetoothA2dpSuspendedInt: " + mBluetoothA2dpSuspendedInt + + ", mBluetoothA2dpSuspendedExt: " + mBluetoothA2dpSuspendedExt); + } if (internal) { mBluetoothA2dpSuspendedInt = enable; } else { @@ -996,26 +998,21 @@ import java.util.concurrent.atomic.AtomicBoolean; if (AudioService.DEBUG_COMM_RTE) { Log.v(TAG, "clearA2dpSuspended"); } - synchronized (mDeviceStateLock) { + synchronized (mBluetoothAudioStateLock) { mBluetoothA2dpSuspendedInt = false; mBluetoothA2dpSuspendedExt = false; updateAudioHalBluetoothState(); } } - /*package*/ void postSetLeAudioSuspended(boolean enable, String eventSource) { - sendILMsgNoDelay( - MSG_IL_SET_LEAUDIO_SUSPENDED, SENDMSG_QUEUE, (enable ? 1 : 0), eventSource); - } - /*package*/ void setLeAudioSuspended(boolean enable, boolean internal, String eventSource) { - if (AudioService.DEBUG_COMM_RTE) { - Log.v(TAG, "setLeAudioSuspended source: " + eventSource + ", enable: " - + enable + ", internal: " + internal - + ", mBluetoothLeSuspendedInt: " + mBluetoothA2dpSuspendedInt - + ", mBluetoothLeSuspendedExt: " + mBluetoothA2dpSuspendedExt); - } - synchronized (mDeviceStateLock) { + synchronized (mBluetoothAudioStateLock) { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "setLeAudioSuspended source: " + eventSource + ", enable: " + + enable + ", internal: " + internal + + ", mBluetoothLeSuspendedInt: " + mBluetoothA2dpSuspendedInt + + ", mBluetoothLeSuspendedExt: " + mBluetoothA2dpSuspendedExt); + } if (internal) { mBluetoothLeSuspendedInt = enable; } else { @@ -1029,7 +1026,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (AudioService.DEBUG_COMM_RTE) { Log.v(TAG, "clearLeAudioSuspended"); } - synchronized (mDeviceStateLock) { + synchronized (mBluetoothAudioStateLock) { mBluetoothLeSuspendedInt = false; mBluetoothLeSuspendedExt = false; updateAudioHalBluetoothState(); @@ -1821,12 +1818,6 @@ import java.util.concurrent.atomic.AtomicBoolean; final int capturePreset = msg.arg1; mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset); } break; - case MSG_IL_SET_A2DP_SUSPENDED: { - setA2dpSuspended((msg.arg1 == 1), false /*internal*/, (String) msg.obj); - } break; - case MSG_IL_SET_LEAUDIO_SUSPENDED: { - setLeAudioSuspended((msg.arg1 == 1), false /*internal*/, (String) msg.obj); - } break; case MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED: { final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; BtHelper.onNotifyPreferredAudioProfileApplied(btDevice); @@ -1905,8 +1896,6 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47; private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48; private static final int MSG_IL_BTLEAUDIO_TIMEOUT = 49; - private static final int MSG_IL_SET_A2DP_SUSPENDED = 50; - private static final int MSG_IL_SET_LEAUDIO_SUSPENDED = 51; private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52; @@ -2150,7 +2139,11 @@ import java.util.concurrent.atomic.AtomicBoolean; */ @GuardedBy("mDeviceStateLock") @Nullable private AudioDeviceAttributes preferredCommunicationDevice() { - boolean btSCoOn = mBluetoothScoOn && mBtHelper.isBluetoothScoOn(); + boolean btSCoOn = mBtHelper.isBluetoothScoOn(); + synchronized (mBluetoothAudioStateLock) { + btSCoOn = btSCoOn && mBluetoothScoOn; + } + if (btSCoOn) { // Use the SCO device known to BtHelper so that it matches exactly // what has been communicated to audio policy manager. The device diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 1d8bef1c6732..a561612b99c6 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -1100,7 +1100,8 @@ public class AudioDeviceInventory { synchronized (rolesMap) { Pair<Integer, Integer> key = new Pair<>(useCase, role); if (!rolesMap.containsKey(key)) { - return AudioSystem.SUCCESS; + // trying to clear a role for a device that wasn't set + return AudioSystem.BAD_VALUE; } final int status = asi.deviceRoleAction(useCase, role, null); if (status == AudioSystem.SUCCESS) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index a72187399acd..355981a0d109 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -3373,8 +3373,13 @@ public class AudioService extends IAudioService.Stub return; } - sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_ADJUST_STREAM_VOL, streamType, - direction/*val1*/, flags/*val2*/, callingPackage)); + final VolumeEvent evt = new VolumeEvent(VolumeEvent.VOL_ADJUST_STREAM_VOL, streamType, + direction/*val1*/, flags/*val2*/, callingPackage); + sVolumeLogger.enqueue(evt); + // also logging mute/unmute calls to the dedicated logger + if (isMuteAdjust(direction)) { + sMuteLogger.enqueue(evt); + } adjustStreamVolume(streamType, direction, flags, callingPackage, callingPackage, Binder.getCallingUid(), Binder.getCallingPid(), attributionTag, callingHasAudioSettingsPermission(), AudioDeviceVolumeManager.ADJUST_MODE_NORMAL); @@ -3475,7 +3480,7 @@ public class AudioService extends IAudioService.Stub } // If either the client forces allowing ringer modes for this adjustment, - // or the stream type is one that is affected by ringer modes + // or stream is used for UI sonification if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) || (isUiSoundsStreamType(streamTypeAlias))) { int ringerMode = getRingerModeInternal(); @@ -3496,6 +3501,13 @@ public class AudioService extends IAudioService.Stub if ((result & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0) { flags |= AudioManager.FLAG_SHOW_VIBRATE_HINT; } + } else if (isStreamMutedByRingerOrZenMode(streamTypeAlias) && streamState.mIsMuted) { + // if the stream is currently muted streams by ringer/zen mode + // then it cannot be unmuted (without FLAG_ALLOW_RINGER_MODES) + if (direction == AudioManager.ADJUST_TOGGLE_MUTE + || direction == AudioManager.ADJUST_UNMUTE) { + adjustVolume = false; + } } // If the ringer mode or zen is muting the stream, do not change stream unless @@ -6419,7 +6431,7 @@ public class AudioService extends IAudioService.Stub final String eventSource = new StringBuilder("setA2dpSuspended(").append(enable) .append(") from u/pid:").append(Binder.getCallingUid()).append("/") .append(Binder.getCallingPid()).toString(); - mDeviceBroker.postSetA2dpSuspended(enable, eventSource); + mDeviceBroker.setA2dpSuspended(enable, false /*internal*/, eventSource); } /** @see AudioManager#setA2dpSuspended(boolean) */ @@ -6429,7 +6441,7 @@ public class AudioService extends IAudioService.Stub final String eventSource = new StringBuilder("setLeAudioSuspended(").append(enable) .append(") from u/pid:").append(Binder.getCallingUid()).append("/") .append(Binder.getCallingPid()).toString(); - mDeviceBroker.postSetLeAudioSuspended(enable, eventSource); + mDeviceBroker.setLeAudioSuspended(enable, false /*internal*/, eventSource); } /** @see AudioManager#isBluetoothScoOn() @@ -11041,6 +11053,11 @@ public class AudioService extends IAudioService.Stub dumpAccessibilityServiceUids(pw); dumpAssistantServicesUids(pw); + pw.print(" supportsBluetoothVariableLatency="); + pw.println(AudioSystem.supportsBluetoothVariableLatency()); + pw.print(" isBluetoothVariableLatencyEnabled="); + pw.println(AudioSystem.isBluetoothVariableLatencyEnabled()); + dumpAudioPolicies(pw); mDynPolicyLogger.dump(pw); mPlaybackMonitor.dump(pw); diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java index 6ad9390ef366..6ebb42e08ade 100644 --- a/services/core/java/com/android/server/audio/AudioServiceEvents.java +++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java @@ -609,7 +609,7 @@ public class AudioServiceEvents { (mStreamType <= AudioSystem.getNumStreamTypes() && mStreamType >= 0) ? AudioSystem.STREAM_NAMES[mStreamType] : ("stream " + mStreamType); - return new StringBuilder("Error trying to unmute ") + return new StringBuilder("Invalid call to unmute ") .append(streamName) .append(" despite muted streams 0x") .append(Integer.toHexString(mRingerZenMutedStreams)) diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index f8cb9e98c714..4538cad513d6 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -70,6 +70,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.server.SystemService; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import java.util.ArrayList; import java.util.Arrays; @@ -262,6 +263,11 @@ public class AuthService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { + VirtualDeviceManagerInternal vdm = getLocalService( + VirtualDeviceManagerInternal.class); + if (vdm != null) { + vdm.onAuthenticationPrompt(callingUid); + } return mBiometricService.authenticate( token, sessionId, userId, receiver, opPackageName, promptInfo); } finally { diff --git a/services/core/java/com/android/server/biometrics/BiometricSensor.java b/services/core/java/com/android/server/biometrics/BiometricSensor.java index bac44809883f..937e3f8f8668 100644 --- a/services/core/java/com/android/server/biometrics/BiometricSensor.java +++ b/services/core/java/com/android/server/biometrics/BiometricSensor.java @@ -22,20 +22,14 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.BiometricConstants; -import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricSensorReceiver; -import android.hardware.biometrics.SensorPropertiesInternal; import android.os.IBinder; import android.os.RemoteException; -import android.text.TextUtils; -import android.util.IndentingPrintWriter; import android.util.Slog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Collections; -import java.util.List; /** * Wraps IBiometricAuthenticator implementation and stores information about the authenticator, @@ -73,7 +67,6 @@ public abstract class BiometricSensor { public final int id; public final @Authenticators.Types int oemStrength; // strength as configured by the OEM public final int modality; - @NonNull public final List<ComponentInfoInternal> componentInfo; public final IBiometricAuthenticator impl; private @Authenticators.Types int mUpdatedStrength; // updated by BiometricStrengthController @@ -93,16 +86,15 @@ public abstract class BiometricSensor { */ abstract boolean confirmationSupported(); - BiometricSensor(@NonNull Context context, int modality, @NonNull SensorPropertiesInternal props, - IBiometricAuthenticator impl) { + BiometricSensor(@NonNull Context context, int id, int modality, + @Authenticators.Types int strength, IBiometricAuthenticator impl) { this.mContext = context; - this.id = props.sensorId; + this.id = id; this.modality = modality; - this.oemStrength = Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); - this.componentInfo = Collections.unmodifiableList(props.componentInfo); + this.oemStrength = strength; this.impl = impl; - mUpdatedStrength = oemStrength; + mUpdatedStrength = strength; goToStateUnknown(); } @@ -186,25 +178,8 @@ public abstract class BiometricSensor { return "ID(" + id + ")" + ", oemStrength: " + oemStrength + ", updatedStrength: " + mUpdatedStrength - + ", modality: " + modality + + ", modality " + modality + ", state: " + mSensorState + ", cookie: " + mCookie; } - - protected void dump(@NonNull IndentingPrintWriter pw) { - pw.println(TextUtils.formatSimple("ID: %d", id)); - pw.increaseIndent(); - pw.println(TextUtils.formatSimple("oemStrength: %d", oemStrength)); - pw.println(TextUtils.formatSimple("updatedStrength: %d", mUpdatedStrength)); - pw.println(TextUtils.formatSimple("modality: %d", modality)); - pw.println("componentInfo:"); - for (ComponentInfoInternal info : componentInfo) { - pw.increaseIndent(); - info.dump(pw); - pw.decreaseIndent(); - } - pw.println(TextUtils.formatSimple("state: %d", mSensorState)); - pw.println(TextUtils.formatSimple("cookie: %d", mCookie)); - pw.decreaseIndent(); - } } diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 0ab74b8580c1..448843477ecd 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -64,7 +64,6 @@ import android.provider.Settings; import android.security.KeyStore; import android.text.TextUtils; import android.util.ArraySet; -import android.util.IndentingPrintWriter; import android.util.Pair; import android.util.Slog; import android.util.proto.ProtoOutputStream; @@ -642,16 +641,13 @@ public class BiometricService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override - public synchronized void registerAuthenticator(int modality, - @NonNull SensorPropertiesInternal props, + public synchronized void registerAuthenticator(int id, int modality, + @Authenticators.Types int strength, @NonNull IBiometricAuthenticator authenticator) { super.registerAuthenticator_enforcePermission(); - @Authenticators.Types final int strength = - Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); - - Slog.d(TAG, "Registering ID: " + props.sensorId + Slog.d(TAG, "Registering ID: " + id + " Modality: " + modality + " Strength: " + strength); @@ -672,12 +668,12 @@ public class BiometricService extends SystemService { } for (BiometricSensor sensor : mSensors) { - if (sensor.id == props.sensorId) { + if (sensor.id == id) { throw new IllegalStateException("Cannot register duplicate authenticator"); } } - mSensors.add(new BiometricSensor(getContext(), modality, props, authenticator) { + mSensors.add(new BiometricSensor(getContext(), id, modality, strength, authenticator) { @Override boolean confirmationAlwaysRequired(int userId) { return mSettingObserver.getConfirmationAlwaysRequired(modality, userId); @@ -1376,17 +1372,13 @@ public class BiometricService extends SystemService { return null; } - private void dumpInternal(PrintWriter printWriter) { - IndentingPrintWriter pw = new IndentingPrintWriter(printWriter); - + private void dumpInternal(PrintWriter pw) { pw.println("Legacy Settings: " + mSettingObserver.mUseLegacyFaceOnlySettings); pw.println(); pw.println("Sensors:"); for (BiometricSensor sensor : mSensors) { - pw.increaseIndent(); - sensor.dump(pw); - pw.decreaseIndent(); + pw.println(" " + sensor); } pw.println(); pw.println("CurrentSession: " + mAuthSession); diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index fb978b2ba4b9..b474cad962c3 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -555,7 +555,7 @@ public class BiometricScheduler { for (BiometricSchedulerOperation pendingOperation : mPendingOperations) { Slog.d(getTag(), "[Watchdog cancelling pending] " + pendingOperation.getClientMonitor()); - pendingOperation.markCanceling(); + pendingOperation.markCancelingForWatchdog(); } Slog.d(getTag(), "[Watchdog cancelling current] " + mCurrentOperation.getClientMonitor()); diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java index 4825f1dea66f..57feedc0e68e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java @@ -267,13 +267,17 @@ public class BiometricSchedulerOperation { /** Flags this operation as canceled, if possible, but does not cancel it until started. */ public boolean markCanceling() { - if (mState == STATE_WAITING_IN_QUEUE) { + if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) { mState = STATE_WAITING_IN_QUEUE_CANCELING; return true; } return false; } + @VisibleForTesting void markCancelingForWatchdog() { + mState = STATE_WAITING_IN_QUEUE_CANCELING; + } + /** * Cancel the operation now. * diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java index d43045b4450f..0f0a81d24473 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java @@ -20,6 +20,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.IBiometricService; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; @@ -27,6 +28,7 @@ import android.hardware.face.IFaceService; import android.os.RemoteException; import android.util.Slog; +import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.BiometricServiceRegistry; import java.util.List; @@ -51,8 +53,10 @@ public class FaceServiceRegistry extends BiometricServiceRegistry<ServiceProvide @Override protected void registerService(@NonNull IBiometricService service, @NonNull FaceSensorPropertiesInternal props) { + @BiometricManager.Authenticators.Types final int strength = + Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); try { - service.registerAuthenticator(TYPE_FACE, props, + service.registerAuthenticator(props.sensorId, TYPE_FACE, strength, new FaceAuthenticator(mService, props.sensorId)); } catch (RemoteException e) { Slog.e(TAG, "Remote exception when registering sensorId: " + props.sensorId); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index 6c26e2b0ce99..ece35c522ec7 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -91,6 +91,7 @@ import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider; import com.android.server.biometrics.sensors.fingerprint.hidl.Fingerprint21; import com.android.server.biometrics.sensors.fingerprint.hidl.Fingerprint21UdfpsMock; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.google.android.collect.Lists; @@ -329,6 +330,16 @@ public class FingerprintService extends SystemService { return -1; } } + final long identity2 = Binder.clearCallingIdentity(); + try { + VirtualDeviceManagerInternal vdm = getLocalService( + VirtualDeviceManagerInternal.class); + if (vdm != null) { + vdm.onAuthenticationPrompt(callingUid); + } + } finally { + Binder.restoreCallingIdentity(identity2); + } return provider.second.scheduleAuthenticate(token, operationId, 0 /* cookie */, new ClientMonitorCallbackConverter(receiver), options, restricted, statsClient, isKeyguard); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java index 6d210eac542b..33810b764f23 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java @@ -20,6 +20,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRIN import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.IBiometricService; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; @@ -27,6 +28,7 @@ import android.hardware.fingerprint.IFingerprintService; import android.os.RemoteException; import android.util.Slog; +import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.BiometricServiceRegistry; import java.util.List; @@ -51,8 +53,10 @@ public class FingerprintServiceRegistry extends BiometricServiceRegistry<Service @Override protected void registerService(@NonNull IBiometricService service, @NonNull FingerprintSensorPropertiesInternal props) { + @BiometricManager.Authenticators.Types final int strength = + Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); try { - service.registerAuthenticator(TYPE_FINGERPRINT, props, + service.registerAuthenticator(props.sensorId, TYPE_FINGERPRINT, strength, new FingerprintAuthenticator(mService, props.sensorId)); } catch (RemoteException e) { Slog.e(TAG, "Remote exception when registering sensorId: " + props.sensorId); diff --git a/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java b/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java index f27761fd644c..35ea36c5d56f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java +++ b/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java @@ -16,10 +16,12 @@ package com.android.server.biometrics.sensors.iris; +import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_IRIS; import android.annotation.NonNull; import android.content.Context; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.SensorPropertiesInternal; import android.hardware.iris.IIrisService; @@ -31,6 +33,7 @@ import android.util.Slog; import com.android.server.ServiceThread; import com.android.server.SystemService; +import com.android.server.biometrics.Utils; import java.util.List; @@ -72,12 +75,17 @@ public class IrisService extends SystemService { ServiceManager.getService(Context.BIOMETRIC_SERVICE)); for (SensorPropertiesInternal hidlSensor : hidlSensors) { + final int sensorId = hidlSensor.sensorId; + final @BiometricManager.Authenticators.Types int strength = + Utils.propertyStrengthToAuthenticatorStrength( + hidlSensor.sensorStrength); + final IrisAuthenticator authenticator = new IrisAuthenticator(mServiceWrapper, + sensorId); try { - biometricService.registerAuthenticator(TYPE_IRIS, hidlSensor, - new IrisAuthenticator(mServiceWrapper, hidlSensor.sensorId)); + biometricService.registerAuthenticator(sensorId, TYPE_IRIS, strength, + authenticator); } catch (RemoteException e) { - Slog.e(TAG, "Remote exception when registering sensorId: " - + hidlSensor.sensorId); + Slog.e(TAG, "Remote exception when registering sensorId: " + sensorId); } } }); diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index 0315352be133..0b04159194d1 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -38,6 +38,7 @@ import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.res.Configuration; import android.graphics.Rect; +import android.hardware.CameraExtensionSessionStats; import android.hardware.CameraSessionStats; import android.hardware.CameraStreamStats; import android.hardware.ICameraService; @@ -247,6 +248,7 @@ public class CameraServiceProxy extends SystemService public final int mSessionIndex; private long mDurationOrStartTimeMs; // Either start time, or duration once completed + public CameraExtensionSessionStats mExtSessionStats = null; CameraUsageEvent(String cameraId, int facing, String clientName, int apiLevel, boolean isNdk, int action, int latencyMs, int operatingMode, boolean deviceError, @@ -269,7 +271,7 @@ public class CameraServiceProxy extends SystemService public void markCompleted(int internalReconfigure, long requestCount, long resultErrorCount, boolean deviceError, List<CameraStreamStats> streamStats, String userTag, - int videoStabilizationMode) { + int videoStabilizationMode, CameraExtensionSessionStats extStats) { if (mCompleted) { return; } @@ -282,6 +284,7 @@ public class CameraServiceProxy extends SystemService mStreamStats = streamStats; mUserTag = userTag; mVideoStabilizationMode = videoStabilizationMode; + mExtSessionStats = extStats; if (CameraServiceProxy.DEBUG) { Slog.v(TAG, "A camera facing " + cameraFacingToString(mCameraFacing) + " was in use by " + mClientName + " for " + @@ -825,6 +828,36 @@ public class CameraServiceProxy extends SystemService Slog.w(TAG, "Unknown camera facing: " + e.mCameraFacing); } + int extensionType = FrameworkStatsLog.CAMERA_ACTION_EVENT__EXT_TYPE__EXTENSION_NONE; + boolean extensionIsAdvanced = false; + if (e.mExtSessionStats != null) { + switch (e.mExtSessionStats.type) { + case CameraExtensionSessionStats.Type.EXTENSION_AUTOMATIC: + extensionType = FrameworkStatsLog + .CAMERA_ACTION_EVENT__EXT_TYPE__EXTENSION_AUTOMATIC; + break; + case CameraExtensionSessionStats.Type.EXTENSION_FACE_RETOUCH: + extensionType = FrameworkStatsLog + .CAMERA_ACTION_EVENT__EXT_TYPE__EXTENSION_FACE_RETOUCH; + break; + case CameraExtensionSessionStats.Type.EXTENSION_BOKEH: + extensionType = + FrameworkStatsLog.CAMERA_ACTION_EVENT__EXT_TYPE__EXTENSION_BOKEH; + break; + case CameraExtensionSessionStats.Type.EXTENSION_HDR: + extensionType = + FrameworkStatsLog.CAMERA_ACTION_EVENT__EXT_TYPE__EXTENSION_HDR; + break; + case CameraExtensionSessionStats.Type.EXTENSION_NIGHT: + extensionType = + FrameworkStatsLog.CAMERA_ACTION_EVENT__EXT_TYPE__EXTENSION_NIGHT; + break; + default: + Slog.w(TAG, "Unknown extension type: " + e.mExtSessionStats.type); + } + extensionIsAdvanced = e.mExtSessionStats.isAdvanced; + } + int streamCount = 0; if (e.mStreamStats != null) { streamCount = e.mStreamStats.size(); @@ -847,7 +880,9 @@ public class CameraServiceProxy extends SystemService + ", userTag is " + e.mUserTag + ", videoStabilizationMode " + e.mVideoStabilizationMode + ", logId " + e.mLogId - + ", sessionIndex " + e.mSessionIndex); + + ", sessionIndex " + e.mSessionIndex + + ", mExtSessionStats {type " + extensionType + + " isAdvanced " + extensionIsAdvanced + "}"); } // Convert from CameraStreamStats to CameraStreamProto CameraStreamProto[] streamProtos = new CameraStreamProto[MAX_STREAM_STATISTICS]; @@ -907,7 +942,8 @@ public class CameraServiceProxy extends SystemService MessageNano.toByteArray(streamProtos[2]), MessageNano.toByteArray(streamProtos[3]), MessageNano.toByteArray(streamProtos[4]), - e.mUserTag, e.mVideoStabilizationMode, e.mLogId, e.mSessionIndex); + e.mUserTag, e.mVideoStabilizationMode, e.mLogId, e.mSessionIndex, + extensionType, extensionIsAdvanced); } } @@ -1098,6 +1134,7 @@ public class CameraServiceProxy extends SystemService int videoStabilizationMode = cameraState.getVideoStabilizationMode(); long logId = cameraState.getLogId(); int sessionIdx = cameraState.getSessionIndex(); + CameraExtensionSessionStats extSessionStats = cameraState.getExtensionSessionStats(); synchronized(mLock) { // Update active camera list and notify NFC if necessary boolean wasEmpty = mActiveCameraUsage.isEmpty(); @@ -1152,7 +1189,8 @@ public class CameraServiceProxy extends SystemService Slog.w(TAG, "Camera " + cameraId + " was already marked as active"); oldEvent.markCompleted(/*internalReconfigure*/0, /*requestCount*/0, /*resultErrorCount*/0, /*deviceError*/false, streamStats, - /*userTag*/"", /*videoStabilizationMode*/-1); + /*userTag*/"", /*videoStabilizationMode*/-1, + new CameraExtensionSessionStats()); mCameraUsageHistory.add(oldEvent); } break; @@ -1163,7 +1201,7 @@ public class CameraServiceProxy extends SystemService doneEvent.markCompleted(internalReconfigureCount, requestCount, resultErrorCount, deviceError, streamStats, userTag, - videoStabilizationMode); + videoStabilizationMode, extSessionStats); mCameraUsageHistory.add(doneEvent); // Do not double count device error deviceError = false; diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java index a33533841e89..4d12574fb6b0 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -70,6 +70,11 @@ public abstract class VirtualDeviceManagerInternal { public abstract void onAppsOnVirtualDeviceChanged(); /** + * Notifies that an authentication prompt is about to be shown for an app with the given uid. + */ + public abstract void onAuthenticationPrompt(int uid); + + /** * Gets the owner uid for a deviceId. * * @param deviceId which device we're asking about diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index c678a92af13a..4208a12f91d4 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -3677,8 +3677,11 @@ public class Vpn { encapType = IkeSessionParams.ESP_ENCAP_TYPE_NONE; break; default: - ipVersion = IkeSessionParams.ESP_IP_VERSION_AUTO; - encapType = IkeSessionParams.ESP_ENCAP_TYPE_AUTO; + // By default, PREFERRED_IKE_PROTOCOL_IPV4_UDP is used for safety. This is + // because some carriers' networks do not support IPv6 very well, and using + // IPv4 can help to prevent problems. + ipVersion = IkeSessionParams.ESP_IP_VERSION_IPV4; + encapType = IkeSessionParams.ESP_ENCAP_TYPE_UDP; break; } return new CarrierConfigInfo(mccMnc, natKeepalive, encapType, ipVersion); diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 964569008acc..eb7fa1069b7b 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -17,6 +17,7 @@ package com.android.server.devicestate; import static android.Manifest.permission.CONTROL_DEVICE_STATE; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE; @@ -73,6 +74,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.WeakHashMap; @@ -162,7 +164,7 @@ public final class DeviceStateManagerService extends SystemService { @GuardedBy("mLock") private final SparseArray<ProcessRecord> mProcessRecords = new SparseArray<>(); - private Set<Integer> mDeviceStatesAvailableForAppRequests; + private Set<Integer> mDeviceStatesAvailableForAppRequests = new HashSet<>(); private Set<Integer> mFoldedDeviceStates; @@ -203,7 +205,7 @@ public final class DeviceStateManagerService extends SystemService { @VisibleForTesting DeviceStateManagerService(@NonNull Context context, @NonNull DeviceStatePolicy policy, - @NonNull SystemPropertySetter systemPropertySetter) { + @NonNull SystemPropertySetter systemPropertySetter) { super(context); mSystemPropertySetter = systemPropertySetter; // We use the DisplayThread because this service indirectly drives @@ -340,16 +342,20 @@ public final class DeviceStateManagerService extends SystemService { return supportedStates; } + /** + * Returns the current {@link DeviceStateInfo} of the device. If there has been no base state + * or committed state provided, {@link DeviceStateManager#INVALID_DEVICE_STATE} will be returned + * respectively. The supported states will always be included. + * + */ + @GuardedBy("mLock") @NonNull private DeviceStateInfo getDeviceStateInfoLocked() { - if (!mBaseState.isPresent() || !mCommittedState.isPresent()) { - throw new IllegalStateException("Trying to get the current DeviceStateInfo before the" - + " initial state has been committed."); - } - final int[] supportedStates = getSupportedStateIdentifiersLocked(); - final int baseState = mBaseState.get().getIdentifier(); - final int currentState = mCommittedState.get().getIdentifier(); + final int baseState = + mBaseState.isPresent() ? mBaseState.get().getIdentifier() : INVALID_DEVICE_STATE; + final int currentState = mCommittedState.isPresent() ? mCommittedState.get().getIdentifier() + : INVALID_DEVICE_STATE; return new DeviceStateInfo(supportedStates, baseState, currentState); } @@ -389,12 +395,7 @@ public final class DeviceStateManagerService extends SystemService { setRearDisplayStateLocked(); - if (!mPendingState.isPresent()) { - // If the change in the supported states didn't result in a change of the pending - // state commitPendingState() will never be called and the callbacks will never be - // notified of the change. - notifyDeviceStateInfoChangedAsync(); - } + notifyDeviceStateInfoChangedAsync(); mHandler.post(this::notifyPolicyIfNeeded); } @@ -458,12 +459,7 @@ public final class DeviceStateManagerService extends SystemService { mOverrideRequestController.handleBaseStateChanged(identifier); updatePendingStateLocked(); - if (!mPendingState.isPresent()) { - // If the change in base state didn't result in a change of the pending state - // commitPendingState() will never be called and the callbacks will never be - // notified of the change. - notifyDeviceStateInfoChangedAsync(); - } + notifyDeviceStateInfoChangedAsync(); mHandler.post(this::notifyPolicyIfNeeded); } @@ -591,6 +587,18 @@ public final class DeviceStateManagerService extends SystemService { private void notifyDeviceStateInfoChangedAsync() { synchronized (mLock) { + if (mPendingState.isPresent()) { + Slog.i(TAG, + "Cannot notify device state info change when pending state is present."); + return; + } + + if (!mBaseState.isPresent() || !mCommittedState.isPresent()) { + Slog.e(TAG, "Cannot notify device state info change before the initial state has" + + " been committed."); + return; + } + if (mProcessRecords.size() == 0) { return; } @@ -656,7 +664,7 @@ public final class DeviceStateManagerService extends SystemService { } } else { throw new IllegalArgumentException( - "Unknown OverrideRest type: " + request.getRequestType()); + "Unknown OverrideRest type: " + request.getRequestType()); } boolean updatedPendingState = updatePendingStateLocked(); @@ -711,6 +719,9 @@ public final class DeviceStateManagerService extends SystemService { } mProcessRecords.put(pid, record); + // Callback clients should not be notified of invalid device states, so calls to + // #getDeviceStateInfoLocked should be gated on checks if a committed state is present + // before getting the device state info. DeviceStateInfo currentInfo = mCommittedState.isPresent() ? getDeviceStateInfoLocked() : null; if (currentInfo != null) { @@ -879,8 +890,16 @@ public final class DeviceStateManagerService extends SystemService { * @param callingPid Process ID that is requesting this state change * @param state state that is being requested. */ - private void assertCanRequestDeviceState(int callingPid, int state) { - if (!isTopApp(callingPid) || !isStateAvailableForAppRequests(state)) { + private void assertCanRequestDeviceState(int callingPid, int callingUid, int state) { + final boolean isTopApp = isTopApp(callingPid); + final boolean isForegroundApp = isForegroundApp(callingPid, callingUid); + final boolean isStateAvailableForAppRequests = isStateAvailableForAppRequests(state); + + final boolean canRequestState = isTopApp + && isForegroundApp + && isStateAvailableForAppRequests; + + if (!canRequestState) { getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE, "Permission required to request device state, " + "or the call must come from the top app " @@ -893,15 +912,43 @@ public final class DeviceStateManagerService extends SystemService { * not the top app, then check if this process holds the CONTROL_DEVICE_STATE permission. * * @param callingPid Process ID that is requesting this state change + * @param callingUid UID that is requesting this state change */ - private void assertCanControlDeviceState(int callingPid) { - if (!isTopApp(callingPid)) { + private void assertCanControlDeviceState(int callingPid, int callingUid) { + final boolean isTopApp = isTopApp(callingPid); + final boolean isForegroundApp = isForegroundApp(callingPid, callingUid); + + final boolean canControlState = isTopApp && isForegroundApp; + + if (!canControlState) { getContext().enforceCallingOrSelfPermission(CONTROL_DEVICE_STATE, "Permission required to request device state, " + "or the call must come from the top app."); } } + /** + * Checks if the caller is in the foreground. Note that callers may be the top app as returned + * from {@link #isTopApp(int)}, but not be in the foreground. For example, keyguard may be on + * top of the top app. + */ + private boolean isForegroundApp(int callingPid, int callingUid) { + try { + final List<ActivityManager.RunningAppProcessInfo> procs = + ActivityManager.getService().getRunningAppProcesses(); + for (int i = 0; i < procs.size(); i++) { + ActivityManager.RunningAppProcessInfo proc = procs.get(i); + if (proc.pid == callingPid && proc.uid == callingUid + && proc.importance <= IMPORTANCE_FOREGROUND) { + return true; + } + } + } catch (RemoteException e) { + Slog.w(TAG, "am.getRunningAppProcesses() failed", e); + } + return false; + } + private boolean isTopApp(int callingPid) { final WindowProcessController topApp = mActivityTaskManagerInternal.getTopApp(); return topApp != null && topApp.getPid() == callingPid; @@ -918,7 +965,6 @@ public final class DeviceStateManagerService extends SystemService { */ @GuardedBy("mLock") private void readStatesAvailableForRequestFromApps() { - mDeviceStatesAvailableForAppRequests = new HashSet<>(); String[] availableAppStatesConfigIdentifiers = getContext().getResources() .getStringArray(R.array.config_deviceStatesAvailableForAppRequests); for (int i = 0; i < availableAppStatesConfigIdentifiers.length; i++) { @@ -1089,6 +1135,7 @@ public final class DeviceStateManagerService extends SystemService { /** Implementation of {@link IDeviceStateManager} published as a binder service. */ private final class BinderService extends IDeviceStateManager.Stub { + @Override // Binder call public DeviceStateInfo getDeviceStateInfo() { synchronized (mLock) { @@ -1118,7 +1165,7 @@ public final class DeviceStateManagerService extends SystemService { // Allow top processes to request a device state change // If the calling process ID is not the top app, then we check if this process // holds a permission to CONTROL_DEVICE_STATE - assertCanRequestDeviceState(callingPid, state); + assertCanRequestDeviceState(callingPid, callingUid, state); if (token == null) { throw new IllegalArgumentException("Request token must not be null."); @@ -1139,10 +1186,11 @@ public final class DeviceStateManagerService extends SystemService { @Override // Binder call public void cancelStateRequest() { final int callingPid = Binder.getCallingPid(); + final int callingUid = Binder.getCallingUid(); // Allow top processes to cancel a device state change // If the calling process ID is not the top app, then we check if this process // holds a permission to CONTROL_DEVICE_STATE - assertCanControlDeviceState(callingPid); + assertCanControlDeviceState(callingPid, callingUid); final long callingIdentity = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java index 22b6a53ab907..e8c65efeb5cd 100644 --- a/services/core/java/com/android/server/display/BrightnessTracker.java +++ b/services/core/java/com/android/server/display/BrightnessTracker.java @@ -316,7 +316,9 @@ public class BrightnessTracker { } /** - * Notify the BrightnessTracker that the user has changed the brightness of the display. + * Notify the BrightnessTracker that the brightness of the display has changed. + * We pass both the user change and system changes, so that we know the starting point + * of the next user interaction. Only user interactions are then sent as BrightnessChangeEvents. */ public void notifyBrightnessChanged(float brightness, boolean userInitiated, float powerBrightnessFactor, boolean wasShortTermModelActive, @@ -352,10 +354,8 @@ public class BrightnessTracker { // Not currently gathering brightness change information return; } - float previousBrightness = mLastBrightness; mLastBrightness = brightness; - if (!userInitiated) { // We want to record what current brightness is so that we know what the user // changed it from, but if it wasn't user initiated then we don't want to record it @@ -429,7 +429,7 @@ public class BrightnessTracker { BrightnessChangeEvent event = builder.build(); if (DEBUG) { - Slog.d(TAG, "Event " + event.brightness + " " + event.packageName); + Slog.d(TAG, "Event: " + event.toString()); } synchronized (mEventsLock) { mEventsDirty = true; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 6c01dd55b5d0..38445295b0f1 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1576,7 +1576,7 @@ public final class DisplayManagerService extends SystemService { // VirtualDisplay has been successfully constructed. session.setVirtualDisplayId(displayId); // Don't start mirroring until user re-grants consent. - session.setWaitingToRecord(waitForPermissionConsent); + session.setWaitingForConsent(waitForPermissionConsent); // We set the content recording session here on the server side instead of using // a second AIDL call in MediaProjection. By ensuring that a virtual display has diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 0861cb59a944..9d31572c7d76 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1908,21 +1908,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - // Report brightness to brightnesstracker: - // If brightness is not temporary (ie the slider has been released) - // AND if we are not in idle screen brightness mode. - if (!brightnessIsTemporary - && (mAutomaticBrightnessController != null - && !mAutomaticBrightnessController.isInIdleMode())) { - if (userInitiatedChange && (mAutomaticBrightnessController == null - || !mAutomaticBrightnessController.hasValidAmbientLux())) { - // If we don't have a valid lux reading we can't report a valid - // slider event so notify as if the system changed the brightness. - userInitiatedChange = false; - } - notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange, - wasShortTermModelActive); - } + notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange, + wasShortTermModelActive, autoBrightnessEnabled, brightnessIsTemporary); // We save the brightness info *after* the brightness setting has been changed and // adjustments made so that the brightness info reflects the latest value. @@ -2758,22 +2745,43 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated, - boolean wasShortTermModelActive) { + boolean wasShortTermModelActive, boolean autobrightnessEnabled, + boolean brightnessIsTemporary) { final float brightnessInNits = convertToAdjustedNits(brightness); - if (mUseAutoBrightness && brightnessInNits >= 0.0f - && mAutomaticBrightnessController != null && mBrightnessTracker != null) { - // We only want to track changes on devices that can actually map the display backlight - // values into a physical brightness unit since the value provided by the API is in - // nits and not using the arbitrary backlight units. - final float powerFactor = mPowerRequest.lowPowerMode - ? mPowerRequest.screenLowPowerBrightnessFactor - : 1.0f; - mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated, - powerFactor, wasShortTermModelActive, - mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId, - mAutomaticBrightnessController.getLastSensorValues(), - mAutomaticBrightnessController.getLastSensorTimestamps()); + + // Don't report brightness to brightnessTracker: + // If brightness is temporary (ie the slider has not been released) + // or if we are in idle screen brightness mode. + // or display is not on + // or we shouldn't be using autobrightness + // or the nits is invalid. + if (brightnessIsTemporary + || mAutomaticBrightnessController == null + || mAutomaticBrightnessController.isInIdleMode() + || !autobrightnessEnabled + || mBrightnessTracker == null + || !mUseAutoBrightness + || brightnessInNits < 0.0f) { + return; } + + if (userInitiated && !mAutomaticBrightnessController.hasValidAmbientLux()) { + // If we don't have a valid lux reading we can't report a valid + // slider event so notify as if the system changed the brightness. + userInitiated = false; + } + + // We only want to track changes on devices that can actually map the display backlight + // values into a physical brightness unit since the value provided by the API is in + // nits and not using the arbitrary backlight units. + final float powerFactor = mPowerRequest.lowPowerMode + ? mPowerRequest.screenLowPowerBrightnessFactor + : 1.0f; + mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated, + powerFactor, wasShortTermModelActive, + mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId, + mAutomaticBrightnessController.getLastSensorValues(), + mAutomaticBrightnessController.getLastSensorTimestamps()); } private float convertToNits(float brightness) { diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 3b3d5da8396c..167414188180 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -1454,7 +1454,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // Skip the animation when the screen is off or suspended. boolean brightnessAdjusted = false; final boolean brightnessIsTemporary = - (mBrightnessReason.getReason() == BrightnessReason.REASON_TEMPORARY) + (mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_TEMPORARY) || mAutomaticBrightnessStrategy .isTemporaryAutoBrightnessAdjustmentApplied(); if (!mPendingScreenOff) { @@ -1539,21 +1539,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal } } - // Report brightness to brightnesstracker: - // If brightness is not temporary (ie the slider has been released) - // AND if we are not in idle screen brightness mode. - if (!brightnessIsTemporary - && (mAutomaticBrightnessController != null - && !mAutomaticBrightnessController.isInIdleMode())) { - if (userInitiatedChange && (mAutomaticBrightnessController == null - || !mAutomaticBrightnessController.hasValidAmbientLux())) { - // If we don't have a valid lux reading we can't report a valid - // slider event so notify as if the system changed the brightness. - userInitiatedChange = false; - } - notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange, - wasShortTermModelActive); - } + notifyBrightnessTrackerChanged(brightnessState, userInitiatedChange, + wasShortTermModelActive, mAutomaticBrightnessStrategy.isAutoBrightnessEnabled(), + brightnessIsTemporary); // We save the brightness info *after* the brightness setting has been changed and // adjustments made so that the brightness info reflects the latest value. @@ -2171,11 +2159,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal }, mClock.uptimeMillis()); } - private float getAutoBrightnessAdjustmentSetting() { - final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(), - Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT); - return Float.isNaN(adj) ? 0.0f : clampAutoBrightnessAdjustment(adj); - } @Override public float getScreenBrightnessSetting() { @@ -2215,23 +2198,45 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal } private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated, - boolean wasShortTermModelActive) { + boolean wasShortTermModelActive, boolean autobrightnessEnabled, + boolean brightnessIsTemporary) { + final float brightnessInNits = mDisplayBrightnessController.convertToAdjustedNits(brightness); - if (mAutomaticBrightnessStrategy.shouldUseAutoBrightness() && brightnessInNits >= 0.0f - && mAutomaticBrightnessController != null && mBrightnessTracker != null) { - // We only want to track changes on devices that can actually map the display backlight - // values into a physical brightness unit since the value provided by the API is in - // nits and not using the arbitrary backlight units. - final float powerFactor = mPowerRequest.lowPowerMode - ? mPowerRequest.screenLowPowerBrightnessFactor - : 1.0f; - mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated, - powerFactor, wasShortTermModelActive, - mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId, - mAutomaticBrightnessController.getLastSensorValues(), - mAutomaticBrightnessController.getLastSensorTimestamps()); + // Don't report brightness to brightnessTracker: + // If brightness is temporary (ie the slider has not been released) + // or if we are in idle screen brightness mode. + // or display is not on + // or we shouldn't be using autobrightness + // or the nits is invalid. + if (brightnessIsTemporary + || mAutomaticBrightnessController == null + || mAutomaticBrightnessController.isInIdleMode() + || !autobrightnessEnabled + || mBrightnessTracker == null + || !mAutomaticBrightnessStrategy.shouldUseAutoBrightness() + || brightnessInNits < 0.0f) { + return; } + + if (userInitiated && (mAutomaticBrightnessController == null + || !mAutomaticBrightnessController.hasValidAmbientLux())) { + // If we don't have a valid lux reading we can't report a valid + // slider event so notify as if the system changed the brightness. + userInitiated = false; + } + + // We only want to track changes on devices that can actually map the display backlight + // values into a physical brightness unit since the value provided by the API is in + // nits and not using the arbitrary backlight units. + final float powerFactor = mPowerRequest.lowPowerMode + ? mPowerRequest.screenLowPowerBrightnessFactor + : 1.0f; + mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated, + powerFactor, wasShortTermModelActive, + mAutomaticBrightnessController.isDefaultConfig(), mUniqueDisplayId, + mAutomaticBrightnessController.getLastSensorValues(), + mAutomaticBrightnessController.getLastSensorTimestamps()); } @Override @@ -2426,9 +2431,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal } } - private static float clampAutoBrightnessAdjustment(float value) { - return MathUtils.constrain(value, -1.0f, 1.0f); - } private void noteScreenState(int screenState) { // Log screen state change with display id diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index e5c50e61bf25..4edc8bc3eceb 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManagerInternal; -import android.text.TextUtils; import android.util.ArraySet; import android.util.SparseArray; import android.view.Display; @@ -333,6 +332,10 @@ final class LogicalDisplay { return mPrimaryDisplayDevice != null; } + boolean isDirtyLocked() { + return mDirty; + } + /** * Updates the {@link DisplayGroup} to which the logical display belongs. * @@ -341,8 +344,7 @@ final class LogicalDisplay { public void updateDisplayGroupIdLocked(int groupId) { if (groupId != mDisplayGroupId) { mDisplayGroupId = groupId; - mBaseDisplayInfo.displayGroupId = groupId; - mInfo.set(null); + mDirty = true; } } @@ -932,18 +934,6 @@ final class LogicalDisplay { return mDisplayGroupName; } - /** - * Returns whether a display group other than the default display group needs to be assigned. - * - * <p>If display group name is empty or {@code Display.FLAG_OWN_DISPLAY_GROUP} is set, the - * display is assigned to the default display group. - */ - public boolean needsOwnDisplayGroupLocked() { - DisplayInfo info = getDisplayInfoLocked(); - return (info.flags & Display.FLAG_OWN_DISPLAY_GROUP) != 0 - || !TextUtils.isEmpty(mDisplayGroupName); - } - public void dumpLocked(PrintWriter pw) { pw.println("mDisplayId=" + mDisplayId); pw.println("mIsEnabled=" + mIsEnabled); diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 254441c2aa13..d01b03f836a5 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -679,7 +679,9 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { for (int i = mLogicalDisplays.size() - 1; i >= 0; i--) { final int displayId = mLogicalDisplays.keyAt(i); LogicalDisplay display = mLogicalDisplays.valueAt(i); + assignDisplayGroupLocked(display); + boolean wasDirty = display.isDirtyLocked(); mTempDisplayInfo.copyFrom(display.getDisplayInfoLocked()); display.getNonOverrideDisplayInfoLocked(mTempNonOverrideDisplayInfo); @@ -713,19 +715,14 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // The display is new. } else if (!wasPreviouslyUpdated) { Slog.i(TAG, "Adding new display: " + displayId + ": " + newDisplayInfo); - assignDisplayGroupLocked(display); mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_ADDED); // Underlying displays device has changed to a different one. } else if (!TextUtils.equals(mTempDisplayInfo.uniqueId, newDisplayInfo.uniqueId)) { - // FLAG_OWN_DISPLAY_GROUP could have changed, recalculate just in case - assignDisplayGroupLocked(display); mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_SWAPPED); // Something about the display device has changed. - } else if (!mTempDisplayInfo.equals(newDisplayInfo)) { - // FLAG_OWN_DISPLAY_GROUP could have changed, recalculate just in case - assignDisplayGroupLocked(display); + } else if (wasDirty || !mTempDisplayInfo.equals(newDisplayInfo)) { // If only the hdr/sdr ratio changed, then send just the event for that case if ((diff == DisplayDeviceInfo.DIFF_HDR_SDR_RATIO)) { mLogicalDisplaysToUpdate.put(displayId, @@ -851,9 +848,18 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { } } + /** This method should be called before LogicalDisplay.updateLocked, + * DisplayInfo in LogicalDisplay (display.getDisplayInfoLocked()) is not updated yet, + * and should not be used directly or indirectly in this method */ private void assignDisplayGroupLocked(LogicalDisplay display) { + if (!display.isValidLocked()) { // null check for display.mPrimaryDisplayDevice + return; + } + // updated primary device directly from LogicalDisplay (not from DisplayInfo) + final DisplayDevice displayDevice = display.getPrimaryDisplayDeviceLocked(); + // final in LogicalDisplay final int displayId = display.getDisplayIdLocked(); - final String primaryDisplayUniqueId = display.getPrimaryDisplayDeviceLocked().getUniqueId(); + final String primaryDisplayUniqueId = displayDevice.getUniqueId(); final Integer linkedDeviceUniqueId = mVirtualDeviceDisplayMapping.get(primaryDisplayUniqueId); @@ -866,8 +872,17 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { } final DisplayGroup oldGroup = getDisplayGroupLocked(groupId); - // Get the new display group if a change is needed - final boolean needsOwnDisplayGroup = display.needsOwnDisplayGroupLocked(); + // groupName directly from LogicalDisplay (not from DisplayInfo) + final String groupName = display.getDisplayGroupNameLocked(); + // DisplayDeviceInfo is safe to use, it is updated earlier + final DisplayDeviceInfo displayDeviceInfo = displayDevice.getDisplayDeviceInfoLocked(); + // Get the new display group if a change is needed, if display group name is empty and + // {@code DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP} is not set, the display is assigned + // to the default display group. + final boolean needsOwnDisplayGroup = + (displayDeviceInfo.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP) != 0 + || !TextUtils.isEmpty(groupName); + final boolean hasOwnDisplayGroup = groupId != Display.DEFAULT_DISPLAY_GROUP; final boolean needsDeviceDisplayGroup = !needsOwnDisplayGroup && linkedDeviceUniqueId != null; diff --git a/services/core/java/com/android/server/display/brightness/BrightnessUtils.java b/services/core/java/com/android/server/display/brightness/BrightnessUtils.java index 169cc4aa1c2d..3fae22434751 100644 --- a/services/core/java/com/android/server/display/brightness/BrightnessUtils.java +++ b/services/core/java/com/android/server/display/brightness/BrightnessUtils.java @@ -42,6 +42,13 @@ public final class BrightnessUtils { } /** + * Clamps the brightness value in the maximum and the minimum brightness adjustment range + */ + public static float clampBrightnessAdjustment(float value) { + return MathUtils.constrain(value, -1.0f, 1.0f); + } + + /** * A utility to construct the DisplayBrightnessState */ public static DisplayBrightnessState constructDisplayBrightnessState( diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java index 95cbf9835e0f..0e885dcd1738 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java @@ -202,7 +202,7 @@ public class AutomaticBrightnessStrategy { final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(), Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT); mPendingAutoBrightnessAdjustment = Float.isNaN(adj) ? Float.NaN - : BrightnessUtils.clampAbsoluteBrightness(adj); + : BrightnessUtils.clampBrightnessAdjustment(adj); if (userSwitch) { processPendingAutoBrightnessAdjustments(); } @@ -402,6 +402,6 @@ public class AutomaticBrightnessStrategy { private float getAutoBrightnessAdjustmentSetting() { final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(), Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT); - return Float.isNaN(adj) ? 0.0f : BrightnessUtils.clampAbsoluteBrightness(adj); + return Float.isNaN(adj) ? 0.0f : BrightnessUtils.clampBrightnessAdjustment(adj); } } diff --git a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java index d7563e085e61..d764ec41b3b9 100644 --- a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java +++ b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java @@ -17,11 +17,14 @@ package com.android.server.hdmi; /** - * Action to query and track the audio status of the System Audio device when enabling or using - * Absolute Volume Control. Must be removed when AVC is disabled. Performs two main functions: - * 1. When enabling AVC: queries the starting audio status of the System Audio device and - * enables the feature upon receiving a response. - * 2. While AVC is enabled: monitors <Report Audio Status> messages from the System Audio device and + * Action to query and track the audio status of the System Audio device when using + * absolute volume behavior, or adjust-only absolute volume behavior. Must be removed when + * neither behavior is used. + * + * Performs two main functions: + * 1. When enabling AVB: queries the starting audio status of the System Audio device and + * adopts the appropriate volume behavior upon receiving a response. + * 2. While AVB is enabled: monitors <Report Audio Status> messages from the System Audio device and * notifies AudioService if the audio status changes. */ final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction { @@ -74,16 +77,23 @@ final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction { boolean mute = HdmiUtils.isAudioStatusMute(cmd); int volume = HdmiUtils.getAudioStatusVolume(cmd); + + // If the volume is out of range, report it as handled and ignore the message. + // According to the spec, such values are either reserved or indicate an unknown volume. + if (volume == Constants.UNKNOWN_VOLUME) { + return true; + } + AudioStatus audioStatus = new AudioStatus(volume, mute); if (mState == STATE_WAIT_FOR_INITIAL_AUDIO_STATUS) { - localDevice().getService().enableAbsoluteVolumeControl(audioStatus); + localDevice().getService().enableAbsoluteVolumeBehavior(audioStatus); mState = STATE_MONITOR_AUDIO_STATUS; } else if (mState == STATE_MONITOR_AUDIO_STATUS) { if (audioStatus.getVolume() != mLastAudioStatus.getVolume()) { - localDevice().getService().notifyAvcVolumeChange(audioStatus.getVolume()); + localDevice().getService().notifyAvbVolumeChange(audioStatus.getVolume()); } if (audioStatus.getMute() != mLastAudioStatus.getMute()) { - localDevice().getService().notifyAvcMuteChange(audioStatus.getMute()); + localDevice().getService().notifyAvbMuteChange(audioStatus.getMute()); } } mLastAudioStatus = audioStatus; diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java index 438c1ea01e29..94842041af82 100644 --- a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java +++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java @@ -16,9 +16,11 @@ package com.android.server.hdmi; +import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener; +import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener; + import android.annotation.CallbackExecutor; import android.annotation.NonNull; -import android.content.Context; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceVolumeManager; import android.media.VolumeInfo; @@ -26,42 +28,48 @@ import android.media.VolumeInfo; import java.util.concurrent.Executor; /** - * Wrapper for {@link AudioDeviceVolumeManager}. Creates an instance of the class and directly - * passes method calls to that instance. + * Interface with the methods from {@link AudioDeviceVolumeManager} used by the HDMI framework. + * Allows the class to be faked for tests. + * + * See implementations {@link DefaultAudioDeviceVolumeManagerWrapper} and + * {@link FakeAudioFramework.FakeAudioDeviceVolumeManagerWrapper}. */ -public class AudioDeviceVolumeManagerWrapper - implements AudioDeviceVolumeManagerWrapperInterface { +public interface AudioDeviceVolumeManagerWrapper { - private static final String TAG = "AudioDeviceVolumeManagerWrapper"; - - private final AudioDeviceVolumeManager mAudioDeviceVolumeManager; + /** + * Wrapper for {@link AudioDeviceVolumeManager#addOnDeviceVolumeBehaviorChangedListener( + * Executor, OnDeviceVolumeBehaviorChangedListener)} + */ + void addOnDeviceVolumeBehaviorChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener); - public AudioDeviceVolumeManagerWrapper(Context context) { - mAudioDeviceVolumeManager = new AudioDeviceVolumeManager(context); - } + /** + * Wrapper for {@link AudioDeviceVolumeManager#removeOnDeviceVolumeBehaviorChangedListener( + * OnDeviceVolumeBehaviorChangedListener)} + */ + void removeOnDeviceVolumeBehaviorChangedListener( + @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener); - @Override - public void addOnDeviceVolumeBehaviorChangedListener( + /** + * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior( + * AudioDeviceAttributes, VolumeInfo, Executor, OnAudioDeviceVolumeChangedListener, boolean)} + */ + void setDeviceAbsoluteVolumeBehavior( + @NonNull AudioDeviceAttributes device, + @NonNull VolumeInfo volume, @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener) - throws SecurityException { - mAudioDeviceVolumeManager.addOnDeviceVolumeBehaviorChangedListener(executor, listener); - } - - @Override - public void removeOnDeviceVolumeBehaviorChangedListener( - @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener) { - mAudioDeviceVolumeManager.removeOnDeviceVolumeBehaviorChangedListener(listener); - } + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, + boolean handlesVolumeAdjustment); - @Override - public void setDeviceAbsoluteVolumeBehavior( + /** + * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeAdjustOnlyBehavior( + * AudioDeviceAttributes, VolumeInfo, Executor, OnAudioDeviceVolumeChangedListener, boolean)} + */ + void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, @NonNull @CallbackExecutor Executor executor, @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { - mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor, - vclistener, handlesVolumeAdjustment); - } + boolean handlesVolumeAdjustment); } diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java deleted file mode 100644 index 1a1d4c19358b..000000000000 --- a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.hdmi; - -import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener; -import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener; - -import android.annotation.CallbackExecutor; -import android.annotation.NonNull; -import android.media.AudioDeviceAttributes; -import android.media.AudioDeviceVolumeManager; -import android.media.VolumeInfo; - -import java.util.concurrent.Executor; - -/** - * Interface with the methods from {@link AudioDeviceVolumeManager} used by the HDMI framework. - * Allows the class to be faked for tests. - */ -public interface AudioDeviceVolumeManagerWrapperInterface { - - /** - * Wrapper for {@link AudioDeviceVolumeManager#addOnDeviceVolumeBehaviorChangedListener( - * Executor, OnDeviceVolumeBehaviorChangedListener)} - */ - void addOnDeviceVolumeBehaviorChangedListener( - @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener); - - /** - * Wrapper for {@link AudioDeviceVolumeManager#removeOnDeviceVolumeBehaviorChangedListener( - * OnDeviceVolumeBehaviorChangedListener)} - */ - void removeOnDeviceVolumeBehaviorChangedListener( - @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener); - - /** - * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior( - * AudioDeviceAttributes, VolumeInfo, Executor, OnAudioDeviceVolumeChangedListener, boolean)} - */ - void setDeviceAbsoluteVolumeBehavior( - @NonNull AudioDeviceAttributes device, - @NonNull VolumeInfo volume, - @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment); -} diff --git a/services/core/java/com/android/server/hdmi/AudioManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioManagerWrapper.java new file mode 100644 index 000000000000..fd4dd516fd51 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/AudioManagerWrapper.java @@ -0,0 +1,105 @@ +/* + * 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.server.hdmi; + +import android.annotation.NonNull; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceVolumeManager; +import android.media.AudioManager; + +import java.util.List; + +/** + * Interface with the methods from {@link AudioDeviceVolumeManager} used by the HDMI control + * framework. Allows the class to be faked for tests. + * + * See implementations {@link DefaultAudioManagerWrapper} and + * {@link FakeAudioFramework.FakeAudioManagerWrapper}. + */ +public interface AudioManagerWrapper { + + /** + * Wraps {@link AudioManager#adjustStreamVolume(int, int, int)} + */ + void adjustStreamVolume(int streamType, int direction, + @AudioManager.PublicVolumeFlags int flags); + + /** + * Wraps {@link AudioManager#setStreamVolume(int, int, int)} + */ + void setStreamVolume(int streamType, int index, @AudioManager.PublicVolumeFlags int flags); + + /** + * Wraps {@link AudioManager#getStreamVolume(int)} + */ + int getStreamVolume(int streamType); + + /** + * Wraps {@link AudioManager#getStreamMinVolume(int)} + */ + int getStreamMinVolume(int streamType); + + /** + * Wraps {@link AudioManager#getStreamMaxVolume(int)} + */ + int getStreamMaxVolume(int streamType); + + /** + * Wraps {@link AudioManager#isStreamMute(int)} + */ + boolean isStreamMute(int streamType); + + /** + * Wraps {@link AudioManager#setStreamMute(int, boolean)} + */ + void setStreamMute(int streamType, boolean state); + + /** + * Wraps {@link AudioManager#setHdmiSystemAudioSupported(boolean)} + */ + int setHdmiSystemAudioSupported(boolean on); + + /** + * Wraps {@link AudioManager#setWiredDeviceConnectionState(AudioDeviceAttributes, int)} + */ + void setWiredDeviceConnectionState(AudioDeviceAttributes attributes, int state); + + /** + * Wraps {@link AudioManager#setWiredDeviceConnectionState(int, int, String, String)} + */ + void setWiredDeviceConnectionState(int device, int state, String address, String name); + + /** + * Wraps {@link AudioManager#getDeviceVolumeBehavior(AudioDeviceAttributes)} + */ + @AudioManager.DeviceVolumeBehavior + int getDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device); + + /** + * Wraps {@link AudioManager#setDeviceVolumeBehavior(AudioDeviceAttributes, int)} + */ + void setDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device, + @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior); + + /** + * Wraps {@link AudioManager#getDevicesForAttributes(AudioAttributes)} + */ + @NonNull + List<AudioDeviceAttributes> getDevicesForAttributes( + @NonNull AudioAttributes attributes); +} diff --git a/services/core/java/com/android/server/hdmi/AudioStatus.java b/services/core/java/com/android/server/hdmi/AudioStatus.java index a884ffb93a6d..6242c45e8262 100644 --- a/services/core/java/com/android/server/hdmi/AudioStatus.java +++ b/services/core/java/com/android/server/hdmi/AudioStatus.java @@ -23,6 +23,8 @@ import java.util.Objects; /** * Immutable representation of the information in the [Audio Status] operand: * volume status (0 <= N <= 100) and mute status (muted or unmuted). + * The volume level is limited to the range [0, 100] upon construction. + * This object cannot represent an audio status where the volume is unknown, or out of bounds. */ public class AudioStatus { public static final int MAX_VOLUME = 100; @@ -32,7 +34,7 @@ public class AudioStatus { boolean mMute; public AudioStatus(int volume, boolean mute) { - mVolume = volume; + mVolume = Math.max(Math.min(volume, MAX_VOLUME), MIN_VOLUME); mMute = mute; } diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java index c235299e3a65..090d728aedc8 100644 --- a/services/core/java/com/android/server/hdmi/Constants.java +++ b/services/core/java/com/android/server/hdmi/Constants.java @@ -631,10 +631,14 @@ final class Constants { static final String DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX = "enable_earc_tx"; static final String DEVICE_CONFIG_FEATURE_FLAG_TRANSITION_ARC_TO_EARC_TX = "transition_arc_to_earc_tx"; + // Name is abbreviated slightly to avoid line length issues + static final String DEVICE_CONFIG_FEATURE_FLAG_TV_NUMERIC_SOUNDBAR_VOLUME_UI = + "enable_numeric_soundbar_volume_ui_on_tv"; @StringDef({ DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE, DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX, - DEVICE_CONFIG_FEATURE_FLAG_TRANSITION_ARC_TO_EARC_TX + DEVICE_CONFIG_FEATURE_FLAG_TRANSITION_ARC_TO_EARC_TX, + DEVICE_CONFIG_FEATURE_FLAG_TV_NUMERIC_SOUNDBAR_VOLUME_UI }) @interface FeatureFlag {} diff --git a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java new file mode 100644 index 000000000000..ff99ace38ef0 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java @@ -0,0 +1,81 @@ +/* + * 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.server.hdmi; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.content.Context; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceVolumeManager; +import android.media.VolumeInfo; + +import java.util.concurrent.Executor; + +/** + * "Default" wrapper for {@link AudioDeviceVolumeManager}, as opposed to a "Fake" wrapper for + * testing - see {@link FakeAudioFramework.FakeAudioDeviceVolumeManagerWrapper}. + * + * Creates an instance of {@link AudioDeviceVolumeManager} and directly passes method calls + * to that instance. + */ +public class DefaultAudioDeviceVolumeManagerWrapper + implements AudioDeviceVolumeManagerWrapper { + + private static final String TAG = "AudioDeviceVolumeManagerWrapper"; + + private final AudioDeviceVolumeManager mAudioDeviceVolumeManager; + + public DefaultAudioDeviceVolumeManagerWrapper(Context context) { + mAudioDeviceVolumeManager = new AudioDeviceVolumeManager(context); + } + + @Override + public void addOnDeviceVolumeBehaviorChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener) + throws SecurityException { + mAudioDeviceVolumeManager.addOnDeviceVolumeBehaviorChangedListener(executor, listener); + } + + @Override + public void removeOnDeviceVolumeBehaviorChangedListener( + @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener) { + mAudioDeviceVolumeManager.removeOnDeviceVolumeBehaviorChangedListener(listener); + } + + @Override + public void setDeviceAbsoluteVolumeBehavior( + @NonNull AudioDeviceAttributes device, + @NonNull VolumeInfo volume, + @NonNull @CallbackExecutor Executor executor, + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, + boolean handlesVolumeAdjustment) { + mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor, + vclistener, handlesVolumeAdjustment); + } + + @Override + public void setDeviceAbsoluteVolumeAdjustOnlyBehavior( + @NonNull AudioDeviceAttributes device, + @NonNull VolumeInfo volume, + @NonNull @CallbackExecutor Executor executor, + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, + boolean handlesVolumeAdjustment) { + mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeAdjustOnlyBehavior(device, volume, + executor, vclistener, handlesVolumeAdjustment); + } +} diff --git a/services/core/java/com/android/server/hdmi/DefaultAudioManagerWrapper.java b/services/core/java/com/android/server/hdmi/DefaultAudioManagerWrapper.java new file mode 100644 index 000000000000..061e145c27f3 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/DefaultAudioManagerWrapper.java @@ -0,0 +1,115 @@ +/* + * 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.server.hdmi; + +import android.annotation.NonNull; +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioManager; + +import java.util.List; + +/** + * "Default" wrapper for {@link AudioManager}, as opposed to a "Fake" wrapper for testing - + * see {@link FakeAudioFramework.FakeAudioManagerWrapper}. + * + * Creates an instance of {@link AudioManager} and directly passes method calls to that instance. + * +*/ +public class DefaultAudioManagerWrapper implements AudioManagerWrapper { + + private static final String TAG = "DefaultAudioManagerWrapper"; + + private final AudioManager mAudioManager; + + public DefaultAudioManagerWrapper(Context context) { + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + @Override + public void adjustStreamVolume(int streamType, int direction, + @AudioManager.PublicVolumeFlags int flags) { + mAudioManager.adjustStreamVolume(streamType, direction, flags); + } + + @Override + public void setStreamVolume(int streamType, int index, + @AudioManager.PublicVolumeFlags int flags) { + mAudioManager.setStreamVolume(streamType, index, flags); + } + + @Override + public int getStreamVolume(int streamType) { + return mAudioManager.getStreamVolume(streamType); + } + + @Override + public int getStreamMinVolume(int streamType) { + return mAudioManager.getStreamMinVolume(streamType); + } + + @Override + public int getStreamMaxVolume(int streamType) { + return mAudioManager.getStreamMaxVolume(streamType); + } + + @Override + public boolean isStreamMute(int streamType) { + return mAudioManager.isStreamMute(streamType); + } + + @Override + public void setStreamMute(int streamType, boolean state) { + mAudioManager.setStreamMute(streamType, state); + } + + @Override + public int setHdmiSystemAudioSupported(boolean on) { + return mAudioManager.setHdmiSystemAudioSupported(on); + } + + @Override + public void setWiredDeviceConnectionState(AudioDeviceAttributes attributes, int state) { + mAudioManager.setWiredDeviceConnectionState(attributes, state); + } + + @Override + public void setWiredDeviceConnectionState(int device, int state, String address, String name) { + mAudioManager.setWiredDeviceConnectionState(device, state, address, name); + } + + @Override + @AudioManager.DeviceVolumeBehavior + public int getDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device) { + return mAudioManager.getDeviceVolumeBehavior(device); + } + + @Override + public void setDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device, + @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior) { + mAudioManager.setDeviceVolumeBehavior(device, deviceVolumeBehavior); + } + + @Override + @NonNull + public List<AudioDeviceAttributes> getDevicesForAttributes( + @NonNull AudioAttributes attributes) { + return mAudioManager.getDevicesForAttributes(attributes); + } + +} diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index 35c70fbce8ad..ca1abd683d4f 100755 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -1012,17 +1012,22 @@ abstract class HdmiCecLocalDevice extends HdmiLocalDevice { action.start(); } - void addAvcAudioStatusAction(int targetAddress) { - if (!hasAction(AbsoluteVolumeAudioStatusAction.class)) { - addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress)); - } + @ServiceThreadOnly + void startNewAvbAudioStatusAction(int targetAddress) { + assertRunOnServiceThread(); + removeAction(AbsoluteVolumeAudioStatusAction.class); + addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress)); } - void removeAvcAudioStatusAction() { + @ServiceThreadOnly + void removeAvbAudioStatusAction() { + assertRunOnServiceThread(); removeAction(AbsoluteVolumeAudioStatusAction.class); } - void updateAvcVolume(int volumeIndex) { + @ServiceThreadOnly + void updateAvbVolume(int volumeIndex) { + assertRunOnServiceThread(); for (AbsoluteVolumeAudioStatusAction action : getActions(AbsoluteVolumeAudioStatusAction.class)) { action.updateVolume(volumeIndex); @@ -1035,7 +1040,7 @@ abstract class HdmiCecLocalDevice extends HdmiLocalDevice { * and send <Set Audio Volume Level> (to see if it gets a <Feature Abort> in response). */ @ServiceThreadOnly - void queryAvcSupport(int targetAddress) { + void querySetAudioVolumeLevelSupport(int targetAddress) { assertRunOnServiceThread(); // Send <Give Features> if using CEC 2.0 or above. @@ -1054,7 +1059,7 @@ abstract class HdmiCecLocalDevice extends HdmiLocalDevice { @Override public void onComplete(int result) { if (result == HdmiControlManager.RESULT_SUCCESS) { - getService().checkAndUpdateAbsoluteVolumeControlState(); + getService().checkAndUpdateAbsoluteVolumeBehavior(); } } })); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index f47c4b2c24d9..5ef06f9f8967 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -66,7 +66,7 @@ import java.util.stream.Collectors; /** * Represent a logical device of type TV residing in Android system. */ -final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { +public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { private static final String TAG = "HdmiCecLocalDeviceTv"; // Whether ARC is available or not. "true" means that ARC is established between TV and diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java index b82129bdf82f..da38a3446e02 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java @@ -54,22 +54,40 @@ public class HdmiCecMessageValidator { int isValid(byte[] params); } - // Only the direct addressing is allowed. - public static final int DEST_DIRECT = 1 << 0; - // Only the broadcast addressing is allowed. - public static final int DEST_BROADCAST = 1 << 1; - // Both the direct and the broadcast addressing are allowed. - public static final int DEST_ALL = DEST_DIRECT | DEST_BROADCAST; - // True if the messages from address 15 (unregistered) are allowed. - public static final int SRC_UNREGISTERED = 1 << 2; + /** + * Bitmasks used for source and destination validations. + */ + static final int ADDR_TV = 1 << 0; + static final int ADDR_RECORDER_1 = 1 << 1; + static final int ADDR_RECORDER_2 = 1 << 2; + static final int ADDR_TUNER_1 = 1 << 3; + static final int ADDR_PLAYBACK_1 = 1 << 4; + static final int ADDR_AUDIO_SYSTEM = 1 << 5; + static final int ADDR_TUNER_2 = 1 << 6; + static final int ADDR_TUNER_3 = 1 << 7; + static final int ADDR_PLAYBACK_2 = 1 << 8; + static final int ADDR_RECORDER_3 = 1 << 9; + static final int ADDR_TUNER_4 = 1 << 10; + static final int ADDR_PLAYBACK_3 = 1 << 11; + static final int ADDR_BACKUP_1 = 1 << 12; + static final int ADDR_BACKUP_2 = 1 << 13; + static final int ADDR_SPECIFIC_USE = 1 << 14; + static final int ADDR_UNREGISTERED = 1 << 15; + static final int ADDR_BROADCAST = 1 << 15; + static final int ADDR_ALL = (1 << 16) - 1; + static final int ADDR_DIRECT = ADDR_ALL ^ ADDR_BROADCAST; + static final int ADDR_NOT_UNREGISTERED = ADDR_ALL ^ ADDR_UNREGISTERED; private static class ValidationInfo { public final ParameterValidator parameterValidator; - public final int addressType; - - public ValidationInfo(ParameterValidator validator, int type) { - parameterValidator = validator; - addressType = type; + public final int validSources; + public final int validDestinations; + + ValidationInfo(ParameterValidator parameterValidator, int validSources, + int validDestinations) { + this.parameterValidator = parameterValidator; + this.validSources = validSources; + this.validDestinations = validDestinations; } } @@ -81,192 +99,213 @@ public class HdmiCecMessageValidator { // Messages related to the physical address. PhysicalAddressValidator physicalAddressValidator = new PhysicalAddressValidator(); addValidationInfo(Constants.MESSAGE_ACTIVE_SOURCE, - physicalAddressValidator, DEST_BROADCAST | SRC_UNREGISTERED); - addValidationInfo(Constants.MESSAGE_INACTIVE_SOURCE, physicalAddressValidator, DEST_DIRECT); + physicalAddressValidator, ADDR_ALL ^ (ADDR_RECORDER_1 | ADDR_RECORDER_2 + | ADDR_AUDIO_SYSTEM | ADDR_RECORDER_3), ADDR_BROADCAST); + addValidationInfo(Constants.MESSAGE_INACTIVE_SOURCE, + physicalAddressValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS, - new ReportPhysicalAddressValidator(), DEST_BROADCAST | SRC_UNREGISTERED); + new ReportPhysicalAddressValidator(), ADDR_ALL, ADDR_BROADCAST); addValidationInfo(Constants.MESSAGE_ROUTING_CHANGE, - new RoutingChangeValidator(), DEST_BROADCAST | SRC_UNREGISTERED); + new RoutingChangeValidator(), ADDR_ALL, ADDR_BROADCAST); addValidationInfo(Constants.MESSAGE_ROUTING_INFORMATION, - physicalAddressValidator, DEST_BROADCAST | SRC_UNREGISTERED); + physicalAddressValidator, ADDR_ALL, ADDR_BROADCAST); addValidationInfo(Constants.MESSAGE_SET_STREAM_PATH, - physicalAddressValidator, DEST_BROADCAST); + physicalAddressValidator, ADDR_NOT_UNREGISTERED, ADDR_BROADCAST); addValidationInfo(Constants.MESSAGE_SYSTEM_AUDIO_MODE_REQUEST, - new SystemAudioModeRequestValidator(), DEST_DIRECT); + new SystemAudioModeRequestValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); // Messages have no parameter. FixedLengthValidator noneValidator = new FixedLengthValidator(0); - addValidationInfo(Constants.MESSAGE_ABORT, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_GET_CEC_VERSION, noneValidator, DEST_DIRECT); + addValidationInfo(Constants.MESSAGE_ABORT, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_GET_CEC_VERSION, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_GET_MENU_LANGUAGE, - noneValidator, DEST_DIRECT | SRC_UNREGISTERED); - addValidationInfo(Constants.MESSAGE_GIVE_AUDIO_STATUS, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_GIVE_DEVICE_POWER_STATUS, noneValidator, DEST_DIRECT); + noneValidator, ADDR_ALL, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_GIVE_AUDIO_STATUS, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_GIVE_DEVICE_POWER_STATUS, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_GIVE_DEVICE_VENDOR_ID, - noneValidator, DEST_DIRECT | SRC_UNREGISTERED); - addValidationInfo(Constants.MESSAGE_GIVE_OSD_NAME, noneValidator, DEST_DIRECT); + noneValidator, ADDR_ALL, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_GIVE_OSD_NAME, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_GIVE_PHYSICAL_ADDRESS, - noneValidator, DEST_DIRECT | SRC_UNREGISTERED); + noneValidator, ADDR_ALL, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS, - noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_IMAGE_VIEW_ON, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_INITIATE_ARC, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_RECORD_OFF, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_RECORD_TV_SCREEN, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_REPORT_ARC_INITIATED, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_REPORT_ARC_TERMINATED, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_REQUEST_ARC_INITIATION, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_REQUEST_ARC_TERMINATION, noneValidator, DEST_DIRECT); + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_IMAGE_VIEW_ON, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_INITIATE_ARC, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_RECORD_OFF, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_RECORD_TV_SCREEN, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_REPORT_ARC_INITIATED, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_REPORT_ARC_TERMINATED, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_REQUEST_ARC_INITIATION, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_REQUEST_ARC_TERMINATION, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_REQUEST_ACTIVE_SOURCE, - noneValidator, DEST_BROADCAST | SRC_UNREGISTERED); - addValidationInfo(Constants.MESSAGE_STANDBY, noneValidator, DEST_ALL | SRC_UNREGISTERED); - addValidationInfo(Constants.MESSAGE_TERMINATE_ARC, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_TEXT_VIEW_ON, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_TUNER_STEP_DECREMENT, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_TUNER_STEP_INCREMENT, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_USER_CONTROL_RELEASED, noneValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_VENDOR_REMOTE_BUTTON_UP, noneValidator, DEST_ALL); + noneValidator, ADDR_ALL, ADDR_BROADCAST); + addValidationInfo(Constants.MESSAGE_STANDBY, + noneValidator, ADDR_ALL, ADDR_ALL); + addValidationInfo(Constants.MESSAGE_TERMINATE_ARC, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_TEXT_VIEW_ON, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_TUNER_STEP_DECREMENT, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_TUNER_STEP_INCREMENT, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_USER_CONTROL_RELEASED, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_VENDOR_REMOTE_BUTTON_UP, + noneValidator, ADDR_NOT_UNREGISTERED, ADDR_ALL); // TODO: Validate more than length for the following messages. // Messages for the One Touch Record. addValidationInfo(Constants.MESSAGE_RECORD_ON, - new VariableLengthValidator(1, 8), DEST_DIRECT); + new VariableLengthValidator(1, 8), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_RECORD_STATUS, - new RecordStatusInfoValidator(), DEST_DIRECT); - - addValidationInfo( - Constants.MESSAGE_CLEAR_ANALOG_TIMER, new AnalogueTimerValidator(), DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_CLEAR_DIGITAL_TIMER, new DigitalTimerValidator(), DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_CLEAR_EXTERNAL_TIMER, new ExternalTimerValidator(), DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_SET_ANALOG_TIMER, new AnalogueTimerValidator(), DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_SET_DIGITAL_TIMER, new DigitalTimerValidator(), DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_SET_EXTERNAL_TIMER, new ExternalTimerValidator(), DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_SET_TIMER_PROGRAM_TITLE, new AsciiValidator(1, 14), DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_TIMER_CLEARED_STATUS, - new TimerClearedStatusValidator(), - DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_TIMER_STATUS, new TimerStatusValidator(), DEST_DIRECT); + new RecordStatusInfoValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + + addValidationInfo(Constants.MESSAGE_CLEAR_ANALOG_TIMER, + new AnalogueTimerValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_CLEAR_DIGITAL_TIMER, + new DigitalTimerValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_CLEAR_EXTERNAL_TIMER, + new ExternalTimerValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_SET_ANALOG_TIMER, + new AnalogueTimerValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_SET_DIGITAL_TIMER, + new DigitalTimerValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_SET_EXTERNAL_TIMER, + new ExternalTimerValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_SET_TIMER_PROGRAM_TITLE, + new AsciiValidator(1, 14), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_TIMER_CLEARED_STATUS, + new TimerClearedStatusValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_TIMER_STATUS, + new TimerStatusValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); // Messages for the System Information. FixedLengthValidator oneByteValidator = new FixedLengthValidator(1); - addValidationInfo(Constants.MESSAGE_CEC_VERSION, oneByteValidator, DEST_DIRECT); + addValidationInfo(Constants.MESSAGE_CEC_VERSION, + oneByteValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_SET_MENU_LANGUAGE, - new AsciiValidator(3), DEST_BROADCAST); + new AsciiValidator(3), ADDR_NOT_UNREGISTERED, ADDR_BROADCAST); ParameterValidator statusRequestValidator = new MinimumOneByteRangeValidator(0x01, 0x03); - addValidationInfo( - Constants.MESSAGE_DECK_CONTROL, - new MinimumOneByteRangeValidator(0x01, 0x04), DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_DECK_STATUS, - new MinimumOneByteRangeValidator(0x11, 0x1F), DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_GIVE_DECK_STATUS, statusRequestValidator, DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_PLAY, new PlayModeValidator(), DEST_DIRECT); - - addValidationInfo( - Constants.MESSAGE_GIVE_TUNER_DEVICE_STATUS, statusRequestValidator, DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_SELECT_ANALOG_SERVICE, - new SelectAnalogueServiceValidator(), - DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_SELECT_DIGITAL_SERVICE, - new SelectDigitalServiceValidator(), - DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_TUNER_DEVICE_STATUS, - new TunerDeviceStatusValidator(), - DEST_DIRECT); + addValidationInfo(Constants.MESSAGE_DECK_CONTROL, + new MinimumOneByteRangeValidator(0x01, 0x04), ADDR_NOT_UNREGISTERED, + ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_DECK_STATUS, + new MinimumOneByteRangeValidator(0x11, 0x1F), ADDR_NOT_UNREGISTERED, + ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_GIVE_DECK_STATUS, + statusRequestValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_PLAY, + new PlayModeValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + + addValidationInfo(Constants.MESSAGE_GIVE_TUNER_DEVICE_STATUS, + statusRequestValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_SELECT_ANALOG_SERVICE, + new SelectAnalogueServiceValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_SELECT_DIGITAL_SERVICE, + new SelectDigitalServiceValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_TUNER_DEVICE_STATUS, + new TunerDeviceStatusValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); // Messages for the Vendor Specific Commands. VariableLengthValidator maxLengthValidator = new VariableLengthValidator(0, 14); addValidationInfo(Constants.MESSAGE_DEVICE_VENDOR_ID, - new FixedLengthValidator(3), DEST_BROADCAST); + new FixedLengthValidator(3), ADDR_NOT_UNREGISTERED, ADDR_BROADCAST); // Allow unregistered source for all vendor specific commands, because we don't know // how to use the commands at this moment. addValidationInfo(Constants.MESSAGE_VENDOR_COMMAND, - new VariableLengthValidator(1, 14), DEST_DIRECT | SRC_UNREGISTERED); + new VariableLengthValidator(1, 14), ADDR_ALL, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_VENDOR_COMMAND_WITH_ID, - new VariableLengthValidator(4, 14), DEST_ALL | SRC_UNREGISTERED); + new VariableLengthValidator(4, 14), ADDR_ALL, ADDR_ALL); addValidationInfo(Constants.MESSAGE_VENDOR_REMOTE_BUTTON_DOWN, - maxLengthValidator, DEST_ALL | SRC_UNREGISTERED); + maxLengthValidator, ADDR_ALL, ADDR_ALL); // Messages for the OSD. - addValidationInfo(Constants.MESSAGE_SET_OSD_STRING, new OsdStringValidator(), DEST_DIRECT); - addValidationInfo(Constants.MESSAGE_SET_OSD_NAME, new AsciiValidator(1, 14), DEST_DIRECT); + addValidationInfo(Constants.MESSAGE_SET_OSD_STRING, + new OsdStringValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_SET_OSD_NAME, + new AsciiValidator(1, 14), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); // Messages for the Device Menu Control. - addValidationInfo( - Constants.MESSAGE_MENU_REQUEST, - new MinimumOneByteRangeValidator(0x00, 0x02), DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_MENU_STATUS, - new MinimumOneByteRangeValidator(0x00, 0x01), DEST_DIRECT); + addValidationInfo(Constants.MESSAGE_MENU_REQUEST, + new MinimumOneByteRangeValidator(0x00, 0x02), ADDR_NOT_UNREGISTERED, + ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_MENU_STATUS, + new MinimumOneByteRangeValidator(0x00, 0x01), ADDR_NOT_UNREGISTERED, + ADDR_DIRECT); // Messages for the Remote Control Passthrough. - addValidationInfo( - Constants.MESSAGE_USER_CONTROL_PRESSED, - new UserControlPressedValidator(), - DEST_DIRECT); + addValidationInfo(Constants.MESSAGE_USER_CONTROL_PRESSED, + new UserControlPressedValidator(), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); // Messages for the Power Status. - addValidationInfo( - Constants.MESSAGE_REPORT_POWER_STATUS, + addValidationInfo(Constants.MESSAGE_REPORT_POWER_STATUS, new MinimumOneByteRangeValidator(0x00, 0x03), - DEST_DIRECT | DEST_BROADCAST); + ADDR_NOT_UNREGISTERED, ADDR_ALL); // Messages for the General Protocol. addValidationInfo(Constants.MESSAGE_FEATURE_ABORT, - new FixedLengthValidator(2), DEST_DIRECT); + new FixedLengthValidator(2), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); // Messages for the System Audio Control. - addValidationInfo(Constants.MESSAGE_REPORT_AUDIO_STATUS, oneByteValidator, DEST_DIRECT); + addValidationInfo(Constants.MESSAGE_REPORT_AUDIO_STATUS, + oneByteValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_REPORT_SHORT_AUDIO_DESCRIPTOR, - new FixedLengthValidator(3), DEST_DIRECT); + new FixedLengthValidator(3), ADDR_NOT_UNREGISTERED, ADDR_DIRECT); addValidationInfo(Constants.MESSAGE_REQUEST_SHORT_AUDIO_DESCRIPTOR, - oneByteValidator, DEST_DIRECT); - addValidationInfo( - Constants.MESSAGE_SET_SYSTEM_AUDIO_MODE, + oneByteValidator, ADDR_NOT_UNREGISTERED, ADDR_DIRECT); + addValidationInfo(Constants.MESSAGE_SET_SYSTEM_AUDIO_MODE, new MinimumOneByteRangeValidator(0x00, 0x01), - DEST_ALL); - addValidationInfo( - Constants.MESSAGE_SYSTEM_AUDIO_MODE_STATUS, - new SingleByteRangeValidator(0x00, 0x01), - DEST_DIRECT); + ADDR_NOT_UNREGISTERED, ADDR_ALL); + addValidationInfo(Constants.MESSAGE_SYSTEM_AUDIO_MODE_STATUS, + new SingleByteRangeValidator(0x00, 0x01), ADDR_NOT_UNREGISTERED, + ADDR_DIRECT); // Messages for the Audio Rate Control. - addValidationInfo( - Constants.MESSAGE_SET_AUDIO_RATE, - new MinimumOneByteRangeValidator(0x00, 0x06), - DEST_DIRECT); + addValidationInfo(Constants.MESSAGE_SET_AUDIO_RATE, + new MinimumOneByteRangeValidator(0x00, 0x06), ADDR_NOT_UNREGISTERED, + ADDR_DIRECT); // Messages for Feature Discovery. - addValidationInfo(Constants.MESSAGE_GIVE_FEATURES, noneValidator, - DEST_DIRECT | SRC_UNREGISTERED); + addValidationInfo(Constants.MESSAGE_GIVE_FEATURES, + noneValidator, ADDR_ALL, ADDR_DIRECT); // Messages for Dynamic Auto Lipsync - addValidationInfo(Constants.MESSAGE_REQUEST_CURRENT_LATENCY, physicalAddressValidator, - DEST_BROADCAST); + addValidationInfo(Constants.MESSAGE_REQUEST_CURRENT_LATENCY, + physicalAddressValidator, ADDR_NOT_UNREGISTERED, ADDR_BROADCAST); addValidationInfo(Constants.MESSAGE_REPORT_CURRENT_LATENCY, - new VariableLengthValidator(4, 14), DEST_BROADCAST); + new VariableLengthValidator(4, 14), ADDR_NOT_UNREGISTERED, ADDR_BROADCAST); // All Messages for the ARC have no parameters. // Messages for the Capability Discovery and Control. - addValidationInfo(Constants.MESSAGE_CDC_MESSAGE, maxLengthValidator, - DEST_BROADCAST | SRC_UNREGISTERED); + addValidationInfo(Constants.MESSAGE_CDC_MESSAGE, + maxLengthValidator, ADDR_ALL, ADDR_BROADCAST); } - private static void addValidationInfo(int opcode, ParameterValidator validator, int addrType) { - sValidationInfo.append(opcode, new ValidationInfo(validator, addrType)); + /** + * validSources and validDestinations are bitmasks that represent the sources and destinations + * that are allowed for a message. + */ + private static void addValidationInfo(int opcode, ParameterValidator validator, + int validSources, int validDestinations) { + sValidationInfo.append(opcode, new ValidationInfo(validator, validSources, + validDestinations)); } /** @@ -281,7 +320,8 @@ public class HdmiCecMessageValidator { return OK; } - int addressValidationResult = validateAddress(source, destination, info.addressType); + int addressValidationResult = validateAddress(source, destination, info.validSources, + info.validDestinations); if (addressValidationResult != OK) { return addressValidationResult; } @@ -301,25 +341,21 @@ public class HdmiCecMessageValidator { * on static information in this class. * @param source Source address to validate * @param destination Destination address to validate - * @param addressType Rules for validating the addresses - e.g. {@link #DEST_BROADCAST} + * @param validSources Bitmask used to validate the source address + * - e.g. {@link #ADDR_SOURCE_DEVICES} + * @param validDestinations Bitmask used to validate the destination address + * - e.g. {@link #ADDR_DIRECT} */ @ValidationResult - static int validateAddress(int source, int destination, int addressType) { + static int validateAddress(int source, int destination, int validSources, + int validDestinations) { // Check the source field. - if (source == Constants.ADDR_UNREGISTERED - && (addressType & SRC_UNREGISTERED) == 0) { + if ((validSources & (1 << source)) == 0) { return ERROR_SOURCE; } - // Check the destination field. - if (destination == Constants.ADDR_BROADCAST) { - if ((addressType & DEST_BROADCAST) == 0) { - return ERROR_DESTINATION; - } - } else { // Direct addressing. - if ((addressType & DEST_DIRECT) == 0) { - return ERROR_DESTINATION; - } + if ((validDestinations & (1 << destination)) == 0) { + return ERROR_DESTINATION; } return OK; } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java index 18a69c8e9d81..7045e65a8936 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java @@ -259,7 +259,7 @@ public class HdmiCecNetwork { // The addition of a local device should not notify listeners return; } - mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState(); + mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { // Don't notify listeners of devices that haven't reported their physical address yet return; @@ -384,7 +384,7 @@ public class HdmiCecNetwork { final void removeCecDevice(HdmiCecLocalDevice localDevice, int address) { assertRunOnServiceThread(); HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address)); - mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState(); + mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); localDevice.mCecMessageCache.flushMessagesFrom(address); if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { // Don't notify listeners of devices that haven't reported their physical address yet @@ -592,7 +592,7 @@ public class HdmiCecNetwork { updateCecDevice(newDeviceInfo); - mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState(); + mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); } @ServiceThreadOnly @@ -625,7 +625,7 @@ public class HdmiCecNetwork { .build(); updateCecDevice(newDeviceInfo); - mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState(); + mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 9cd5272b356b..cede2738cff0 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -231,8 +231,8 @@ public class HdmiControlService extends SystemService { new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, ""); - // Audio output devices used for Absolute Volume Control - private static final List<AudioDeviceAttributes> AVC_AUDIO_OUTPUT_DEVICES = + // Audio output devices used for absolute volume behavior + private static final List<AudioDeviceAttributes> AVB_AUDIO_OUTPUT_DEVICES = Collections.unmodifiableList(Arrays.asList(AUDIO_OUTPUT_DEVICE_HDMI, AUDIO_OUTPUT_DEVICE_HDMI_ARC, AUDIO_OUTPUT_DEVICE_HDMI_EARC)); @@ -265,7 +265,7 @@ public class HdmiControlService extends SystemService { @HdmiControlManager.VolumeControl private int mHdmiCecVolumeControl; - // Caches the volume behaviors of all audio output devices in AVC_AUDIO_OUTPUT_DEVICES. + // Caches the volume behaviors of all audio output devices in AVB_AUDIO_OUTPUT_DEVICES. @GuardedBy("mLock") private Map<AudioDeviceAttributes, Integer> mAudioDeviceVolumeBehaviors = new HashMap<>(); @@ -458,6 +458,9 @@ public class HdmiControlService extends SystemService { private boolean mEarcTxFeatureFlagEnabled = false; @ServiceThreadOnly + private boolean mNumericSoundbarVolumeUiOnTvFeatureFlagEnabled = false; + + @ServiceThreadOnly private boolean mTransitionFromArcToEarcTxEnabled = false; @ServiceThreadOnly @@ -491,10 +494,10 @@ public class HdmiControlService extends SystemService { private PowerManagerInternalWrapper mPowerManagerInternal; @Nullable - private AudioManager mAudioManager; + private AudioManagerWrapper mAudioManager; @Nullable - private AudioDeviceVolumeManagerWrapperInterface mAudioDeviceVolumeManager; + private AudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager; @Nullable private Looper mIoLooper; @@ -528,18 +531,20 @@ public class HdmiControlService extends SystemService { /** * Constructor for testing. * - * It's critical to use a fake AudioDeviceVolumeManager because a normally instantiated - * AudioDeviceVolumeManager can access the "real" AudioService on the DUT. + * Takes fakes for AudioManager and AudioDeviceVolumeManager. * - * @see FakeAudioDeviceVolumeManagerWrapper + * This is especially important for AudioDeviceVolumeManager because a normally instantiated + * AudioDeviceVolumeManager can access the "real" AudioService on the DUT. */ @VisibleForTesting HdmiControlService(Context context, List<Integer> deviceTypes, - AudioDeviceVolumeManagerWrapperInterface audioDeviceVolumeManager) { + AudioManagerWrapper audioManager, + AudioDeviceVolumeManagerWrapper audioDeviceVolumeManager) { super(context); mCecLocalDevices = deviceTypes; mSettingsObserver = new SettingsObserver(mHandler); mHdmiCecConfig = new HdmiCecConfig(context); mDeviceConfig = new DeviceConfigWrapper(); + mAudioManager = audioManager; mAudioDeviceVolumeManager = audioDeviceVolumeManager; } @@ -680,6 +685,8 @@ public class HdmiControlService extends SystemService { Constants.DEVICE_CONFIG_FEATURE_FLAG_ENABLE_EARC_TX, false); mTransitionFromArcToEarcTxEnabled = mDeviceConfig.getBoolean( Constants.DEVICE_CONFIG_FEATURE_FLAG_TRANSITION_ARC_TO_EARC_TX, false); + mNumericSoundbarVolumeUiOnTvFeatureFlagEnabled = mDeviceConfig.getBoolean( + Constants.DEVICE_CONFIG_FEATURE_FLAG_TV_NUMERIC_SOUNDBAR_VOLUME_UI, false); synchronized (mLock) { mEarcEnabled = (mHdmiCecConfig.getIntValue( @@ -899,6 +906,17 @@ public class HdmiControlService extends SystemService { false); } }); + + mDeviceConfig.addOnPropertiesChangedListener(getContext().getMainExecutor(), + new DeviceConfig.OnPropertiesChangedListener() { + @Override + public void onPropertiesChanged(DeviceConfig.Properties properties) { + mNumericSoundbarVolumeUiOnTvFeatureFlagEnabled = properties.getBoolean( + Constants.DEVICE_CONFIG_FEATURE_FLAG_TV_NUMERIC_SOUNDBAR_VOLUME_UI, + false); + checkAndUpdateAbsoluteVolumeBehavior(); + } + }); } /** Returns true if the device screen is off */ boolean isScreenOff() { @@ -934,11 +952,6 @@ public class HdmiControlService extends SystemService { } @VisibleForTesting - void setAudioManager(AudioManager audioManager) { - mAudioManager = audioManager; - } - - @VisibleForTesting void setCecController(HdmiCecController cecController) { mCecController = cecController; } @@ -975,11 +988,13 @@ public class HdmiControlService extends SystemService { Context.TV_INPUT_SERVICE); mPowerManager = new PowerManagerWrapper(getContext()); mPowerManagerInternal = new PowerManagerInternalWrapper(); - mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); + if (mAudioManager == null) { + mAudioManager = new DefaultAudioManagerWrapper(getContext()); + } mStreamMusicMaxVolume = getAudioManager().getStreamMaxVolume(AudioManager.STREAM_MUSIC); if (mAudioDeviceVolumeManager == null) { mAudioDeviceVolumeManager = - new AudioDeviceVolumeManagerWrapper(getContext()); + new DefaultAudioDeviceVolumeManagerWrapper(getContext()); } getAudioDeviceVolumeManager().addOnDeviceVolumeBehaviorChangedListener( mServiceThreadExecutor, this::onDeviceVolumeBehaviorChanged); @@ -1773,7 +1788,7 @@ public class HdmiControlService extends SystemService { == HdmiControlManager.VOLUME_CONTROL_DISABLED) { return; } - AudioManager audioManager = getAudioManager(); + AudioManagerWrapper audioManager = getAudioManager(); boolean muted = audioManager.isStreamMute(AudioManager.STREAM_MUSIC); if (mute) { if (!muted) { @@ -2751,7 +2766,7 @@ public class HdmiControlService extends SystemService { pw.println("mPowerStatus: " + mPowerStatusController.getPowerStatus()); pw.println("mIsCecAvailable: " + mIsCecAvailable); pw.println("mCecVersion: " + mCecVersion); - pw.println("mIsAbsoluteVolumeControlEnabled: " + isAbsoluteVolumeControlEnabled()); + pw.println("mIsAbsoluteVolumeBehaviorEnabled: " + isAbsoluteVolumeBehaviorEnabled()); // System settings pw.println("System_settings:"); @@ -2919,7 +2934,7 @@ public class HdmiControlService extends SystemService { @HdmiControlManager.VolumeControl int hdmiCecVolumeControl) { mHdmiCecVolumeControl = hdmiCecVolumeControl; announceHdmiCecVolumeControlFeatureChange(hdmiCecVolumeControl); - runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState); + runOnServiceThread(this::checkAndUpdateAbsoluteVolumeBehavior); } // Get the source address to send out commands to devices connected to the current device @@ -3485,7 +3500,7 @@ public class HdmiControlService extends SystemService { * Returns null before the boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}. */ @Nullable - AudioManager getAudioManager() { + AudioManagerWrapper getAudioManager() { return mAudioManager; } @@ -3493,7 +3508,7 @@ public class HdmiControlService extends SystemService { * Returns null before the boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}. */ @Nullable - private AudioDeviceVolumeManagerWrapperInterface getAudioDeviceVolumeManager() { + private AudioDeviceVolumeManagerWrapper getAudioDeviceVolumeManager() { return mAudioDeviceVolumeManager; } @@ -3882,7 +3897,7 @@ public class HdmiControlService extends SystemService { synchronized (mLock) { mSystemAudioActivated = on; } - runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState); + runOnServiceThread(this::checkAndUpdateAbsoluteVolumeBehavior); } @ServiceThreadOnly @@ -3993,7 +4008,7 @@ public class HdmiControlService extends SystemService { deviceIsActiveSource, caller); } - runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState); + runOnServiceThread(this::checkAndUpdateAbsoluteVolumeBehavior); } // This method should only be called when the device can be the active source @@ -4189,17 +4204,17 @@ public class HdmiControlService extends SystemService { /** * Listener for changes to the volume behavior of an audio output device. Caches the - * volume behavior of devices used for Absolute Volume Control. + * volume behavior of devices used for absolute volume behavior. */ @VisibleForTesting @ServiceThreadOnly void onDeviceVolumeBehaviorChanged(AudioDeviceAttributes device, int volumeBehavior) { assertRunOnServiceThread(); - if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) { + if (AVB_AUDIO_OUTPUT_DEVICES.contains(device)) { synchronized (mLock) { mAudioDeviceVolumeBehaviors.put(device, volumeBehavior); } - checkAndUpdateAbsoluteVolumeControlState(); + checkAndUpdateAbsoluteVolumeBehavior(); } } @@ -4209,7 +4224,7 @@ public class HdmiControlService extends SystemService { */ @AudioManager.DeviceVolumeBehavior private int getDeviceVolumeBehavior(AudioDeviceAttributes device) { - if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) { + if (AVB_AUDIO_OUTPUT_DEVICES.contains(device)) { synchronized (mLock) { if (mAudioDeviceVolumeBehaviors.containsKey(device)) { return mAudioDeviceVolumeBehaviors.get(device); @@ -4220,25 +4235,29 @@ public class HdmiControlService extends SystemService { } /** - * Returns whether Absolute Volume Control is enabled or not. This is determined by the + * Returns whether absolute volume behavior is enabled or not. This is determined by the * volume behavior of the relevant HDMI audio output device(s) for this device's type. */ - public boolean isAbsoluteVolumeControlEnabled() { + public boolean isAbsoluteVolumeBehaviorEnabled() { if (!isTvDevice() && !isPlaybackDevice()) { return false; } - AudioDeviceAttributes avcAudioOutputDevice = getAvcAudioOutputDevice(); - if (avcAudioOutputDevice == null) { + AudioDeviceAttributes avbAudioOutputDevice = getAvbAudioOutputDevice(); + if (avbAudioOutputDevice == null) { return false; } - return getDeviceVolumeBehavior(avcAudioOutputDevice) - == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE; + + @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior = + getDeviceVolumeBehavior(avbAudioOutputDevice); + + return deviceVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE + || deviceVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY; } - private AudioDeviceAttributes getAvcAudioOutputDevice() { - if (isTvDevice()) { + private AudioDeviceAttributes getAvbAudioOutputDevice() { + if (tv() != null) { return tv().getSystemAudioOutputDevice(); - } else if (isPlaybackDevice()) { + } else if (playback() != null) { return AUDIO_OUTPUT_DEVICE_HDMI; } else { return null; @@ -4246,30 +4265,30 @@ public class HdmiControlService extends SystemService { } /** - * Checks the conditions for Absolute Volume Control (AVC), and enables or disables the feature - * if necessary. AVC is enabled precisely when a specific audio output device - * (HDMI for playback devices, and HDMI_ARC or HDMI_EARC for TVs) is using absolute volume - * behavior. + * This method is responsible for adopting or disabling absolute volume behavior and + * adjust-only absolute volume behavior in AudioService. These volume behaviors are adopted on + * specific audio output devices: HDMI for playback devices, and HDMI_ARC or HDMI_EARC for TVs. * - * AVC must be enabled on a Playback device or TV precisely when it is playing - * audio on an external device (the System Audio device) that supports the feature. - * This reduces to these conditions: + * This method enables absolute volume behavior on a Playback device or TV panel when it is + * playing audio on an external device (the System Audio device) that supports the feature. + * This allows the volume level of the System Audio device to be tracked and set by Android. * + * Absolute volume behavior requires the following conditions: * 1. If the System Audio Device is an Audio System: System Audio Mode is active * 2. Our HDMI audio output device is using full volume behavior * 3. CEC volume is enabled - * 4. The System Audio device supports AVC (i.e. it supports <Set Audio Volume Level>) - * - * If not all of these conditions are met, this method disables AVC if necessary. + * 4. The System Audio device supports the <Set Audio Volume Level> message * - * If all of these conditions are met, this method starts an action to query the System Audio - * device's audio status, which enables AVC upon obtaining the audio status. + * This method enables adjust-only absolute volume behavior on TV panels when conditions + * 1, 2, and 3 are met, but condition 4 is not. This allows TVs to track the volume level of + * the System Audio device and display numeric volume UI for it, even if the System Audio device + * does not support <Set Audio Volume Level>. */ @ServiceThreadOnly - void checkAndUpdateAbsoluteVolumeControlState() { + void checkAndUpdateAbsoluteVolumeBehavior() { assertRunOnServiceThread(); - // Can't enable or disable AVC before we have access to system services + // Can't set volume behavior before we have access to system services if (getAudioManager() == null) { return; } @@ -4281,84 +4300,112 @@ public class HdmiControlService extends SystemService { // (Doesn't apply to Playback Devices, where if SAM isn't active, we assume the // TV is the System Audio Device instead.) if (!isSystemAudioActivated()) { - disableAbsoluteVolumeControl(); + switchToFullVolumeBehavior(); return; } } else if (isPlaybackDevice() && playback() != null) { localCecDevice = playback(); } else { - // Either this device type doesn't support AVC, or it hasn't fully initialized yet + // Either this device type doesn't support AVB, or it hasn't fully initialized yet return; } - HdmiDeviceInfo systemAudioDeviceInfo = getHdmiCecNetwork().getSafeCecDeviceInfo( + HdmiDeviceInfo systemAudioDeviceInfo = getDeviceInfo( localCecDevice.findAudioReceiverAddress()); @AudioManager.DeviceVolumeBehavior int currentVolumeBehavior = - getDeviceVolumeBehavior(getAvcAudioOutputDevice()); + getDeviceVolumeBehavior(getAvbAudioOutputDevice()); // Condition 2: Already using full or absolute volume behavior boolean alreadyUsingFullOrAbsoluteVolume = - currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL - || currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE; + (currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL) + || (currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) + || (currentVolumeBehavior + == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); // Condition 3: CEC volume is enabled boolean cecVolumeEnabled = getHdmiCecVolumeControl() == HdmiControlManager.VOLUME_CONTROL_ENABLED; if (!cecVolumeEnabled || !alreadyUsingFullOrAbsoluteVolume) { - disableAbsoluteVolumeControl(); + switchToFullVolumeBehavior(); return; } - // Check for safety: if the System Audio device is a candidate for AVC, we should already + // Check for safety: if the System Audio device is a candidate for AVB, we should already // have received messages from it to trigger the other conditions. if (systemAudioDeviceInfo == null) { - disableAbsoluteVolumeControl(); + switchToFullVolumeBehavior(); return; } - // Condition 4: The System Audio device supports AVC (i.e. <Set Audio Volume Level>). + + // Condition 4: The System Audio device supports <Set Audio Volume Level> switch (systemAudioDeviceInfo.getDeviceFeatures().getSetAudioVolumeLevelSupport()) { case DeviceFeatures.FEATURE_SUPPORTED: - if (!isAbsoluteVolumeControlEnabled()) { - // Start an action that will call {@link #enableAbsoluteVolumeControl} + if (currentVolumeBehavior != AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) { + // Start an action that will call enableAbsoluteVolumeBehavior // once the System Audio device sends <Report Audio Status> - localCecDevice.addAvcAudioStatusAction( + localCecDevice.startNewAvbAudioStatusAction( systemAudioDeviceInfo.getLogicalAddress()); } return; case DeviceFeatures.FEATURE_NOT_SUPPORTED: - disableAbsoluteVolumeControl(); + // TVs may adopt adjust-only absolute volume behavior if condition 4 isn't met. + // This allows the device to display numeric volume UI for the System Audio device. + if (tv() != null && mNumericSoundbarVolumeUiOnTvFeatureFlagEnabled) { + if (currentVolumeBehavior + != AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY) { + // If we're currently using absolute volume behavior, switch to full volume + // behavior until we successfully adopt adjust-only absolute volume behavior + if (currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) { + getAudioManager().setDeviceVolumeBehavior(getAvbAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + } + // Start an action that will call enableAbsoluteVolumeBehavior + // once the System Audio device sends <Report Audio Status> + localCecDevice.startNewAvbAudioStatusAction( + systemAudioDeviceInfo.getLogicalAddress()); + } + } else { + switchToFullVolumeBehavior(); + } return; case DeviceFeatures.FEATURE_SUPPORT_UNKNOWN: - disableAbsoluteVolumeControl(); - localCecDevice.queryAvcSupport(systemAudioDeviceInfo.getLogicalAddress()); - return; - default: - return; + if (currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) { + switchToFullVolumeBehavior(); + } + localCecDevice.querySetAudioVolumeLevelSupport( + systemAudioDeviceInfo.getLogicalAddress()); } } - private void disableAbsoluteVolumeControl() { - if (isPlaybackDevice()) { - playback().removeAvcAudioStatusAction(); - } else if (isTvDevice()) { - tv().removeAvcAudioStatusAction(); - } - AudioDeviceAttributes device = getAvcAudioOutputDevice(); - if (getDeviceVolumeBehavior(device) == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) { + /** + * Switches to full volume behavior, if either absolute or adjust-only absolute volume behavior + * are currently used. Removes the action for handling volume updates for these behaviors. + */ + private void switchToFullVolumeBehavior() { + if (playback() != null) { + playback().removeAvbAudioStatusAction(); + } else if (tv() != null) { + tv().removeAvbAudioStatusAction(); + } + AudioDeviceAttributes device = getAvbAudioOutputDevice(); + int volumeBehavior = getDeviceVolumeBehavior(device); + if (volumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE + || volumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY) { getAudioManager().setDeviceVolumeBehavior(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); } } /** - * Enables Absolute Volume Control. Should only be called when all the conditions for - * AVC are met (see {@link #checkAndUpdateAbsoluteVolumeControlState}). + * Enables absolute volume behavior or adjust-only absolute volume behavior. Should only be + * called when the conditions for one of these behaviors is met - + * see {@link #checkAndUpdateAbsoluteVolumeBehavior}. + * * @param audioStatus The initial audio status to set the audio output device to */ - void enableAbsoluteVolumeControl(AudioStatus audioStatus) { + void enableAbsoluteVolumeBehavior(AudioStatus audioStatus) { HdmiCecLocalDevice localDevice = isPlaybackDevice() ? playback() : tv(); - HdmiDeviceInfo systemAudioDevice = getHdmiCecNetwork().getDeviceInfo( - localDevice.findAudioReceiverAddress()); + HdmiDeviceInfo systemAudioDevice = getDeviceInfo(localDevice.findAudioReceiverAddress()); VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) .setMuted(audioStatus.getMute()) .setVolumeIndex(audioStatus.getVolume()) @@ -4370,10 +4417,21 @@ public class HdmiControlService extends SystemService { // AudioService sets the volume of the stream and device based on the input VolumeInfo // when enabling absolute volume behavior, but not the mute state - notifyAvcMuteChange(audioStatus.getMute()); - getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior( - getAvcAudioOutputDevice(), volumeInfo, mServiceThreadExecutor, - mAbsoluteVolumeChangedListener, true); + notifyAvbMuteChange(audioStatus.getMute()); + + // If <Set Audio Volume Level> is supported, enable absolute volume behavior. + // Otherwise, enable adjust-only AVB on TVs only. + if (systemAudioDevice.getDeviceFeatures().getSetAudioVolumeLevelSupport() + == DeviceFeatures.FEATURE_SUPPORTED) { + getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior( + getAvbAudioOutputDevice(), volumeInfo, mServiceThreadExecutor, + mAbsoluteVolumeChangedListener, true); + } else if (tv() != null) { + getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeAdjustOnlyBehavior( + getAvbAudioOutputDevice(), volumeInfo, mServiceThreadExecutor, + mAbsoluteVolumeChangedListener, true); + } + } private AbsoluteVolumeChangedListener mAbsoluteVolumeChangedListener; @@ -4407,6 +4465,14 @@ public class HdmiControlService extends SystemService { public void onAudioDeviceVolumeChanged( @NonNull AudioDeviceAttributes audioDevice, @NonNull VolumeInfo volumeInfo) { + + // Do nothing if the System Audio device does not support <Set Audio Volume Level> + if (mSystemAudioDevice.getDeviceFeatures().getSetAudioVolumeLevelSupport() + != DeviceFeatures.FEATURE_SUPPORTED) { + return; + } + + // Send <Set Audio Volume Level> to notify the System Audio device of the volume change int localDeviceAddress = mLocalDevice.getDeviceInfo().getLogicalAddress(); sendCecCommand(SetAudioVolumeLevelMessage.build( localDeviceAddress, @@ -4418,8 +4484,8 @@ public class HdmiControlService extends SystemService { if (errorCode == SendMessageResult.SUCCESS) { // Update the volume tracked in our AbsoluteVolumeAudioStatusAction // so it correctly processes incoming <Report Audio Status> messages - HdmiCecLocalDevice avcDevice = isTvDevice() ? tv() : playback(); - avcDevice.updateAvcVolume(volumeInfo.getVolumeIndex()); + HdmiCecLocalDevice avbDevice = isTvDevice() ? tv() : playback(); + avbDevice.updateAvbVolume(volumeInfo.getVolumeIndex()); } else { sendCecCommand(HdmiCecMessageBuilder.buildGiveAudioStatus( localDeviceAddress, @@ -4477,13 +4543,13 @@ public class HdmiControlService extends SystemService { /** * Notifies AudioService of a change in the volume of the System Audio device. Has no effect if - * AVC is disabled, or the audio output device for AVC is not playing for STREAM_MUSIC + * AVB is disabled, or the audio output device for AVB is not playing for STREAM_MUSIC */ - void notifyAvcVolumeChange(int volume) { - if (!isAbsoluteVolumeControlEnabled()) return; + void notifyAvbVolumeChange(int volume) { + if (!isAbsoluteVolumeBehaviorEnabled()) return; List<AudioDeviceAttributes> streamMusicDevices = getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES); - if (streamMusicDevices.contains(getAvcAudioOutputDevice())) { + if (streamMusicDevices.contains(getAvbAudioOutputDevice())) { int flags = AudioManager.FLAG_ABSOLUTE_VOLUME; if (isTvDevice()) { flags |= AudioManager.FLAG_SHOW_UI; @@ -4494,13 +4560,13 @@ public class HdmiControlService extends SystemService { /** * Notifies AudioService of a change in the mute status of the System Audio device. Has no - * effect if AVC is disabled, or the audio output device for AVC is not playing for STREAM_MUSIC + * effect if AVB is disabled, or the audio output device for AVB is not playing for STREAM_MUSIC */ - void notifyAvcMuteChange(boolean mute) { - if (!isAbsoluteVolumeControlEnabled()) return; + void notifyAvbMuteChange(boolean mute) { + if (!isAbsoluteVolumeBehaviorEnabled()) return; List<AudioDeviceAttributes> streamMusicDevices = getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES); - if (streamMusicDevices.contains(getAvcAudioOutputDevice())) { + if (streamMusicDevices.contains(getAvbAudioOutputDevice())) { int direction = mute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE; int flags = AudioManager.FLAG_ABSOLUTE_VOLUME; if (isTvDevice()) { diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java index c4b98c21b608..6147c10f85d5 100644 --- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java +++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java @@ -172,7 +172,7 @@ final class NewDeviceAction extends HdmiCecFeatureAction { return; } if (mDisplayName == null) { - mDisplayName = HdmiUtils.getDefaultDeviceName(mDeviceLogicalAddress); + mDisplayName = ""; } HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder() .setLogicalAddress(mDeviceLogicalAddress) diff --git a/services/core/java/com/android/server/hdmi/ReportFeaturesMessage.java b/services/core/java/com/android/server/hdmi/ReportFeaturesMessage.java index 80bfb96d15d4..4ef8989cd0b9 100644 --- a/services/core/java/com/android/server/hdmi/ReportFeaturesMessage.java +++ b/services/core/java/com/android/server/hdmi/ReportFeaturesMessage.java @@ -167,7 +167,8 @@ public class ReportFeaturesMessage extends HdmiCecMessage { */ public static int validateAddress(int source, int destination) { return HdmiCecMessageValidator.validateAddress(source, destination, - HdmiCecMessageValidator.DEST_BROADCAST); + HdmiCecMessageValidator.ADDR_NOT_UNREGISTERED, + HdmiCecMessageValidator.ADDR_BROADCAST); } /** diff --git a/services/core/java/com/android/server/hdmi/SendKeyAction.java b/services/core/java/com/android/server/hdmi/SendKeyAction.java index 7daeaf19c657..2703a2c01848 100644 --- a/services/core/java/com/android/server/hdmi/SendKeyAction.java +++ b/services/core/java/com/android/server/hdmi/SendKeyAction.java @@ -172,10 +172,10 @@ final class SendKeyAction extends HdmiCecFeatureAction { } private void sendKeyUp() { - // When using Absolute Volume Control, query audio status after a volume key is released. + // When using absolute volume behavior, query audio status after a volume key is released. // This allows us to notify AudioService of the resulting volume or mute status changes. if (HdmiCecKeycode.isVolumeKeycode(mLastKeycode) - && localDevice().getService().isAbsoluteVolumeControlEnabled()) { + && localDevice().getService().isAbsoluteVolumeBehaviorEnabled()) { sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(), mTargetAddress), __ -> sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus( diff --git a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java index eb3b33d8ca22..bfa8509b4e5f 100644 --- a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java +++ b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java @@ -90,7 +90,7 @@ public class SetAudioVolumeLevelDiscoveryAction extends HdmiCecFeatureAction { } void handleTimerEvent(int state) { - if (updateAvcSupport(FEATURE_SUPPORTED)) { + if (updateSetAudioVolumeLevelSupport(FEATURE_SUPPORTED)) { finishWithCallback(HdmiControlManager.RESULT_SUCCESS); } else { finishWithCallback(HdmiControlManager.RESULT_EXCEPTION); @@ -104,7 +104,7 @@ public class SetAudioVolumeLevelDiscoveryAction extends HdmiCecFeatureAction { * * @return Whether support was successfully updated in the network. */ - private boolean updateAvcSupport( + private boolean updateSetAudioVolumeLevelSupport( @DeviceFeatures.FeatureSupportStatus int setAudioVolumeLevelSupport) { HdmiCecNetwork network = localDevice().mService.getHdmiCecNetwork(); HdmiDeviceInfo currentDeviceInfo = network.getCecDeviceInfo(mTargetAddress); diff --git a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelMessage.java b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelMessage.java index 2ec0e7feec32..9ef96c888a53 100644 --- a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelMessage.java +++ b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelMessage.java @@ -88,7 +88,7 @@ public class SetAudioVolumeLevelMessage extends HdmiCecMessage { */ public static int validateAddress(int source, int destination) { return HdmiCecMessageValidator.validateAddress(source, destination, - HdmiCecMessageValidator.DEST_DIRECT); + HdmiCecMessageValidator.ADDR_NOT_UNREGISTERED, HdmiCecMessageValidator.ADDR_DIRECT); } /** diff --git a/services/core/java/com/android/server/hdmi/VolumeControlAction.java b/services/core/java/com/android/server/hdmi/VolumeControlAction.java index d5761e170d1a..30b188c5dd78 100644 --- a/services/core/java/com/android/server/hdmi/VolumeControlAction.java +++ b/services/core/java/com/android/server/hdmi/VolumeControlAction.java @@ -159,7 +159,7 @@ final class VolumeControlAction extends HdmiCecFeatureAction { // Update audio status if current volume position is edge of volume bar, // i.e max or min volume. - AudioManager audioManager = tv().getService().getAudioManager(); + AudioManagerWrapper audioManager = tv().getService().getAudioManager(); int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); if (mIsVolumeUp) { int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); diff --git a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java index 182aa6fcef02..93f6ff3de3c2 100644 --- a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java +++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java @@ -42,13 +42,10 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.Objects; - /* package */ final class AudioPoliciesDeviceRouteController implements DeviceRouteController { private static final String TAG = "APDeviceRoutesController"; - private static final String DEVICE_ROUTE_ID = "DEVICE_ROUTE"; - @NonNull private final Context mContext; @NonNull @@ -182,10 +179,12 @@ import java.util.Objects; synchronized (this) { return new MediaRoute2Info.Builder( - DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) - .setVolumeHandling(mAudioManager.isVolumeFixed() - ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED - : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) + MediaRoute2Info.ROUTE_ID_DEVICE, + mContext.getResources().getText(name).toString()) + .setVolumeHandling( + mAudioManager.isVolumeFixed() + ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED + : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) .setVolume(mDeviceVolume) .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) .setType(type) diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index cac22a6b8b20..b79991e5d013 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -28,6 +28,7 @@ import static com.android.internal.util.function.pooled.PooledLambda.obtainMessa import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.ActivityThread; import android.content.BroadcastReceiver; @@ -75,8 +76,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -97,6 +100,17 @@ class MediaRouter2ServiceImpl { private static final String KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE = "scanning_package_minimum_importance"; + /** + * Contains the list of bluetooth permissions that are required to do system routing. + * + * <p>Alternatively, apps that hold {@link android.Manifest.permission#MODIFY_AUDIO_ROUTING} are + * also allowed to do system routing. + */ + private static final String[] BLUETOOTH_PERMISSIONS_FOR_SYSTEM_ROUTING = + new String[] { + Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN + }; + private static int sPackageImportanceForScanning = DeviceConfig.getInt( MEDIA_BETTER_TOGETHER_NAMESPACE, /* name */ KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE, @@ -142,6 +156,7 @@ class MediaRouter2ServiceImpl { } }; + @RequiresPermission(Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) /* package */ MediaRouter2ServiceImpl(Context context) { mContext = context; mActivityManager = mContext.getSystemService(ActivityManager.class); @@ -155,12 +170,28 @@ class MediaRouter2ServiceImpl { screenOnOffIntentFilter.addAction(ACTION_SCREEN_OFF); mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter); + mContext.getPackageManager().addOnPermissionsChangeListener(this::onPermissionsChanged); DeviceConfig.addOnPropertiesChangedListener(MEDIA_BETTER_TOGETHER_NAMESPACE, ActivityThread.currentApplication().getMainExecutor(), this::onDeviceConfigChange); } + /** + * Called when there's a change in the permissions of an app. + * + * @param uid The uid of the app whose permissions changed. + */ + private void onPermissionsChanged(int uid) { + synchronized (mLock) { + Optional<RouterRecord> affectedRouter = + mAllRouterRecords.values().stream().filter(it -> it.mUid == uid).findFirst(); + if (affectedRouter.isPresent()) { + affectedRouter.get().maybeUpdateSystemRoutingPermissionLocked(); + } + } + } + // Start of methods that implement MediaRouter2 operations. @NonNull @@ -1511,6 +1542,7 @@ class MediaRouter2ServiceImpl { public final int mPid; public final boolean mHasConfigureWifiDisplayPermission; public final boolean mHasModifyAudioRoutingPermission; + public final AtomicBoolean mHasBluetoothRoutingPermission; public final int mRouterId; public RouteDiscoveryPreference mDiscoveryPreference; @@ -1528,15 +1560,47 @@ class MediaRouter2ServiceImpl { mPid = pid; mHasConfigureWifiDisplayPermission = hasConfigureWifiDisplayPermission; mHasModifyAudioRoutingPermission = hasModifyAudioRoutingPermission; + mHasBluetoothRoutingPermission = new AtomicBoolean(fetchBluetoothPermission()); mRouterId = mNextRouterOrManagerId.getAndIncrement(); } + private boolean fetchBluetoothPermission() { + boolean hasBluetoothRoutingPermission = true; + for (String permission : BLUETOOTH_PERMISSIONS_FOR_SYSTEM_ROUTING) { + hasBluetoothRoutingPermission &= + mContext.checkPermission(permission, mPid, mUid) + == PackageManager.PERMISSION_GRANTED; + } + return hasBluetoothRoutingPermission; + } + /** * Returns whether the corresponding router has permission to query and control system * routes. */ public boolean hasSystemRoutingPermission() { - return mHasModifyAudioRoutingPermission; + return mHasModifyAudioRoutingPermission || mHasBluetoothRoutingPermission.get(); + } + + public void maybeUpdateSystemRoutingPermissionLocked() { + boolean oldSystemRoutingPermissionValue = hasSystemRoutingPermission(); + mHasBluetoothRoutingPermission.set(fetchBluetoothPermission()); + boolean newSystemRoutingPermissionValue = hasSystemRoutingPermission(); + if (oldSystemRoutingPermissionValue != newSystemRoutingPermissionValue) { + Map<String, MediaRoute2Info> routesToReport = + newSystemRoutingPermissionValue + ? mUserRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters + : mUserRecord.mHandler.mLastNotifiedRoutesToNonPrivilegedRouters; + notifyRoutesUpdated(routesToReport.values().stream().toList()); + + List<RoutingSessionInfo> sessionInfos = + mUserRecord.mHandler.mSystemProvider.getSessionInfos(); + RoutingSessionInfo systemSessionToReport = + newSystemRoutingPermissionValue && !sessionInfos.isEmpty() + ? sessionInfos.get(0) + : mUserRecord.mHandler.mSystemProvider.getDefaultSessionInfo(); + notifySessionInfoChanged(systemSessionToReport); + } } public void dispose() { @@ -1559,6 +1623,14 @@ class MediaRouter2ServiceImpl { pw.println(indent + "mPid=" + mPid); pw.println(indent + "mHasConfigureWifiDisplayPermission=" + mHasConfigureWifiDisplayPermission); + pw.println( + indent + + "mHasModifyAudioRoutingPermission=" + + mHasModifyAudioRoutingPermission); + pw.println( + indent + + "mHasBluetoothRoutingPermission=" + + mHasBluetoothRoutingPermission.get()); pw.println(indent + "hasSystemRoutingPermission=" + hasSystemRoutingPermission()); pw.println(indent + "mRouterId=" + mRouterId); @@ -1581,6 +1653,19 @@ class MediaRouter2ServiceImpl { } /** + * Sends the corresponding router an update for the given session. + * + * <p>Note: These updates are not directly visible to the app. + */ + public void notifySessionInfoChanged(RoutingSessionInfo sessionInfo) { + try { + mRouter.notifySessionInfoChanged(sessionInfo); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify session info changed. Router probably died.", ex); + } + } + + /** * Returns a filtered copy of {@code routes} that contains only the routes that are {@link * MediaRoute2Info#isVisibleTo visible} to the router corresponding to this record. */ @@ -2471,11 +2556,7 @@ class MediaRouter2ServiceImpl { @NonNull List<RouterRecord> routerRecords, @NonNull RoutingSessionInfo sessionInfo) { for (RouterRecord routerRecord : routerRecords) { - try { - routerRecord.mRouter.notifySessionInfoChanged(sessionInfo); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify session info changed. Router probably died.", ex); - } + routerRecord.notifySessionInfoChanged(sessionInfo); } } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 4d134b69811d..b440e8815c16 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -18,6 +18,7 @@ package com.android.server.media; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -137,6 +138,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub private final String mDefaultAudioRouteId; private final String mBluetoothA2dpRouteId; + @RequiresPermission(Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) public MediaRouterService(Context context) { mService2 = new MediaRouter2ServiceImpl(context); mContext = context; diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 02b705344cb9..47f648547a88 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -33,6 +33,8 @@ import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -74,6 +76,7 @@ import android.util.Log; import android.view.KeyEvent; import com.android.server.LocalServices; +import com.android.server.uri.UriGrantsManagerInternal; import java.io.PrintWriter; import java.util.ArrayList; @@ -101,6 +104,10 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR static final long THROW_FOR_INVALID_BROADCAST_RECEIVER = 270049379L; private static final String TAG = "MediaSessionRecord"; + private static final String[] ART_URIS = new String[] { + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI}; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** @@ -154,6 +161,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR private final SessionStub mSession; private final SessionCb mSessionCb; private final MediaSessionService mService; + private final UriGrantsManagerInternal mUgmInternal; private final Context mContext; private final boolean mVolumeAdjustmentForRemoteGroupSessions; @@ -215,6 +223,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); mAudioAttrs = DEFAULT_ATTRIBUTES; mPolicies = policies; + mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class); mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean( com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions); @@ -1080,21 +1089,45 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR public void setMetadata(MediaMetadata metadata, long duration, String metadataDescription) throws RemoteException { synchronized (mLock) { - MediaMetadata temp = metadata == null ? null : new MediaMetadata.Builder(metadata) - .build(); - // This is to guarantee that the underlying bundle is unparceled - // before we set it to prevent concurrent reads from throwing an - // exception - if (temp != null) { - temp.size(); - } - mMetadata = temp; mDuration = duration; mMetadataDescription = metadataDescription; + mMetadata = sanitizeMediaMetadata(metadata); } mHandler.post(MessageHandler.MSG_UPDATE_METADATA); } + private MediaMetadata sanitizeMediaMetadata(MediaMetadata metadata) { + if (metadata == null) { + return null; + } + MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(metadata); + for (String key: ART_URIS) { + String uriString = metadata.getString(key); + if (TextUtils.isEmpty(uriString)) { + continue; + } + Uri uri = Uri.parse(uriString); + if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + continue; + } + try { + mUgmInternal.checkGrantUriPermission(getUid(), + getPackageName(), + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, getUserId())); + } catch (SecurityException e) { + metadataBuilder.putString(key, null); + } + } + MediaMetadata sanitizedMetadata = metadataBuilder.build(); + // sanitizedMetadata.size() guarantees that the underlying bundle is unparceled + // before we set it to prevent concurrent reads from throwing an + // exception + sanitizedMetadata.size(); + return sanitizedMetadata; + } + @Override public void setPlaybackState(PlaybackState state) throws RemoteException { int oldState = mPlaybackState == null diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 6d2d2e405ab9..426bc5eed051 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -55,7 +55,6 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { SystemMediaRoute2Provider.class.getPackage().getName(), SystemMediaRoute2Provider.class.getName()); - static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE"; static final String SYSTEM_SESSION_ID = "SYSTEM_SESSION"; private final AudioManager mAudioManager; @@ -170,7 +169,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { Bundle sessionHints) { // Assume a router without MODIFY_AUDIO_ROUTING permission can't request with // a route ID different from the default route ID. The service should've filtered. - if (TextUtils.equals(routeId, DEFAULT_ROUTE_ID)) { + if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { mCallback.onSessionCreated(this, requestId, mDefaultSessionInfo); return; } @@ -213,7 +212,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { @Override public void transferToRoute(long requestId, String sessionId, String routeId) { - if (TextUtils.equals(routeId, DEFAULT_ROUTE_ID)) { + if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { // The currently selected route is the default route. return; } @@ -326,10 +325,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { builder.addTransferableRoute(deviceRoute.getId()); } mSelectedRouteId = selectedRoute.getId(); - mDefaultRoute = new MediaRoute2Info.Builder(DEFAULT_ROUTE_ID, selectedRoute) - .setSystemRoute(true) - .setProviderId(mUniqueId) - .build(); + mDefaultRoute = + new MediaRoute2Info.Builder(MediaRoute2Info.ROUTE_ID_DEFAULT, selectedRoute) + .setSystemRoute(true) + .setProviderId(mUniqueId) + .build(); builder.addSelectedRoute(mSelectedRouteId); for (MediaRoute2Info route : mBluetoothRouteController.getTransferableRoutes()) { @@ -363,12 +363,13 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } mSessionInfos.clear(); mSessionInfos.add(newSessionInfo); - mDefaultSessionInfo = new RoutingSessionInfo.Builder( - SYSTEM_SESSION_ID, "" /* clientPackageName */) - .setProviderId(mUniqueId) - .setSystemSession(true) - .addSelectedRoute(DEFAULT_ROUTE_ID) - .build(); + mDefaultSessionInfo = + new RoutingSessionInfo.Builder( + SYSTEM_SESSION_ID, "" /* clientPackageName */) + .setProviderId(mUniqueId) + .setSystemSession(true) + .addSelectedRoute(MediaRoute2Info.ROUTE_ID_DEFAULT) + .build(); return true; } } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 377b8cf1230e..38631c8ad1cf 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -406,7 +406,7 @@ public final class MediaProjectionManagerService extends SystemService return; } if (mProjectionGrant.mSession == null - || !mProjectionGrant.mSession.isWaitingToRecord()) { + || !mProjectionGrant.mSession.isWaitingForConsent()) { Slog.w(TAG, "Reusing token: Ignore consent result " + consentResult + " if not waiting for the result."); return; @@ -445,7 +445,7 @@ public final class MediaProjectionManagerService extends SystemService */ private void setReviewedConsentSessionLocked(@Nullable ContentRecordingSession session) { if (session != null) { - session.setWaitingToRecord(false); + session.setWaitingForConsent(false); session.setVirtualDisplayId(mProjectionGrant.mVirtualDisplayId); } @@ -490,7 +490,7 @@ public final class MediaProjectionManagerService extends SystemService // Supposedly the package has re-used the user's consent; confirm the provided details // against the current projection token before re-using the current projection. if (mProjectionGrant == null || mProjectionGrant.mSession == null - || !mProjectionGrant.mSession.isWaitingToRecord()) { + || !mProjectionGrant.mSession.isWaitingForConsent()) { Slog.e(TAG, "Reusing token: Not possible to reuse the current projection " + "instance"); return null; diff --git a/services/core/java/com/android/server/net/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java index acfa66595afa..e5ffa7e15f28 100644 --- a/services/core/java/com/android/server/net/NetworkManagementService.java +++ b/services/core/java/com/android/server/net/NetworkManagementService.java @@ -53,7 +53,6 @@ import android.net.NetworkPolicyManager; import android.net.NetworkStack; import android.net.NetworkStats; import android.net.RouteInfo; -import android.net.UidRangeParcel; import android.net.util.NetdService; import android.os.BatteryStats; import android.os.Binder; @@ -97,7 +96,6 @@ import java.io.PrintWriter; import java.net.InetAddress; import java.net.InterfaceAddress; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -1351,70 +1349,6 @@ public class NetworkManagementService extends INetworkManagementService.Stub { } } - private void closeSocketsForFirewallChainLocked(int chain, String chainName) { - // UID ranges to close sockets on. - UidRangeParcel[] ranges; - // UID ranges whose sockets we won't touch. - int[] exemptUids; - - int numUids = 0; - if (DBG) Slog.d(TAG, "Closing sockets after enabling chain " + chainName); - if (getFirewallType(chain) == FIREWALL_ALLOWLIST) { - // Close all sockets on all non-system UIDs... - ranges = new UidRangeParcel[] { - // TODO: is there a better way of finding all existing users? If so, we could - // specify their ranges here. - new UidRangeParcel(Process.FIRST_APPLICATION_UID, Integer.MAX_VALUE), - }; - // ... except for the UIDs that have allow rules. - synchronized (mRulesLock) { - final SparseIntArray rules = getUidFirewallRulesLR(chain); - exemptUids = new int[rules.size()]; - for (int i = 0; i < exemptUids.length; i++) { - if (rules.valueAt(i) == FIREWALL_RULE_ALLOW) { - exemptUids[numUids] = rules.keyAt(i); - numUids++; - } - } - } - // Normally, allowlist chains only contain deny rules, so numUids == exemptUids.length. - // But the code does not guarantee this in any way, and at least in one case - if we add - // a UID rule to the firewall, and then disable the firewall - the chains can contain - // the wrong type of rule. In this case, don't close connections that we shouldn't. - // - // TODO: tighten up this code by ensuring we never set the wrong type of rule, and - // fix setFirewallEnabled to grab mQuotaLock and clear rules. - if (numUids != exemptUids.length) { - exemptUids = Arrays.copyOf(exemptUids, numUids); - } - } else { - // Close sockets for every UID that has a deny rule... - synchronized (mRulesLock) { - final SparseIntArray rules = getUidFirewallRulesLR(chain); - ranges = new UidRangeParcel[rules.size()]; - for (int i = 0; i < ranges.length; i++) { - if (rules.valueAt(i) == FIREWALL_RULE_DENY) { - int uid = rules.keyAt(i); - ranges[numUids] = new UidRangeParcel(uid, uid); - numUids++; - } - } - } - // As above; usually numUids == ranges.length, but not always. - if (numUids != ranges.length) { - ranges = Arrays.copyOf(ranges, numUids); - } - // ... with no exceptions. - exemptUids = new int[0]; - } - - try { - mNetdService.socketDestroy(ranges, exemptUids); - } catch(RemoteException | ServiceSpecificException e) { - Slog.e(TAG, "Error closing sockets after enabling chain " + chainName + ": " + e); - } - } - @Override public void setFirewallChainEnabled(int chain, boolean enable) { enforceSystemUid(); @@ -1439,14 +1373,6 @@ public class NetworkManagementService extends INetworkManagementService.Stub { } catch (RuntimeException e) { throw new IllegalStateException(e); } - - // Close any sockets that were opened by the affected UIDs. This has to be done after - // disabling network connectivity, in case they react to the socket close by reopening - // the connection and race with the iptables commands that enable the firewall. All - // allowlist and denylist chains allow RSTs through. - if (enable) { - closeSocketsForFirewallChainLocked(chain, chainName); - } } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 31074c1039e6..e56eba659d37 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1652,7 +1652,8 @@ public class NotificationManagerService extends SystemService { if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) { // update system notification channels SystemNotificationChannels.createAll(context); - mZenModeHelper.updateDefaultZenRules(); + mZenModeHelper.updateDefaultZenRules(Binder.getCallingUid(), + isCallerIsSystemOrSystemUi()); mPreferencesHelper.onLocaleChanged(context, ActivityManager.getCurrentUser()); } } @@ -2280,7 +2281,8 @@ public class NotificationManagerService extends SystemService { mRankingHandler = rankingHandler; mConditionProviders = conditionProviders; mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders, - new SysUiStatsEvent.BuilderFactory()); + new SysUiStatsEvent.BuilderFactory(), flagResolver, + new ZenModeEventLogger(mPackageManagerClient)); mZenModeHelper.addCallback(new ZenModeHelper.Callback() { @Override public void onConfigChanged() { @@ -2862,7 +2864,8 @@ public class NotificationManagerService extends SystemService { final NotificationChannel preUpdate = mPreferencesHelper.getNotificationChannel(pkg, uid, channel.getId(), true); - mPreferencesHelper.updateNotificationChannel(pkg, uid, channel, true); + mPreferencesHelper.updateNotificationChannel(pkg, uid, channel, true, + Binder.getCallingUid(), isCallerIsSystemOrSystemUi()); if (mPreferencesHelper.onlyHasDefaultChannel(pkg, uid)) { mPermissionHelper.setNotificationPermission(pkg, UserHandle.getUserId(uid), channel.getImportance() != IMPORTANCE_NONE, true); @@ -2910,7 +2913,7 @@ public class NotificationManagerService extends SystemService { final NotificationChannelGroup preUpdate = mPreferencesHelper.getNotificationChannelGroup(group.getId(), pkg, uid); mPreferencesHelper.createNotificationChannelGroup(pkg, uid, group, - fromApp); + fromApp, Binder.getCallingUid(), isCallerIsSystemOrSystemUi()); if (!fromApp) { maybeNotifyChannelGroupOwner(pkg, uid, preUpdate, group); } @@ -3875,7 +3878,8 @@ public class NotificationManagerService extends SystemService { needsPolicyFileChange = mPreferencesHelper.createNotificationChannel(pkg, uid, channel, true /* fromTargetApp */, mConditionProviders.isPackageOrComponentAllowed( - pkg, UserHandle.getUserId(uid))); + pkg, UserHandle.getUserId(uid)), Binder.getCallingUid(), + isCallerIsSystemOrSystemUi()); if (needsPolicyFileChange) { mListeners.notifyNotificationChannelChanged(pkg, UserHandle.getUserHandleForUid(uid), @@ -4011,6 +4015,7 @@ public class NotificationManagerService extends SystemService { public void deleteNotificationChannel(String pkg, String channelId) { checkCallerIsSystemOrSameApp(pkg); final int callingUid = Binder.getCallingUid(); + final boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi(); final int callingUser = UserHandle.getUserId(callingUid); if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) { throw new IllegalArgumentException("Cannot delete default channel"); @@ -4020,7 +4025,7 @@ public class NotificationManagerService extends SystemService { cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true, callingUser, REASON_CHANNEL_REMOVED, null); boolean previouslyExisted = mPreferencesHelper.deleteNotificationChannel( - pkg, callingUid, channelId); + pkg, callingUid, channelId, callingUid, isSystemOrSystemUi); if (previouslyExisted) { // Remove from both recent notification archive and notification history mArchive.removeChannelNotifications(pkg, callingUser, channelId); @@ -4053,6 +4058,7 @@ public class NotificationManagerService extends SystemService { checkCallerIsSystemOrSameApp(pkg); final int callingUid = Binder.getCallingUid(); + final boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi(); NotificationChannelGroup groupToDelete = mPreferencesHelper.getNotificationChannelGroupWithChannels( pkg, callingUid, groupId, false); @@ -4066,7 +4072,8 @@ public class NotificationManagerService extends SystemService { enforceDeletingChannelHasNoUserInitiatedJob(pkg, userId, channelId); } List<NotificationChannel> deletedChannels = - mPreferencesHelper.deleteNotificationChannelGroup(pkg, callingUid, groupId); + mPreferencesHelper.deleteNotificationChannelGroup(pkg, callingUid, groupId, + callingUid, isSystemOrSystemUi); for (int i = 0; i < deletedChannels.size(); i++) { final NotificationChannel deletedChannel = deletedChannels.get(i); cancelAllNotificationsInt(MY_UID, MY_PID, pkg, deletedChannel.getId(), 0, 0, @@ -4964,11 +4971,14 @@ public class NotificationManagerService extends SystemService { @Override public void requestInterruptionFilterFromListener(INotificationListener token, int interruptionFilter) throws RemoteException { + final int callingUid = Binder.getCallingUid(); + final boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi(); final long identity = Binder.clearCallingIdentity(); try { synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - mZenModeHelper.requestFromListener(info.component, interruptionFilter); + mZenModeHelper.requestFromListener(info.component, interruptionFilter, + callingUid, isSystemOrSystemUi); updateInterruptionFilterLocked(); } } finally { @@ -5008,9 +5018,12 @@ public class NotificationManagerService extends SystemService { @Override public void setZenMode(int mode, Uri conditionId, String reason) throws RemoteException { enforceSystemOrSystemUI("INotificationManager.setZenMode"); + final int callingUid = Binder.getCallingUid(); + final boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi(); final long identity = Binder.clearCallingIdentity(); try { - mZenModeHelper.setManualZenMode(mode, conditionId, null, reason); + mZenModeHelper.setManualZenMode(mode, conditionId, null, reason, callingUid, + isSystemOrSystemUi); } finally { Binder.restoreCallingIdentity(identity); } @@ -5057,7 +5070,8 @@ public class NotificationManagerService extends SystemService { } return mZenModeHelper.addAutomaticZenRule(rulePkg, automaticZenRule, - "addAutomaticZenRule"); + "addAutomaticZenRule", Binder.getCallingUid(), + isCallerIsSystemOrSystemUi()); } @Override @@ -5074,7 +5088,8 @@ public class NotificationManagerService extends SystemService { enforcePolicyAccess(Binder.getCallingUid(), "updateAutomaticZenRule"); return mZenModeHelper.updateAutomaticZenRule(id, automaticZenRule, - "updateAutomaticZenRule"); + "updateAutomaticZenRule", Binder.getCallingUid(), + isCallerIsSystemOrSystemUi()); } @Override @@ -5083,7 +5098,8 @@ public class NotificationManagerService extends SystemService { // Verify that they can modify zen rules. enforcePolicyAccess(Binder.getCallingUid(), "removeAutomaticZenRule"); - return mZenModeHelper.removeAutomaticZenRule(id, "removeAutomaticZenRule"); + return mZenModeHelper.removeAutomaticZenRule(id, "removeAutomaticZenRule", + Binder.getCallingUid(), isCallerIsSystemOrSystemUi()); } @Override @@ -5092,7 +5108,8 @@ public class NotificationManagerService extends SystemService { enforceSystemOrSystemUI("removeAutomaticZenRules"); return mZenModeHelper.removeAutomaticZenRules(packageName, - packageName + "|removeAutomaticZenRules"); + packageName + "|removeAutomaticZenRules", Binder.getCallingUid(), + isCallerIsSystemOrSystemUi()); } @Override @@ -5110,7 +5127,8 @@ public class NotificationManagerService extends SystemService { enforcePolicyAccess(Binder.getCallingUid(), "setAutomaticZenRuleState"); - mZenModeHelper.setAutomaticZenRuleState(id, condition); + mZenModeHelper.setAutomaticZenRuleState(id, condition, Binder.getCallingUid(), + isCallerIsSystemOrSystemUi()); } @Override @@ -5118,9 +5136,12 @@ public class NotificationManagerService extends SystemService { enforcePolicyAccess(pkg, "setInterruptionFilter"); final int zen = NotificationManager.zenModeFromInterruptionFilter(filter, -1); if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter); + final int callingUid = Binder.getCallingUid(); + final boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi(); final long identity = Binder.clearCallingIdentity(); try { - mZenModeHelper.setManualZenMode(zen, null, pkg, "setInterruptionFilter"); + mZenModeHelper.setManualZenMode(zen, null, pkg, "setInterruptionFilter", + callingUid, isSystemOrSystemUi); } finally { Binder.restoreCallingIdentity(identity); } @@ -5421,6 +5442,7 @@ public class NotificationManagerService extends SystemService { public void setNotificationPolicy(String pkg, Policy policy) { enforcePolicyAccess(pkg, "setNotificationPolicy"); int callingUid = Binder.getCallingUid(); + boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi(); final long identity = Binder.clearCallingIdentity(); try { final ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkg, @@ -5460,7 +5482,7 @@ public class NotificationManagerService extends SystemService { policy.priorityCallSenders, policy.priorityMessageSenders, newVisualEffects, policy.priorityConversationSenders); ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, policy); - mZenModeHelper.setNotificationPolicy(policy); + mZenModeHelper.setNotificationPolicy(policy, callingUid, isSystemOrSystemUi); } catch (RemoteException e) { } finally { Binder.restoreCallingIdentity(identity); @@ -6698,7 +6720,8 @@ public class NotificationManagerService extends SystemService { channel.setUserVisibleTaskShown(true); } mPreferencesHelper.updateNotificationChannel( - pkg, notificationUid, channel, false); + pkg, notificationUid, channel, false, callingUid, + isCallerIsSystemOrSystemUi()); r.updateNotificationChannel(channel); } else if (!channel.isUserVisibleTaskShown() && !TextUtils.isEmpty(channelId) && !NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) { @@ -6771,7 +6794,8 @@ public class NotificationManagerService extends SystemService { mHistoryManager.deleteConversations(pkg, uid, shortcuts); List<String> deletedChannelIds = - mPreferencesHelper.deleteConversations(pkg, uid, shortcuts); + mPreferencesHelper.deleteConversations(pkg, uid, shortcuts, + /* callingUid */ Process.SYSTEM_UID, /* is system */ true); for (String channelId : deletedChannelIds) { cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channelId, 0, 0, true, UserHandle.getUserId(uid), REASON_CHANNEL_REMOVED, @@ -10000,7 +10024,8 @@ public class NotificationManagerService extends SystemService { return isUidSystemOrPhone(Binder.getCallingUid()); } - private boolean isCallerIsSystemOrSystemUi() { + @VisibleForTesting + protected boolean isCallerIsSystemOrSystemUi() { if (isCallerSystemOrPhone()) { return true; } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 2460ce56f165..4399a3ca46c5 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -51,6 +51,7 @@ import android.metrics.LogMaker; import android.net.Uri; import android.os.Binder; import android.os.Build; +import android.os.Process; import android.os.UserHandle; import android.provider.Settings; import android.service.notification.ConversationChannelWrapper; @@ -217,7 +218,7 @@ public class PreferencesHelper implements RankingConfig { updateBadgingEnabled(); updateBubblesEnabled(); updateMediaNotificationFilteringEnabled(); - syncChannelsBypassingDnd(); + syncChannelsBypassingDnd(Process.SYSTEM_UID, true); // init comes from system } public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId) @@ -834,7 +835,7 @@ public class PreferencesHelper implements RankingConfig { @Override public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group, - boolean fromTargetApp) { + boolean fromTargetApp, int callingUid, boolean fromSystemOrSystemUi) { Objects.requireNonNull(pkg); Objects.requireNonNull(group); Objects.requireNonNull(group.getId()); @@ -880,13 +881,14 @@ public class PreferencesHelper implements RankingConfig { r.groups.put(group.getId(), group); } if (needsDndChange) { - updateChannelsBypassingDnd(); + updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } } @Override public boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel, - boolean fromTargetApp, boolean hasDndAccess) { + boolean fromTargetApp, boolean hasDndAccess, int callingUid, + boolean fromSystemOrSystemUi) { Objects.requireNonNull(pkg); Objects.requireNonNull(channel); Objects.requireNonNull(channel.getId()); @@ -1027,7 +1029,7 @@ public class PreferencesHelper implements RankingConfig { } if (needsDndChange) { - updateChannelsBypassingDnd(); + updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } return needsPolicyFileChange; @@ -1056,7 +1058,7 @@ public class PreferencesHelper implements RankingConfig { @Override public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel, - boolean fromUser) { + boolean fromUser, int callingUid, boolean fromSystemOrSystemUi) { Objects.requireNonNull(updatedChannel); Objects.requireNonNull(updatedChannel.getId()); boolean changed = false; @@ -1112,7 +1114,7 @@ public class PreferencesHelper implements RankingConfig { } } if (needsDndChange) { - updateChannelsBypassingDnd(); + updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } if (changed) { updateConfig(); @@ -1188,7 +1190,8 @@ public class PreferencesHelper implements RankingConfig { } @Override - public boolean deleteNotificationChannel(String pkg, int uid, String channelId) { + public boolean deleteNotificationChannel(String pkg, int uid, String channelId, + int callingUid, boolean fromSystemOrSystemUi) { boolean deletedChannel = false; boolean channelBypassedDnd = false; synchronized (mPackagePreferences) { @@ -1203,7 +1206,7 @@ public class PreferencesHelper implements RankingConfig { } } if (channelBypassedDnd) { - updateChannelsBypassingDnd(); + updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } return deletedChannel; } @@ -1394,7 +1397,7 @@ public class PreferencesHelper implements RankingConfig { } public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid, - String groupId) { + String groupId, int callingUid, boolean fromSystemOrSystemUi) { List<NotificationChannel> deletedChannels = new ArrayList<>(); boolean groupBypassedDnd = false; synchronized (mPackagePreferences) { @@ -1420,7 +1423,7 @@ public class PreferencesHelper implements RankingConfig { } } if (groupBypassedDnd) { - updateChannelsBypassingDnd(); + updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } return deletedChannels; } @@ -1543,7 +1546,7 @@ public class PreferencesHelper implements RankingConfig { } public @NonNull List<String> deleteConversations(String pkg, int uid, - Set<String> conversationIds) { + Set<String> conversationIds, int callingUid, boolean fromSystemOrSystemUi) { List<String> deletedChannelIds = new ArrayList<>(); synchronized (mPackagePreferences) { PackagePreferences r = getPackagePreferencesLocked(pkg, uid); @@ -1568,7 +1571,7 @@ public class PreferencesHelper implements RankingConfig { } } if (!deletedChannelIds.isEmpty() && mAreChannelsBypassingDnd) { - updateChannelsBypassingDnd(); + updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } return deletedChannelIds; } @@ -1673,18 +1676,18 @@ public class PreferencesHelper implements RankingConfig { * Syncs {@link #mAreChannelsBypassingDnd} with the current user's notification policy before * updating */ - private void syncChannelsBypassingDnd() { + private void syncChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) { mAreChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) == 1; - updateChannelsBypassingDnd(); + updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } /** * Updates the user's NotificationPolicy based on whether the current userId * has channels bypassing DND */ - private void updateChannelsBypassingDnd() { + private void updateChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) { ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>(); final int currentUserId = getCurrentUser(); @@ -1714,7 +1717,7 @@ public class PreferencesHelper implements RankingConfig { boolean haveBypassingApps = candidatePkgs.size() > 0; if (mAreChannelsBypassingDnd != haveBypassingApps) { mAreChannelsBypassingDnd = haveBypassingApps; - updateZenPolicy(mAreChannelsBypassingDnd); + updateZenPolicy(mAreChannelsBypassingDnd, callingUid, fromSystemOrSystemUi); } } @@ -1739,14 +1742,15 @@ public class PreferencesHelper implements RankingConfig { return true; } - public void updateZenPolicy(boolean areChannelsBypassingDnd) { + public void updateZenPolicy(boolean areChannelsBypassingDnd, int callingUid, + boolean fromSystemOrSystemUi) { NotificationManager.Policy policy = mZenModeHelper.getNotificationPolicy(); mZenModeHelper.setNotificationPolicy(new NotificationManager.Policy( policy.priorityCategories, policy.priorityCallSenders, policy.priorityMessageSenders, policy.suppressedVisualEffects, (areChannelsBypassingDnd ? NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND : 0), - policy.priorityConversationSenders)); + policy.priorityConversationSenders), callingUid, fromSystemOrSystemUi); } public boolean areChannelsBypassingDnd() { diff --git a/services/core/java/com/android/server/notification/RankingConfig.java b/services/core/java/com/android/server/notification/RankingConfig.java index 3e9d90c440b6..fec359198e88 100644 --- a/services/core/java/com/android/server/notification/RankingConfig.java +++ b/services/core/java/com/android/server/notification/RankingConfig.java @@ -39,19 +39,21 @@ public interface RankingConfig { Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg, int uid); void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group, - boolean fromTargetApp); + boolean fromTargetApp, int callingUid, boolean isSystemOrSystemUi); ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty); boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel, - boolean fromTargetApp, boolean hasDndAccess); - void updateNotificationChannel(String pkg, int uid, NotificationChannel channel, - boolean fromUser); + boolean fromTargetApp, boolean hasDndAccess, int callingUid, + boolean isSystemOrSystemUi); + void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel, + boolean fromUser, int callingUid, boolean fromSystemOrSystemUi); NotificationChannel getNotificationChannel(String pkg, int uid, String channelId, boolean includeDeleted); NotificationChannel getConversationNotificationChannel(String pkg, int uid, String channelId, String conversationId, boolean returnParentIfNoConversationChannel, boolean includeDeleted); - boolean deleteNotificationChannel(String pkg, int uid, String channelId); + boolean deleteNotificationChannel(String pkg, int uid, String channelId, + int callingUid, boolean fromSystemOrSystemUi); void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId); void permanentlyDeleteNotificationChannels(String pkg, int uid); ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java index 50b4d438984c..6ecd799bf8e6 100644 --- a/services/core/java/com/android/server/notification/ZenModeConditions.java +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -18,6 +18,8 @@ package com.android.server.notification; import android.content.ComponentName; import android.net.Uri; +import android.os.Binder; +import android.os.Process; import android.service.notification.Condition; import android.service.notification.IConditionProvider; import android.service.notification.ZenModeConfig; @@ -108,7 +110,9 @@ public class ZenModeConditions implements ConditionProviders.Callback { @Override public void onServiceAdded(ComponentName component) { if (DEBUG) Log.d(TAG, "onServiceAdded " + component); - mHelper.setConfig(mHelper.getConfig(), component, "zmc.onServiceAdded:" + component); + final int callingUid = Binder.getCallingUid(); + mHelper.setConfig(mHelper.getConfig(), component, "zmc.onServiceAdded:" + component, + callingUid, callingUid == Process.SYSTEM_UID); } @Override @@ -116,7 +120,9 @@ public class ZenModeConditions implements ConditionProviders.Callback { if (DEBUG) Log.d(TAG, "onConditionChanged " + id + " " + condition); ZenModeConfig config = mHelper.getConfig(); if (config == null) return; - mHelper.setAutomaticZenRuleState(id, condition); + final int callingUid = Binder.getCallingUid(); + mHelper.setAutomaticZenRuleState(id, condition, callingUid, + callingUid == Process.SYSTEM_UID); } // Only valid for CPS backed rules diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java new file mode 100644 index 000000000000..1641d4a6ed46 --- /dev/null +++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java @@ -0,0 +1,611 @@ +/* + * 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.server.notification; + +import static android.app.NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND; +import static android.provider.Settings.Global.ZEN_MODE_OFF; +import static android.service.notification.NotificationServiceProto.RULE_TYPE_AUTOMATIC; +import static android.service.notification.NotificationServiceProto.RULE_TYPE_MANUAL; +import static android.service.notification.NotificationServiceProto.RULE_TYPE_UNKNOWN; + +import android.annotation.NonNull; +import android.app.NotificationManager; +import android.content.pm.PackageManager; +import android.os.Process; +import android.service.notification.DNDPolicyProto; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeDiff; +import android.service.notification.ZenPolicy; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.util.FrameworkStatsLog; + +import java.io.ByteArrayOutputStream; +import java.util.Objects; + +/** + * Class for writing DNDStateChanged atoms to the statsd log. + * Use ZenModeEventLoggerFake for testing. + */ +class ZenModeEventLogger { + private static final String TAG = "ZenModeEventLogger"; + + // Placeholder int for unknown zen mode, to distinguish from "off". + static final int ZEN_MODE_UNKNOWN = -1; + + // Object for tracking config changes and policy changes associated with an overall zen + // mode change. + ZenModeEventLogger.ZenStateChanges mChangeState = new ZenModeEventLogger.ZenStateChanges(); + + private PackageManager mPm; + + ZenModeEventLogger(PackageManager pm) { + mPm = pm; + } + + /** + * Enum used to log the type of DND state changed events. + * These use UiEvent IDs for ease of integrating with other UiEvents. + */ + enum ZenStateChangedEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "DND was turned on; may additionally include policy change.") + DND_TURNED_ON(1368), + @UiEvent(doc = "DND was turned off; may additionally include policy change.") + DND_TURNED_OFF(1369), + @UiEvent(doc = "DND policy was changed but the zen mode did not change.") + DND_POLICY_CHANGED(1370), + @UiEvent(doc = "Change in DND automatic rules active, without changing mode or policy.") + DND_ACTIVE_RULES_CHANGED(1371); + + private final int mId; + + ZenStateChangedEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + + /** + * Potentially log a zen mode change if the provided config and policy changes warrant it. + * + * @param prevInfo ZenModeInfo (zen mode setting, config, policy) prior to this change + * @param newInfo ZenModeInfo after this change takes effect + * @param callingUid the calling UID associated with the change; may be used to attribute the + * change to a particular package or determine if this is a user action + * @param fromSystemOrSystemUi whether the calling UID is either system UID or system UI + */ + public final void maybeLogZenChange(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid, + boolean fromSystemOrSystemUi) { + mChangeState.init(prevInfo, newInfo, callingUid, fromSystemOrSystemUi); + if (mChangeState.shouldLogChanges()) { + maybeReassignCallingUid(); + logChanges(); + } + + // clear out the state for a fresh start next time + mChangeState = new ZenModeEventLogger.ZenStateChanges(); + } + + /** + * Reassign callingUid in mChangeState if we have more specific information that warrants it + * (for instance, if the change is automatic and due to an automatic rule change). + */ + private void maybeReassignCallingUid() { + int userId = Process.INVALID_UID; + String packageName = null; + + // For a manual rule, we consider reassigning the UID only when the call seems to come from + // the system and there is a non-null enabler in the new config. + // We don't consider the manual rule in the old config because if a manual rule is turning + // off with a call from system, that could easily be a user action to explicitly turn it off + if (mChangeState.getChangedRuleType() == RULE_TYPE_MANUAL) { + if (!mChangeState.mFromSystemOrSystemUi + || mChangeState.getNewManualRuleEnabler() == null) { + return; + } + packageName = mChangeState.getNewManualRuleEnabler(); + userId = mChangeState.mNewConfig.user; // mNewConfig must not be null if enabler exists + } + + // The conditions where we should consider reassigning UID for an automatic rule change: + // - we've determined it's not a user action + // - our current best guess is that the calling uid is system/sysui + if (mChangeState.getChangedRuleType() == RULE_TYPE_AUTOMATIC) { + if (mChangeState.getIsUserAction() || !mChangeState.mFromSystemOrSystemUi) { + return; + } + + // Only try to get the package UID if there's exactly one changed automatic rule. If + // there's more than one that changes simultaneously, this is likely to be a boot and + // we can leave it attributed to system. + ArrayMap<String, ZenModeDiff.RuleDiff> changedRules = + mChangeState.getChangedAutomaticRules(); + if (changedRules.size() != 1) { + return; + } + Pair<String, Integer> ruleInfo = mChangeState.getRulePackageAndUser( + changedRules.keyAt(0), + changedRules.valueAt(0)); + + if (ruleInfo == null || ruleInfo.first.equals(ZenModeConfig.SYSTEM_AUTHORITY)) { + // leave system rules as-is + return; + } + + packageName = ruleInfo.first; + userId = ruleInfo.second; + } + + if (userId == Process.INVALID_UID || packageName == null) { + // haven't found anything to look up. + return; + } + + try { + int uid = mPm.getPackageUidAsUser(packageName, userId); + mChangeState.mCallingUid = uid; + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "unable to find package name " + packageName + " " + userId); + } + } + + /** + * Actually log all changes stored in the current change state to statsd output. This method + * should not be used directly by callers; visible for override by subclasses. + */ + void logChanges() { + FrameworkStatsLog.write(FrameworkStatsLog.DND_STATE_CHANGED, + /* int32 event_id = 1 */ mChangeState.getEventId().getId(), + /* android.stats.dnd.ZenMode new_mode = 2 */ mChangeState.mNewZenMode, + /* android.stats.dnd.ZenMode previous_mode = 3 */ mChangeState.mPrevZenMode, + /* android.stats.dnd.RuleType rule_type = 4 */ mChangeState.getChangedRuleType(), + /* int32 num_rules_active = 5 */ mChangeState.getNumRulesActive(), + /* bool user_action = 6 */ mChangeState.getIsUserAction(), + /* int32 package_uid = 7 */ mChangeState.getPackageUid(), + /* DNDPolicyProto current_policy = 8 */ mChangeState.getDNDPolicyProto(), + /* bool are_channels_bypassing = 9 */ mChangeState.getAreChannelsBypassing()); + } + + /** + * Helper class for storing the set of information about a zen mode configuration at a specific + * time: the current zen mode setting, ZenModeConfig, and consolidated policy (a result of + * evaluating all active zen rules at the time). + */ + public static class ZenModeInfo { + final int mZenMode; + final ZenModeConfig mConfig; + final NotificationManager.Policy mPolicy; + + ZenModeInfo(int zenMode, ZenModeConfig config, NotificationManager.Policy policy) { + mZenMode = zenMode; + // Store a copy of configs & policies to not accidentally pick up any further changes + mConfig = config != null ? config.copy() : null; + mPolicy = policy != null ? policy.copy() : null; + } + } + + /** + * Class used to track overall changes in zen mode, since changes such as config updates happen + * in multiple stages (first changing the config, then re-evaluating zen mode and the + * consolidated policy), and which contains the logic of 1) whether to log the zen mode change + * and 2) deriving the properties to log. + */ + static class ZenStateChanges { + int mPrevZenMode = ZEN_MODE_UNKNOWN; + int mNewZenMode = ZEN_MODE_UNKNOWN; + ZenModeConfig mPrevConfig, mNewConfig; + NotificationManager.Policy mPrevPolicy, mNewPolicy; + int mCallingUid = Process.INVALID_UID; + boolean mFromSystemOrSystemUi = false; + + private void init(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid, + boolean fromSystemOrSystemUi) { + // previous & new may be the same -- that would indicate that zen mode hasn't changed. + mPrevZenMode = prevInfo.mZenMode; + mNewZenMode = newInfo.mZenMode; + mPrevConfig = prevInfo.mConfig; + mNewConfig = newInfo.mConfig; + mPrevPolicy = prevInfo.mPolicy; + mNewPolicy = newInfo.mPolicy; + mCallingUid = callingUid; + mFromSystemOrSystemUi = fromSystemOrSystemUi; + } + + /** + * Returns whether there is a policy diff represented by this change. This doesn't count + * if the previous policy is null, as that would indicate having no information rather than + * having no previous policy. + */ + private boolean hasPolicyDiff() { + return mPrevPolicy != null && !Objects.equals(mPrevPolicy, mNewPolicy); + } + + /** + * Whether the set of changes encapsulated in this state should be logged. This should only + * be called after methods to store config and zen mode info. + */ + private boolean shouldLogChanges() { + // Did zen mode change from off to on or vice versa? If so, log in all cases. + if (zenModeFlipped()) { + return true; + } + + // If zen mode didn't change, did the policy or number of active rules change? We only + // care about changes that take effect while zen mode is on, so make sure the current + // zen mode is not "OFF" + if (mNewZenMode == ZEN_MODE_OFF) { + return false; + } + return hasPolicyDiff() || hasRuleCountDiff(); + } + + // Does the difference in zen mode go from off to on or vice versa? + private boolean zenModeFlipped() { + if (mPrevZenMode == mNewZenMode) { + return false; + } + + // then it flipped if one or the other is off. (there's only one off state; there are + // multiple states one could consider "on") + return mPrevZenMode == ZEN_MODE_OFF || mNewZenMode == ZEN_MODE_OFF; + } + + // Helper methods below to fill out the atom contents below: + + /** + * Based on the changes, returns the event ID corresponding to the change. Assumes that + * shouldLogChanges() is true and already checked (and will Log.wtf if not true). + */ + ZenStateChangedEvent getEventId() { + if (!shouldLogChanges()) { + Log.wtf(TAG, "attempt to get DNDStateChanged fields without shouldLog=true"); + } + if (zenModeFlipped()) { + if (mPrevZenMode == ZEN_MODE_OFF) { + return ZenStateChangedEvent.DND_TURNED_ON; + } else { + return ZenStateChangedEvent.DND_TURNED_OFF; + } + } + + // zen mode didn't change; we must be here because of a policy change or rule change + if (hasPolicyDiff() || hasChannelsBypassingDiff()) { + return ZenStateChangedEvent.DND_POLICY_CHANGED; + } + + // Also no policy change, so it has to be a rule change + return ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED; + } + + /** + * Based on the config diff, determine which type of rule changed (or "unknown" to indicate + * unknown or neither). + * In the (probably somewhat unusual) case that there are both, manual takes precedence over + * automatic. + */ + int getChangedRuleType() { + ZenModeDiff.ConfigDiff diff = new ZenModeDiff.ConfigDiff(mPrevConfig, mNewConfig); + if (!diff.hasDiff()) { + // no diff in the config. this probably shouldn't happen, but we can consider it + // unknown (given that if zen mode changes it is usually accompanied by some rule + // turning on or off, which should cause a config diff). + return RULE_TYPE_UNKNOWN; + } + + ZenModeDiff.RuleDiff manualDiff = diff.getManualRuleDiff(); + if (manualDiff != null && manualDiff.hasDiff()) { + // a diff in the manual rule doesn't *necessarily* mean that it's responsible for + // the change -- only if it's been added or removed. + if (manualDiff.wasAdded() || manualDiff.wasRemoved()) { + return RULE_TYPE_MANUAL; + } + } + + ArrayMap<String, ZenModeDiff.RuleDiff> autoDiffs = diff.getAllAutomaticRuleDiffs(); + if (autoDiffs != null) { + for (ZenModeDiff.RuleDiff d : autoDiffs.values()) { + if (d != null && d.hasDiff()) { + // If the rule became active or inactive, then this is probably relevant. + if (d.becameActive() || d.becameInactive()) { + return RULE_TYPE_AUTOMATIC; + } + } + } + } + return RULE_TYPE_UNKNOWN; + } + + /** + * Returns whether the previous config and new config have a different number of active + * automatic or manual rules. + */ + private boolean hasRuleCountDiff() { + return numActiveRulesInConfig(mPrevConfig) != numActiveRulesInConfig(mNewConfig); + } + + /** + * Get the number of active rules represented in a zen mode config. Because this is based + * on a config, this does not take into account the zen mode at the time of the config, + * which means callers need to take the zen mode into account for whether the rules are + * actually active. + */ + int numActiveRulesInConfig(ZenModeConfig config) { + // If the config is null, return early + if (config == null) { + return 0; + } + + int rules = 0; + // Loop through the config and check: + // - does a manual rule exist? (if it's non-null, it's active) + // - how many automatic rules are active, as defined by isAutomaticActive()? + if (config.manualRule != null) { + rules++; + } + + if (config.automaticRules != null) { + for (ZenModeConfig.ZenRule rule : config.automaticRules.values()) { + if (rule != null && rule.isAutomaticActive()) { + rules++; + } + } + } + return rules; + } + + // Determine the number of (automatic & manual) rules active after the change takes place. + int getNumRulesActive() { + // If the zen mode has turned off, that means nothing can be active. + if (mNewZenMode == ZEN_MODE_OFF) { + return 0; + } + return numActiveRulesInConfig(mNewConfig); + } + + /** + * Return our best guess as to whether the changes observed are due to a user action. + * Note that this won't be 100% accurate as we can't necessarily distinguish between a + * system uid call indicating "user interacted with Settings" vs "a system app changed + * something automatically". + */ + boolean getIsUserAction() { + // Approach: + // - if manual rule turned on or off, the calling UID is system, and the new manual + // rule does not have an enabler set, guess that this is likely to be a user action. + // This may represent a system app turning on DND automatically, but we guess "user" + // in this case. + // - note that this has a known failure mode of "manual rule turning off + // automatically after the default time runs out". We currently have no way + // of distinguishing this case from a user manually turning off the rule. + // - the reason for checking the enabler field is that a call may look like it's + // coming from a system UID, but if an enabler is set then the request came + // from an external source. "enabler" will be blank when manual rule is turned + // on from Quick Settings or Settings. + // - if an automatic rule's state changes in whether it is "enabled", then + // that is probably a user action. + // - if an automatic rule goes from "not snoozing" to "snoozing", that is probably + // a user action; that means that the user temporarily turned off DND associated + // with that rule. + // - if an automatic rule becomes active but does *not* change in its enabled state + // (covered by a previous case anyway), we guess that this is an automatic change. + // - if a rule is added or removed and the call comes from the system, we guess that + // this is a user action (as system rules can't be added or removed without a user + // action). + switch (getChangedRuleType()) { + case RULE_TYPE_MANUAL: + // TODO(b/278888961): Distinguish the automatically-turned-off state + return mFromSystemOrSystemUi && (getNewManualRuleEnabler() == null); + case RULE_TYPE_AUTOMATIC: + for (ZenModeDiff.RuleDiff d : getChangedAutomaticRules().values()) { + if (d.wasAdded() || d.wasRemoved()) { + // If the change comes from system, a rule being added/removed indicates + // a likely user action. From an app, it's harder to know for sure. + return mFromSystemOrSystemUi; + } + ZenModeDiff.FieldDiff enabled = d.getDiffForField( + ZenModeDiff.RuleDiff.FIELD_ENABLED); + if (enabled != null && enabled.hasDiff()) { + return true; + } + ZenModeDiff.FieldDiff snoozing = d.getDiffForField( + ZenModeDiff.RuleDiff.FIELD_SNOOZING); + if (snoozing != null && snoozing.hasDiff() && (boolean) snoozing.to()) { + return true; + } + } + // If the change was in an automatic rule and none of the "probably triggered + // by a user" cases apply, then it's probably an automatic change. + return false; + case RULE_TYPE_UNKNOWN: + default: + } + + // If the change wasn't in a rule, but was in the zen policy: consider to be user action + // if the calling uid is system + if (hasPolicyDiff() || hasChannelsBypassingDiff()) { + return mCallingUid == Process.SYSTEM_UID; + } + + // don't know, or none of the other things triggered; assume not a user action + return false; + } + + /** + * Get the package UID associated with this change, which is just the calling UID for the + * relevant method changes. This may get reset by ZenModeEventLogger, which has access to + * a PackageManager to get an appropriate UID for a package. + */ + int getPackageUid() { + return mCallingUid; + } + + /** + * Convert the new policy to a DNDPolicyProto format for output in logs. + */ + byte[] getDNDPolicyProto() { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ProtoOutputStream proto = new ProtoOutputStream(bytes); + + // While we don't expect this to be null at any point, guard against any weird cases. + if (mNewPolicy != null) { + proto.write(DNDPolicyProto.CALLS, toState(mNewPolicy.allowCalls())); + proto.write(DNDPolicyProto.REPEAT_CALLERS, + toState(mNewPolicy.allowRepeatCallers())); + proto.write(DNDPolicyProto.MESSAGES, toState(mNewPolicy.allowMessages())); + proto.write(DNDPolicyProto.CONVERSATIONS, toState(mNewPolicy.allowConversations())); + proto.write(DNDPolicyProto.REMINDERS, toState(mNewPolicy.allowReminders())); + proto.write(DNDPolicyProto.EVENTS, toState(mNewPolicy.allowEvents())); + proto.write(DNDPolicyProto.ALARMS, toState(mNewPolicy.allowAlarms())); + proto.write(DNDPolicyProto.MEDIA, toState(mNewPolicy.allowMedia())); + proto.write(DNDPolicyProto.SYSTEM, toState(mNewPolicy.allowSystem())); + + proto.write(DNDPolicyProto.FULLSCREEN, toState(mNewPolicy.showFullScreenIntents())); + proto.write(DNDPolicyProto.LIGHTS, toState(mNewPolicy.showLights())); + proto.write(DNDPolicyProto.PEEK, toState(mNewPolicy.showPeeking())); + proto.write(DNDPolicyProto.STATUS_BAR, toState(mNewPolicy.showStatusBarIcons())); + proto.write(DNDPolicyProto.BADGE, toState(mNewPolicy.showBadges())); + proto.write(DNDPolicyProto.AMBIENT, toState(mNewPolicy.showAmbient())); + proto.write(DNDPolicyProto.NOTIFICATION_LIST, + toState(mNewPolicy.showInNotificationList())); + + // Note: The DND policy proto uses the people type enum from *ZenPolicy* and not + // *NotificationManager.Policy* (which is the type of the consolidated policy). + // This applies to both call and message senders, but not conversation senders, + // where they use the same enum values. + proto.write(DNDPolicyProto.ALLOW_CALLS_FROM, + ZenModeConfig.getZenPolicySenders(mNewPolicy.allowCallsFrom())); + proto.write(DNDPolicyProto.ALLOW_MESSAGES_FROM, + ZenModeConfig.getZenPolicySenders(mNewPolicy.allowMessagesFrom())); + proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, + mNewPolicy.allowConversationsFrom()); + } else { + Log.wtf(TAG, "attempted to write zen mode log event with null policy"); + } + + proto.flush(); + return bytes.toByteArray(); + } + + /** + * Get whether any channels are bypassing DND based on the current new policy. + */ + boolean getAreChannelsBypassing() { + if (mNewPolicy != null) { + return (mNewPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0; + } + return false; + } + + private boolean hasChannelsBypassingDiff() { + boolean prevChannelsBypassing = mPrevPolicy != null + ? (mPrevPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0 : false; + return prevChannelsBypassing != getAreChannelsBypassing(); + } + + /** + * helper method to turn a boolean allow or disallow state into STATE_ALLOW or + * STATE_DISALLOW (there is no concept of "unset" in NM.Policy.) + */ + private int toState(boolean allow) { + return allow ? ZenPolicy.STATE_ALLOW : ZenPolicy.STATE_DISALLOW; + } + + /** + * Get the list of automatic rules that have any diff (as a List of ZenModeDiff.RuleDiff). + * Returns an empty list if there isn't anything. + */ + private @NonNull ArrayMap<String, ZenModeDiff.RuleDiff> getChangedAutomaticRules() { + ArrayMap<String, ZenModeDiff.RuleDiff> ruleDiffs = new ArrayMap<>(); + + ZenModeDiff.ConfigDiff diff = new ZenModeDiff.ConfigDiff(mPrevConfig, mNewConfig); + if (!diff.hasDiff()) { + return ruleDiffs; + } + + ArrayMap<String, ZenModeDiff.RuleDiff> autoDiffs = diff.getAllAutomaticRuleDiffs(); + if (autoDiffs != null) { + return autoDiffs; + } + return ruleDiffs; + } + + /** + * Get the package name associated with this rule's owner, given its id and associated + * RuleDiff, as well as the user ID associated with the config it was found in. Returns null + * if none could be found. + */ + private Pair<String, Integer> getRulePackageAndUser(String id, ZenModeDiff.RuleDiff diff) { + // look for the rule info in the new config unless the rule was deleted. + ZenModeConfig configForSearch = mNewConfig; + if (diff.wasRemoved()) { + configForSearch = mPrevConfig; + } + + if (configForSearch == null) { + return null; + } + + ZenModeConfig.ZenRule rule = configForSearch.automaticRules.getOrDefault(id, null); + if (rule != null) { + if (rule.component != null) { + return new Pair(rule.component.getPackageName(), configForSearch.user); + } + if (rule.configurationActivity != null) { + return new Pair(rule.configurationActivity.getPackageName(), + configForSearch.user); + } + } + return null; + } + + /** + * Get the package name listed as the manual rule "enabler", if it exists in the new config. + */ + private String getNewManualRuleEnabler() { + if (mNewConfig == null || mNewConfig.manualRule == null) { + return null; + } + return mNewConfig.manualRule.enabler; + } + + /** + * Makes a copy for storing intermediate state for testing purposes. + */ + protected ZenStateChanges copy() { + ZenStateChanges copy = new ZenStateChanges(); + copy.mPrevZenMode = mPrevZenMode; + copy.mNewZenMode = mNewZenMode; + copy.mPrevConfig = mPrevConfig.copy(); + copy.mNewConfig = mNewConfig.copy(); + copy.mPrevPolicy = mPrevPolicy.copy(); + copy.mNewPolicy = mNewPolicy.copy(); + copy.mCallingUid = mCallingUid; + copy.mFromSystemOrSystemUi = mFromSystemOrSystemUi; + return copy; + } + } +} diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index f38c6c1aa827..36a0b0c0d8e9 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -76,6 +76,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; import com.android.internal.logging.MetricsLogger; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.notification.SystemNotificationChannels; @@ -125,6 +126,8 @@ public class ZenModeHelper { @VisibleForTesting final SparseArray<ZenModeConfig> mConfigs = new SparseArray<>(); private final Metrics mMetrics = new Metrics(); private final ConditionProviders.Config mServiceConfig; + private SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver; + @VisibleForTesting protected ZenModeEventLogger mZenModeEventLogger; @VisibleForTesting protected int mZenMode; @VisibleForTesting protected NotificationManager.Policy mConsolidatedPolicy; @@ -144,7 +147,9 @@ public class ZenModeHelper { private String[] mPriorityOnlyDndExemptPackages; public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders, - SysUiStatsEvent.BuilderFactory statsEventBuilderFactory) { + SysUiStatsEvent.BuilderFactory statsEventBuilderFactory, + SystemUiSystemPropertiesFlags.FlagResolver flagResolver, + ZenModeEventLogger zenModeEventLogger) { mContext = context; mHandler = new H(looper); addCallback(mMetrics); @@ -165,6 +170,8 @@ public class ZenModeHelper { mConditions = new ZenModeConditions(this, conditionProviders); mServiceConfig = conditionProviders.getConfig(); mStatsEventBuilderFactory = statsEventBuilderFactory; + mFlagResolver = flagResolver; + mZenModeEventLogger = zenModeEventLogger; } public Looper getLooper() { @@ -214,7 +221,13 @@ public class ZenModeHelper { public void initZenMode() { if (DEBUG) Log.d(TAG, "initZenMode"); - evaluateZenMode("init", true /*setRingerMode*/); + synchronized (mConfig) { + // "update" config to itself, which will have no effect in the case where a config + // was read in via XML, but will initialize zen mode if nothing was read in and the + // config remains the default. + updateConfigAndZenModeLocked(mConfig, "init", true /*setRingerMode*/, + Process.SYSTEM_UID /* callingUid */, true /* is system */); + } } public void onSystemReady() { @@ -266,7 +279,7 @@ public class ZenModeHelper { config.user = user; } synchronized (mConfig) { - setConfigLocked(config, null, reason); + setConfigLocked(config, null, reason, Process.SYSTEM_UID, true); } cleanUpZenRules(); } @@ -275,11 +288,13 @@ public class ZenModeHelper { return NotificationManager.zenModeToInterruptionFilter(mZenMode); } - public void requestFromListener(ComponentName name, int filter) { + public void requestFromListener(ComponentName name, int filter, int callingUid, + boolean fromSystemOrSystemUi) { final int newZen = NotificationManager.zenModeFromInterruptionFilter(filter, -1); if (newZen != -1) { setManualZenMode(newZen, null, name != null ? name.getPackageName() : null, - "listener:" + (name != null ? name.flattenToShortString() : null)); + "listener:" + (name != null ? name.flattenToShortString() : null), + callingUid, fromSystemOrSystemUi); } } @@ -324,7 +339,7 @@ public class ZenModeHelper { } public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule, - String reason) { + String reason, int callingUid, boolean fromSystemOrSystemUi) { if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) { PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner()); if (component == null) { @@ -360,7 +375,8 @@ public class ZenModeHelper { ZenRule rule = new ZenRule(); populateZenRule(pkg, automaticZenRule, rule, true); newConfig.automaticRules.put(rule.id, rule); - if (setConfigLocked(newConfig, reason, rule.component, true)) { + if (setConfigLocked(newConfig, reason, rule.component, true, callingUid, + fromSystemOrSystemUi)) { return rule.id; } else { throw new AndroidRuntimeException("Could not create rule"); @@ -369,7 +385,7 @@ public class ZenModeHelper { } public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule, - String reason) { + String reason, int callingUid, boolean fromSystemOrSystemUi) { ZenModeConfig newConfig; synchronized (mConfig) { if (mConfig == null) return false; @@ -395,11 +411,13 @@ public class ZenModeHelper { } populateZenRule(rule.pkg, automaticZenRule, rule, false); - return setConfigLocked(newConfig, reason, rule.component, true); + return setConfigLocked(newConfig, reason, rule.component, true, callingUid, + fromSystemOrSystemUi); } } - public boolean removeAutomaticZenRule(String id, String reason) { + public boolean removeAutomaticZenRule(String id, String reason, int callingUid, + boolean fromSystemOrSystemUi) { ZenModeConfig newConfig; synchronized (mConfig) { if (mConfig == null) return false; @@ -424,11 +442,13 @@ public class ZenModeHelper { } dispatchOnAutomaticRuleStatusChanged( mConfig.user, ruleToRemove.getPkg(), id, AUTOMATIC_RULE_STATUS_REMOVED); - return setConfigLocked(newConfig, reason, null, true); + return setConfigLocked(newConfig, reason, null, true, callingUid, + fromSystemOrSystemUi); } } - public boolean removeAutomaticZenRules(String packageName, String reason) { + public boolean removeAutomaticZenRules(String packageName, String reason, int callingUid, + boolean fromSystemOrSystemUi) { ZenModeConfig newConfig; synchronized (mConfig) { if (mConfig == null) return false; @@ -439,11 +459,13 @@ public class ZenModeHelper { newConfig.automaticRules.removeAt(i); } } - return setConfigLocked(newConfig, reason, null, true); + return setConfigLocked(newConfig, reason, null, true, callingUid, + fromSystemOrSystemUi); } } - public void setAutomaticZenRuleState(String id, Condition condition) { + public void setAutomaticZenRuleState(String id, Condition condition, int callingUid, + boolean fromSystemOrSystemUi) { ZenModeConfig newConfig; synchronized (mConfig) { if (mConfig == null) return; @@ -451,11 +473,13 @@ public class ZenModeHelper { newConfig = mConfig.copy(); ArrayList<ZenRule> rules = new ArrayList<>(); rules.add(newConfig.automaticRules.get(id)); - setAutomaticZenRuleStateLocked(newConfig, rules, condition); + setAutomaticZenRuleStateLocked(newConfig, rules, condition, callingUid, + fromSystemOrSystemUi); } } - public void setAutomaticZenRuleState(Uri ruleDefinition, Condition condition) { + public void setAutomaticZenRuleState(Uri ruleDefinition, Condition condition, int callingUid, + boolean fromSystemOrSystemUi) { ZenModeConfig newConfig; synchronized (mConfig) { if (mConfig == null) return; @@ -463,18 +487,19 @@ public class ZenModeHelper { setAutomaticZenRuleStateLocked(newConfig, findMatchingRules(newConfig, ruleDefinition, condition), - condition); + condition, callingUid, fromSystemOrSystemUi); } } private void setAutomaticZenRuleStateLocked(ZenModeConfig config, List<ZenRule> rules, - Condition condition) { + Condition condition, int callingUid, boolean fromSystemOrSystemUi) { if (rules == null || rules.isEmpty()) return; for (ZenRule rule : rules) { rule.condition = condition; updateSnoozing(rule); - setConfigLocked(config, rule.component, "conditionChanged"); + setConfigLocked(config, rule.component, "conditionChanged", callingUid, + fromSystemOrSystemUi); } } @@ -561,7 +586,7 @@ public class ZenModeHelper { } } - protected void updateDefaultZenRules() { + protected void updateDefaultZenRules(int callingUid, boolean fromSystemOrSystemUi) { updateDefaultAutomaticRuleNames(); for (ZenRule defaultRule : mDefaultConfig.automaticRules.values()) { ZenRule currRule = mConfig.automaticRules.get(defaultRule.id); @@ -575,7 +600,7 @@ public class ZenModeHelper { // update default rule (if locale changed, name of rule will change) currRule.name = defaultRule.name; updateAutomaticZenRule(defaultRule.id, createAutomaticZenRule(currRule), - "locale changed"); + "locale changed", callingUid, fromSystemOrSystemUi); } } } @@ -650,14 +675,16 @@ public class ZenModeHelper { return azr; } - public void setManualZenMode(int zenMode, Uri conditionId, String caller, String reason) { - setManualZenMode(zenMode, conditionId, reason, caller, true /*setRingerMode*/); + public void setManualZenMode(int zenMode, Uri conditionId, String caller, String reason, + int callingUid, boolean fromSystemOrSystemUi) { + setManualZenMode(zenMode, conditionId, reason, caller, true /*setRingerMode*/, callingUid, + fromSystemOrSystemUi); Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.SHOW_ZEN_SETTINGS_SUGGESTION, 0); } private void setManualZenMode(int zenMode, Uri conditionId, String reason, String caller, - boolean setRingerMode) { + boolean setRingerMode, int callingUid, boolean fromSystemOrSystemUi) { ZenModeConfig newConfig; synchronized (mConfig) { if (mConfig == null) return; @@ -681,7 +708,8 @@ public class ZenModeHelper { newRule.enabler = caller; newConfig.manualRule = newRule; } - setConfigLocked(newConfig, reason, null, setRingerMode); + setConfigLocked(newConfig, reason, null, setRingerMode, callingUid, + fromSystemOrSystemUi); } } @@ -806,7 +834,7 @@ public class ZenModeHelper { } if (DEBUG) Log.d(TAG, reason); synchronized (mConfig) { - setConfigLocked(config, null, reason); + setConfigLocked(config, null, reason, Process.SYSTEM_UID, true); } } } @@ -838,12 +866,13 @@ public class ZenModeHelper { /** * Sets the global notification policy used for priority only do not disturb */ - public void setNotificationPolicy(Policy policy) { + public void setNotificationPolicy(Policy policy, int callingUid, boolean fromSystemOrSystemUi) { if (policy == null || mConfig == null) return; synchronized (mConfig) { final ZenModeConfig newConfig = mConfig.copy(); newConfig.applyNotificationPolicy(policy); - setConfigLocked(newConfig, null, "setNotificationPolicy"); + setConfigLocked(newConfig, null, "setNotificationPolicy", callingUid, + fromSystemOrSystemUi); } } @@ -868,7 +897,8 @@ public class ZenModeHelper { } } } - setConfigLocked(newConfig, null, "cleanUpZenRules"); + setConfigLocked(newConfig, null, "cleanUpZenRules", Process.SYSTEM_UID, + true); } } @@ -889,18 +919,21 @@ public class ZenModeHelper { } public boolean setConfigLocked(ZenModeConfig config, ComponentName triggeringComponent, - String reason) { - return setConfigLocked(config, reason, triggeringComponent, true /*setRingerMode*/); + String reason, int callingUid, boolean fromSystemOrSystemUi) { + return setConfigLocked(config, reason, triggeringComponent, true /*setRingerMode*/, + callingUid, fromSystemOrSystemUi); } - public void setConfig(ZenModeConfig config, ComponentName triggeringComponent, String reason) { + public void setConfig(ZenModeConfig config, ComponentName triggeringComponent, String reason, + int callingUid, boolean fromSystemOrSystemUi) { synchronized (mConfig) { - setConfigLocked(config, triggeringComponent, reason); + setConfigLocked(config, triggeringComponent, reason, callingUid, fromSystemOrSystemUi); } } private boolean setConfigLocked(ZenModeConfig config, String reason, - ComponentName triggeringComponent, boolean setRingerMode) { + ComponentName triggeringComponent, boolean setRingerMode, int callingUid, + boolean fromSystemOrSystemUi) { final long identity = Binder.clearCallingIdentity(); try { if (config == null || !config.isValid()) { @@ -927,17 +960,11 @@ public class ZenModeHelper { // send some broadcasts final boolean policyChanged = !Objects.equals(getNotificationPolicy(mConfig), getNotificationPolicy(config)); - if (!config.equals(mConfig)) { - mConfig = config; - dispatchOnConfigChanged(); - updateConsolidatedPolicy(reason); - } if (policyChanged) { dispatchOnPolicyChanged(); } - final String val = Integer.toString(config.hashCode()); - Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val); - evaluateZenMode(reason, setRingerMode); + updateConfigAndZenModeLocked(config, reason, setRingerMode, callingUid, + fromSystemOrSystemUi); mConditions.evaluateConfig(config, triggeringComponent, true /*processSubscriptions*/); return true; } catch (SecurityException e) { @@ -948,6 +975,34 @@ public class ZenModeHelper { } } + /** + * Carries out a config update (if needed) and (re-)evaluates the zen mode value afterwards. + * If logging is enabled, will also request logging of the outcome of this change if needed. + */ + private void updateConfigAndZenModeLocked(ZenModeConfig config, String reason, + boolean setRingerMode, int callingUid, boolean fromSystemOrSystemUi) { + final boolean logZenModeEvents = mFlagResolver.isEnabled( + SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS); + // Store (a copy of) all config and zen mode info prior to any changes taking effect + ZenModeEventLogger.ZenModeInfo prevInfo = new ZenModeEventLogger.ZenModeInfo( + mZenMode, mConfig, mConsolidatedPolicy); + if (!config.equals(mConfig)) { + mConfig = config; + dispatchOnConfigChanged(); + updateConsolidatedPolicy(reason); + } + final String val = Integer.toString(config.hashCode()); + Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val); + evaluateZenMode(reason, setRingerMode); + // After all changes have occurred, log if requested + if (logZenModeEvents) { + ZenModeEventLogger.ZenModeInfo newInfo = new ZenModeEventLogger.ZenModeInfo( + mZenMode, mConfig, mConsolidatedPolicy); + mZenModeEventLogger.maybeLogZenChange(prevInfo, newInfo, callingUid, + fromSystemOrSystemUi); + } + } + private int getZenModeSetting() { return Global.getInt(mContext.getContentResolver(), Global.ZEN_MODE, Global.ZEN_MODE_OFF); } @@ -1366,7 +1421,7 @@ public class ZenModeHelper { if (newZen != -1) { setManualZenMode(newZen, null, "ringerModeInternal", null, - false /*setRingerMode*/); + false /*setRingerMode*/, Process.SYSTEM_UID, true); } if (isChange || newZen != -1 || ringerModeExternal != ringerModeExternalOut) { ZenLog.traceSetRingerModeInternal(ringerModeOld, ringerModeNew, caller, @@ -1404,7 +1459,7 @@ public class ZenModeHelper { } if (newZen != -1) { setManualZenMode(newZen, null, "ringerModeExternal", caller, - false /*setRingerMode*/); + false /*setRingerMode*/, Process.SYSTEM_UID, true); } ZenLog.traceSetRingerModeExternal(ringerModeOld, ringerModeNew, caller, diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 1aa1fd10b00e..a3866caacf39 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -2310,6 +2310,9 @@ public class ComputerEngine implements Computer { if ((intent.getFlags() & Intent.FLAG_IGNORE_EPHEMERAL) != 0) { return false; } + if ((intent.getFlags() & Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER) != 0) { + return false; + } if (!skipPackageCheck && intent.getPackage() != null) { return false; } diff --git a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java index a610b5b7f88f..5f28e56e996c 100644 --- a/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java +++ b/services/core/java/com/android/server/pm/CrossProfileAppsServiceImpl.java @@ -14,6 +14,7 @@ * limitations under the License. */ package com.android.server.pm; + import static android.Manifest.permission.CONFIGURE_INTERACT_ACROSS_PROFILES; import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; import static android.Manifest.permission.INTERACT_ACROSS_USERS; @@ -147,7 +148,7 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { if (launchMainActivity) { launchIntent.setAction(Intent.ACTION_MAIN); launchIntent.addCategory(Intent.CATEGORY_LAUNCHER); - if (targetTask == null) { + if (targetTask == null || options != null) { launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); } else { @@ -333,9 +334,10 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub { } private boolean isCrossProfilePackageAllowlisted(String packageName) { + int userId = mInjector.getCallingUserId(); return mInjector.withCleanCallingIdentity(() -> mInjector.getDevicePolicyManagerInternal() - .getAllCrossProfilePackages().contains(packageName)); + .getAllCrossProfilePackages(userId).contains(packageName)); } private boolean isCrossProfilePackageAllowlistedByDefault(String packageName) { diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java index 8a2888881e59..5b3f7a58e653 100644 --- a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java +++ b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java @@ -382,7 +382,11 @@ public class DefaultCrossProfileIntentFiltersUtils { return filters; } - /** Call intent with tel scheme exclusively handled my managed profile. */ + /** Call intent with tel scheme exclusively handled my managed profile. + * Note that work profile telephony relies on this intent filter to redirect intents to + * the IntentForwarderActivity. Work profile telephony error handling must be updated in + * the Telecomm package CallsManager if this filter is changed. + */ private static final DefaultCrossProfileIntentFilter CALL_MANAGED_PROFILE = new DefaultCrossProfileIntentFilter.Builder( DefaultCrossProfileIntentFilter.Direction.TO_PROFILE, diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 5979b12e4a30..06db5be349d4 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -280,7 +280,7 @@ final class InstallPackageHelper { SharedUserSetting requestSharedUserSetting = mPm.mSettings.getSharedUserSettingLPr( request.getScanRequestPackageSetting()); SharedUserSetting resultSharedUserSetting = mPm.mSettings.getSharedUserSettingLPr( - request.getScanRequestPackageSetting()); + request.getScannedPackageSetting()); if (requestSharedUserSetting != null && requestSharedUserSetting != resultSharedUserSetting) { // shared user changed, remove from old shared user @@ -324,7 +324,6 @@ final class InstallPackageHelper { InstallSource installSource = request.getInstallSource(); final boolean isApex = (scanFlags & SCAN_AS_APEX) != 0; final boolean pkgAlreadyExists = oldPkgSetting != null; - final boolean isAllowUpdateOwnership = parsedPackage.isAllowUpdateOwnership(); final String oldUpdateOwner = pkgAlreadyExists ? oldPkgSetting.getInstallSource().mUpdateOwnerPackageName : null; final String updateOwnerFromSysconfig = isApex || !pkgSetting.isSystem() ? null @@ -346,11 +345,7 @@ final class InstallPackageHelper { } // Handle the update ownership enforcement for APK - if (!isAllowUpdateOwnership) { - // If the app wants to opt-out of the update ownership enforcement via manifest, - // it overrides the installer's use of #setRequestUpdateOwnership. - installSource = installSource.setUpdateOwnerPackageName(null); - } else if (!isApex) { + if (!isApex) { // User installer UID as "current" userId if present; otherwise, use the userId // from InstallRequest. final int userId = installSource.mInstallerPackageUid != Process.INVALID_UID @@ -391,22 +386,18 @@ final class InstallPackageHelper { // For non-standard install (addForInit), installSource is null. } else if (pkgSetting.isSystem()) { // We still honor the manifest attr if the system app wants to opt-out of it. - if (!isAllowUpdateOwnership) { - pkgSetting.setUpdateOwnerPackage(null); + final boolean isSameUpdateOwner = isUpdateOwnershipEnabled + && TextUtils.equals(oldUpdateOwner, updateOwnerFromSysconfig); + + // Here we handle the update owner for the system package, and the rules are: + // -. We use the update owner from sysconfig as the initial value. + // -. Once an app becomes to system app later via OTA, only retains the update + // owner if it's consistence with sysconfig. + // -. Clear the update owner when update owner changes from sysconfig. + if (!pkgAlreadyExists || isSameUpdateOwner) { + pkgSetting.setUpdateOwnerPackage(updateOwnerFromSysconfig); } else { - final boolean isSameUpdateOwner = isUpdateOwnershipEnabled - && TextUtils.equals(oldUpdateOwner, updateOwnerFromSysconfig); - - // Here we handle the update owner for the system package, and the rules are: - // -. We use the update owner from sysconfig as the initial value. - // -. Once an app becomes to system app later via OTA, only retains the update - // owner if it's consistence with sysconfig. - // -. Clear the update owner when update owner changes from sysconfig. - if (!pkgAlreadyExists || isSameUpdateOwner) { - pkgSetting.setUpdateOwnerPackage(updateOwnerFromSysconfig); - } else { - pkgSetting.setUpdateOwnerPackage(null); - } + pkgSetting.setUpdateOwnerPackage(null); } } @@ -606,8 +597,8 @@ final class InstallPackageHelper { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } - public int installExistingPackageAsUser(@Nullable String packageName, @UserIdInt int userId, - @PackageManager.InstallFlags int installFlags, + public Pair<Integer, IntentSender> installExistingPackageAsUser(@Nullable String packageName, + @UserIdInt int userId, @PackageManager.InstallFlags int installFlags, @PackageManager.InstallReason int installReason, @Nullable List<String> allowlistedRestrictedPermissions, @Nullable IntentSender intentSender) { @@ -632,7 +623,7 @@ final class InstallPackageHelper { true /* requireFullPermission */, true /* checkShell */, "installExistingPackage for user " + userId); if (mPm.isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) { - return PackageManager.INSTALL_FAILED_USER_RESTRICTED; + return Pair.create(PackageManager.INSTALL_FAILED_USER_RESTRICTED, intentSender); } final long callingId = Binder.clearCallingIdentity(); @@ -648,7 +639,7 @@ final class InstallPackageHelper { final Computer snapshot = mPm.snapshotComputer(); pkgSetting = mPm.mSettings.getPackageLPr(packageName); if (pkgSetting == null || pkgSetting.getPkg() == null) { - return PackageManager.INSTALL_FAILED_INVALID_URI; + return Pair.create(PackageManager.INSTALL_FAILED_INVALID_URI, intentSender); } if (!snapshot.canViewInstantApps(callingUid, UserHandle.getUserId(callingUid))) { // only allow the existing package to be used if it's installed as a full @@ -661,7 +652,7 @@ final class InstallPackageHelper { } } if (!installAllowed) { - return PackageManager.INSTALL_FAILED_INVALID_URI; + return Pair.create(PackageManager.INSTALL_FAILED_INVALID_URI, intentSender); } } if (!pkgSetting.getInstalled(userId)) { @@ -719,14 +710,17 @@ final class InstallPackageHelper { } // start async restore with no post-install since we finish install here + final IntentSender onCompleteSender = intentSender; + intentSender = null; + InstallRequest request = new InstallRequest(userId, PackageManager.INSTALL_SUCCEEDED, pkgSetting.getPkg(), new int[]{ userId }, () -> { mPm.restorePermissionsAndUpdateRolesForNewUserInstall(packageName, userId); - if (intentSender != null) { - onRestoreComplete(PackageManager.INSTALL_SUCCEEDED, mContext, - intentSender); + if (onCompleteSender != null) { + onInstallComplete(PackageManager.INSTALL_SUCCEEDED, mContext, + onCompleteSender); } }); restoreAndPostInstall(request); @@ -735,10 +729,10 @@ final class InstallPackageHelper { Binder.restoreCallingIdentity(callingId); } - return PackageManager.INSTALL_SUCCEEDED; + return Pair.create(PackageManager.INSTALL_SUCCEEDED, intentSender); } - private static void onRestoreComplete(int returnCode, Context context, IntentSender target) { + static void onInstallComplete(int returnCode, Context context, IntentSender target) { Intent fillIn = new Intent(); fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageManager.installStatusToPublicStatus(returnCode)); diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 0bd6dffed024..ae169318cedc 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -2021,7 +2021,7 @@ public class LauncherAppsService extends SystemService { } // Each launcher has a different set of pinned shortcuts, so we need to do a // query in here. - // (As of now, only one launcher has the permission at a time, so it's bit + // (As of now, only one launcher has the permission at a time, so it's a bit // moot, but we may change the permission model eventually.) final List<ShortcutInfo> list = mShortcutServiceInternal.getShortcuts(launcherUserId, diff --git a/services/core/java/com/android/server/pm/NoFilteringResolver.java b/services/core/java/com/android/server/pm/NoFilteringResolver.java index ccd5b0ef4285..b87256dbbd9a 100644 --- a/services/core/java/com/android/server/pm/NoFilteringResolver.java +++ b/services/core/java/com/android/server/pm/NoFilteringResolver.java @@ -60,15 +60,9 @@ public class NoFilteringResolver extends CrossProfileResolver { public static boolean isIntentRedirectionAllowed(Context context, AppCloningDeviceConfigHelper appCloningDeviceConfigHelper, boolean resolveForStart, long flags) { - final long token = Binder.clearCallingIdentity(); - try { - return context.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks) - && appCloningDeviceConfigHelper.getEnableAppCloningBuildingBlocks() + return isAppCloningBuildingBlocksEnabled(context, appCloningDeviceConfigHelper) && (resolveForStart || (((flags & PackageManager.MATCH_CLONE_PROFILE) != 0) && hasPermission(context, Manifest.permission.QUERY_CLONED_APPS))); - } finally { - Binder.restoreCallingIdentity(token); - } } public NoFilteringResolver(ComponentResolverApi componentResolver, @@ -146,4 +140,18 @@ public class NoFilteringResolver extends CrossProfileResolver { return context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; } + + /** + * Checks if the AppCloningBuildingBlocks flag is enabled. + */ + private static boolean isAppCloningBuildingBlocksEnabled(Context context, + AppCloningDeviceConfigHelper appCloningDeviceConfigHelper) { + final long token = Binder.clearCallingIdentity(); + try { + return context.getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks) + && appCloningDeviceConfigHelper.getEnableAppCloningBuildingBlocks(); + } finally { + Binder.restoreCallingIdentity(token); + } + } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index e8bf82fb80b4..6491fd1b1f98 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -104,6 +104,7 @@ import com.android.server.SystemConfig; import com.android.server.SystemService; import com.android.server.SystemServiceManager; import com.android.server.pm.parsing.PackageParser2; +import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.utils.RequestThrottle; import libcore.io.IoUtils; @@ -1292,8 +1293,15 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements public void installExistingPackage(String packageName, int installFlags, int installReason, IntentSender statusReceiver, int userId, List<String> allowListedPermissions) { final InstallPackageHelper installPackageHelper = new InstallPackageHelper(mPm); - installPackageHelper.installExistingPackageAsUser(packageName, userId, installFlags, - installReason, allowListedPermissions, statusReceiver); + + var result = installPackageHelper.installExistingPackageAsUser(packageName, userId, + installFlags, installReason, allowListedPermissions, statusReceiver); + + int returnCode = result.first; + IntentSender onCompleteSender = result.second; + if (onCompleteSender != null) { + InstallPackageHelper.onInstallComplete(returnCode, mContext, onCompleteSender); + } } @Override @@ -1308,6 +1316,13 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } } + private boolean isValidForInstallConstraints(PackageStateInternal ps, + String installerPackageName) { + return TextUtils.equals(ps.getInstallSource().mInstallerPackageName, installerPackageName) + || TextUtils.equals(ps.getInstallSource().mUpdateOwnerPackageName, + installerPackageName); + } + private CompletableFuture<InstallConstraintsResult> checkInstallConstraintsInternal( String installerPackageName, List<String> packageNames, InstallConstraints constraints, long timeoutMillis) { @@ -1324,8 +1339,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements if (!PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid)) { for (var packageName : packageNames) { var ps = snapshot.getPackageStateInternal(packageName); - if (ps == null || !TextUtils.equals( - ps.getInstallSource().mInstallerPackageName, installerPackageName)) { + if (ps == null || !isValidForInstallConstraints(ps, installerPackageName)) { throw new SecurityException("Caller has no access to package " + packageName); } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index d3f7002e859f..f0e38955f050 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -745,9 +745,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private int mValidatedTargetSdk = INVALID_TARGET_SDK_VERSION; - @GuardedBy("mLock") - private boolean mAllowsUpdateOwnership = true; - private static final FileFilter sAddedApkFilter = new FileFilter() { @Override public boolean accept(File file) { @@ -869,11 +866,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final int USER_ACTION_NOT_NEEDED = 0; private static final int USER_ACTION_REQUIRED = 1; + private static final int USER_ACTION_PENDING_APK_PARSING = 2; private static final int USER_ACTION_REQUIRED_UPDATE_OWNER_REMINDER = 3; @IntDef({ USER_ACTION_NOT_NEEDED, USER_ACTION_REQUIRED, + USER_ACTION_PENDING_APK_PARSING, USER_ACTION_REQUIRED_UPDATE_OWNER_REMINDER, }) @interface UserActionRequirement {} @@ -964,11 +963,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { && !isApexSession() && !isUpdateOwner && !isInstallerShell - && mAllowsUpdateOwnership // We don't enforce the update ownership for the managed user and profile. && !isFromManagedUserOrProfile) { return USER_ACTION_REQUIRED_UPDATE_OWNER_REMINDER; } + if (isPermissionGranted) { return USER_ACTION_NOT_NEEDED; } @@ -983,20 +982,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { && isUpdateWithoutUserActionPermissionGranted && ((isUpdateOwnershipEnforcementEnabled ? isUpdateOwner : isInstallerOfRecord) || isSelfUpdate)) { - if (!isApexSession()) { - if (!isTargetSdkConditionSatisfied(this)) { - return USER_ACTION_REQUIRED; - } - - if (!mSilentUpdatePolicy.isSilentUpdateAllowed( - getInstallerPackageName(), getPackageName())) { - // Fall back to the non-silent update if a repeated installation is invoked - // within the throttle time. - return USER_ACTION_REQUIRED; - } - mSilentUpdatePolicy.track(getInstallerPackageName(), getPackageName()); - return USER_ACTION_NOT_NEEDED; - } + return USER_ACTION_PENDING_APK_PARSING; } return USER_ACTION_REQUIRED; @@ -2404,6 +2390,26 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { session.sendPendingUserActionIntent(target); return true; } + + if (!session.isApexSession() && userActionRequirement == USER_ACTION_PENDING_APK_PARSING) { + if (!isTargetSdkConditionSatisfied(session)) { + session.sendPendingUserActionIntent(target); + return true; + } + + if (session.params.requireUserAction == SessionParams.USER_ACTION_NOT_REQUIRED) { + if (!session.mSilentUpdatePolicy.isSilentUpdateAllowed( + session.getInstallerPackageName(), session.getPackageName())) { + // Fall back to the non-silent update if a repeated installation is invoked + // within the throttle time. + session.sendPendingUserActionIntent(target); + return true; + } + session.mSilentUpdatePolicy.track(session.getInstallerPackageName(), + session.getPackageName()); + } + } + return false; } @@ -3409,8 +3415,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // {@link PackageLite#getTargetSdk()} mValidatedTargetSdk = packageLite.getTargetSdk(); - mAllowsUpdateOwnership = packageLite.isAllowUpdateOwnership(); - return packageLite; } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 0c52bb5e7ead..ae520c00d977 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -5350,7 +5350,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService public int installExistingPackageAsUser(String packageName, int userId, int installFlags, int installReason, List<String> whiteListedPermissions) { return mInstallPackageHelper.installExistingPackageAsUser(packageName, userId, installFlags, - installReason, whiteListedPermissions, null); + installReason, whiteListedPermissions, null).first; } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index d3f3a69f507d..05bfec48464b 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -698,7 +698,7 @@ class PackageManagerShellCommand extends ShellCommand { null /* usesSplitNames */, null /* configForSplit */, null /* splitApkPaths */, null /* splitRevisionCodes */, apkLite.getTargetSdkVersion(), null /* requiredSplitTypes */, - null /* splitTypes */, apkLite.isAllowUpdateOwnership()); + null /* splitTypes */); sessionSize += InstallLocationUtils.calculateInstalledSize(pkgLite, params.sessionParams.abiOverride, fd.getFileDescriptor()); } catch (IOException e) { diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java index 9127a93a46ee..ba825774daf6 100644 --- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java +++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java @@ -697,8 +697,6 @@ public final class SuspendPackageHelper { Computer snapshot, int userId, boolean suspend) { final Set<String> toSuspend = packagesToSuspendInQuietMode(snapshot, userId); if (!suspend) { - // Note: this method is called from DPMS constructor to suspend apps on upgrade, but - // it won't enter here because 'suspend' will equal 'true'. final DevicePolicyManagerInternal dpm = LocalServices.getService(DevicePolicyManagerInternal.class); if (dpm != null) { diff --git a/services/core/java/com/android/server/pm/UserJourneyLogger.java b/services/core/java/com/android/server/pm/UserJourneyLogger.java index f48a1669c259..895edce093b5 100644 --- a/services/core/java/com/android/server/pm/UserJourneyLogger.java +++ b/services/core/java/com/android/server/pm/UserJourneyLogger.java @@ -99,6 +99,8 @@ public class UserJourneyLogger { FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN; public static final int USER_JOURNEY_REVOKE_ADMIN = FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN; + public static final int USER_JOURNEY_USER_LIFECYCLE = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_LIFECYCLE; @IntDef(prefix = {"USER_JOURNEY"}, value = { USER_JOURNEY_UNKNOWN, @@ -109,7 +111,8 @@ public class UserJourneyLogger { USER_JOURNEY_USER_CREATE, USER_JOURNEY_USER_REMOVE, USER_JOURNEY_GRANT_ADMIN, - USER_JOURNEY_REVOKE_ADMIN + USER_JOURNEY_REVOKE_ADMIN, + USER_JOURNEY_USER_LIFECYCLE }) public @interface UserJourney { } @@ -272,11 +275,12 @@ public class UserJourneyLogger { int userType, int userFlags, @UserJourneyErrorCode int errorCode) { if (session == null) { writeUserLifecycleJourneyReported(-1, journey, originalUserId, targetUserId, - userType, userFlags, ERROR_CODE_INVALID_SESSION_ID); + userType, userFlags, ERROR_CODE_INVALID_SESSION_ID, -1); } else { + final long elapsedTime = System.currentTimeMillis() - session.mStartTimeInMills; writeUserLifecycleJourneyReported( session.mSessionId, journey, originalUserId, targetUserId, userType, userFlags, - errorCode); + errorCode, elapsedTime); } } @@ -285,10 +289,10 @@ public class UserJourneyLogger { */ @VisibleForTesting public void writeUserLifecycleJourneyReported(long sessionId, int journey, int originalUserId, - int targetUserId, int userType, int userFlags, int errorCode) { + int targetUserId, int userType, int userFlags, int errorCode, long elapsedTime) { FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, sessionId, journey, originalUserId, targetUserId, userType, userFlags, - errorCode); + errorCode, elapsedTime); } /** @@ -452,6 +456,29 @@ public class UserJourneyLogger { } /** + * Log user journey event and report finishing with error + */ + public UserJourneySession logDelayedUserJourneyFinishWithError(@UserIdInt int originalUserId, + UserInfo targetUser, @UserJourney int journey, @UserJourneyErrorCode int errorCode) { + synchronized (mLock) { + final int key = getUserJourneyKey(targetUser.id, journey); + final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(key); + if (userJourneySession != null) { + logUserLifecycleJourneyReported( + userJourneySession, + journey, originalUserId, targetUser.id, + getUserTypeForStatsd(targetUser.userType), + targetUser.flags, + errorCode); + mUserIdToUserJourneyMap.remove(key); + + return userJourneySession; + } + } + return null; + } + + /** * Log event and report finish when user is null. This is edge case when UserInfo * can not be passed because it is null, therefore all information are passed as arguments. */ @@ -533,6 +560,23 @@ public class UserJourneyLogger { } /** + * This keeps the start time when finishing extensively long journey was began. + * For instance full user lifecycle ( from creation to deletion )when user is about to delete + * we need to get user creation time before it was deleted. + */ + public UserJourneySession startSessionForDelayedJourney(@UserIdInt int targetId, + @UserJourney int journey, long startTime) { + final long newSessionId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE); + synchronized (mLock) { + final int key = getUserJourneyKey(targetId, journey); + final UserJourneySession userJourneySession = + new UserJourneySession(newSessionId, journey, startTime); + mUserIdToUserJourneyMap.append(key, userJourneySession); + return userJourneySession; + } + } + + /** * Helper class to store user journey and session id. * * <p> User journey tracks a chain of user lifecycle events occurring during different user @@ -542,11 +586,19 @@ public class UserJourneyLogger { public final long mSessionId; @UserJourney public final int mJourney; + public long mStartTimeInMills; @VisibleForTesting public UserJourneySession(long sessionId, @UserJourney int journey) { mJourney = journey; mSessionId = sessionId; + mStartTimeInMills = System.currentTimeMillis(); + } + @VisibleForTesting + public UserJourneySession(long sessionId, @UserJourney int journey, long startTimeInMills) { + mJourney = journey; + mSessionId = sessionId; + mStartTimeInMills = startTimeInMills; } } -} +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 4ef68d8f5aaf..7e88e13e1788 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -30,6 +30,7 @@ import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_USER_IS_NOT_AN_ import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_GRANT_ADMIN; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_CREATE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_LIFECYCLE; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_REMOVE; import android.Manifest; @@ -2853,9 +2854,8 @@ public class UserManagerService extends IUserManager.Stub { UserHandle.USER_NULL, UserManager.RESTRICTION_SOURCE_SYSTEM)); } - synchronized (mRestrictionsLock) { - result.addAll(mDevicePolicyUserRestrictions.getEnforcingUsers(restrictionKey, userId)); - } + result.addAll(getDevicePolicyManagerInternal() + .getUserRestrictionSources(restrictionKey, userId)); return result; } @@ -3332,7 +3332,7 @@ public class UserManagerService extends IUserManager.Stub { } /** - * Enforces that only the system UID or root's UID or apps that have the + * Enforces that only the system UID or root's UID or apps that have the * {@link android.Manifest.permission#MANAGE_USERS MANAGE_USERS} or * {@link android.Manifest.permission#QUERY_USERS QUERY_USERS} * can make certain calls to the UserManager. @@ -5535,6 +5535,8 @@ public class UserManagerService extends IUserManager.Stub { } mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_USER_REMOVE); + mUserJourneyLogger.startSessionForDelayedJourney(userId, + USER_JOURNEY_USER_LIFECYCLE, userData.info.creationTime); try { mAppOpsService.removeUser(userId); @@ -5560,6 +5562,10 @@ public class UserManagerService extends IUserManager.Stub { mUserJourneyLogger.logUserJourneyFinishWithError(originUserId, userData.info, USER_JOURNEY_USER_REMOVE, ERROR_CODE_UNSPECIFIED); + mUserJourneyLogger + .logDelayedUserJourneyFinishWithError(originUserId, + userData.info, USER_JOURNEY_USER_LIFECYCLE, + ERROR_CODE_UNSPECIFIED); } @Override public void userStopAborted(int userIdParam) { @@ -5567,6 +5573,10 @@ public class UserManagerService extends IUserManager.Stub { mUserJourneyLogger.logUserJourneyFinishWithError(originUserId, userData.info, USER_JOURNEY_USER_REMOVE, ERROR_CODE_ABORTED); + mUserJourneyLogger + .logDelayedUserJourneyFinishWithError(originUserId, + userData.info, USER_JOURNEY_USER_LIFECYCLE, + ERROR_CODE_ABORTED); } }); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index 6032fecfea67..8815834f6b5c 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -238,6 +238,7 @@ public class UserRestrictionsUtils { UserManager.DISALLOW_CONFIG_DATE_TIME, UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserManager.DISALLOW_CHANGE_WIFI_STATE, + UserManager.DISALLOW_DEBUGGING_FEATURES, UserManager.DISALLOW_WIFI_TETHERING, UserManager.DISALLOW_WIFI_DIRECT, UserManager.DISALLOW_ADD_WIFI_CONFIG, diff --git a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java index de31b4699918..f036835f7d4e 100644 --- a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java +++ b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java @@ -1810,11 +1810,6 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public boolean isAllowUpdateOwnership() { - return getBoolean2(Booleans2.ALLOW_UPDATE_OWNERSHIP); - } - - @Override public boolean isVmSafeMode() { return getBoolean(Booleans.VM_SAFE_MODE); } @@ -2518,11 +2513,6 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setAllowUpdateOwnership(boolean value) { - return setBoolean2(Booleans2.ALLOW_UPDATE_OWNERSHIP, value); - } - - @Override public PackageImpl sortActivities() { Collections.sort(this.activities, ORDER_COMPARATOR); return this; @@ -3736,6 +3726,5 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, private static final long STUB = 1L; private static final long APEX = 1L << 1; - private static final long ALLOW_UPDATE_OWNERSHIP = 1L << 2; } } diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java index 4d2b119d7800..951052922586 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -216,6 +216,7 @@ final class DefaultPermissionGrantPolicy { STORAGE_PERMISSIONS.add(Manifest.permission.READ_MEDIA_AUDIO); STORAGE_PERMISSIONS.add(Manifest.permission.READ_MEDIA_VIDEO); STORAGE_PERMISSIONS.add(Manifest.permission.READ_MEDIA_IMAGES); + STORAGE_PERMISSIONS.add(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED); } private static final Set<String> NEARBY_DEVICES_PERMISSIONS = new ArraySet<>(); diff --git a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java index 2fdda1210394..e54f34d1c4ac 100644 --- a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -1483,10 +1483,4 @@ public interface AndroidPackage { * @hide */ boolean isVisibleToInstantApps(); - - /** - * @see R.styleable#AndroidManifest_allowUpdateOwnership - * @hide - */ - boolean isAllowUpdateOwnership(); } diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java index 6cb6a9783134..7fc33568f9b9 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java @@ -388,8 +388,6 @@ public interface ParsingPackage { ParsingPackage setLocaleConfigResourceId(int localeConfigRes); - ParsingPackage setAllowUpdateOwnership(boolean value); - /** * Sets the trusted host certificates of apps that are allowed to embed activities of this * application. diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index fda44e495b89..1567af081857 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -219,7 +219,6 @@ public class ParsingPackageUtils { public static final int PARSE_DEFAULT_INSTALL_LOCATION = PackageInfo.INSTALL_LOCATION_UNSPECIFIED; public static final int PARSE_DEFAULT_TARGET_SANDBOX = 1; - public static final boolean PARSE_DEFAULT_ALLOW_UPDATE_OWNERSHIP = true; /** * If set to true, we will only allow package files that exactly match the DTD. Otherwise, we @@ -887,9 +886,7 @@ public class ParsingPackageUtils { .setTargetSandboxVersion(anInteger(PARSE_DEFAULT_TARGET_SANDBOX, R.styleable.AndroidManifest_targetSandboxVersion, sa)) /* Set the global "on SD card" flag */ - .setExternalStorage((flags & PARSE_EXTERNAL_STORAGE) != 0) - .setAllowUpdateOwnership(bool(PARSE_DEFAULT_ALLOW_UPDATE_OWNERSHIP, - R.styleable.AndroidManifest_allowUpdateOwnership, sa)); + .setExternalStorage((flags & PARSE_EXTERNAL_STORAGE) != 0); boolean foundApp = false; final int depth = parser.getDepth(); diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 93d6676dd929..4a57592aa1ae 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -1675,6 +1675,7 @@ public class BatteryStatsImpl extends BatteryStats { String mLastWakeupReason = null; long mLastWakeupUptimeMs = 0; + long mLastWakeupElapsedTimeMs = 0; private final HashMap<String, SamplingTimer> mWakeupReasonStats = new HashMap<>(); public Map<String, ? extends Timer> getWakeupReasonStats() { @@ -5048,7 +5049,7 @@ public class BatteryStatsImpl extends BatteryStats { SamplingTimer timer = getWakeupReasonTimerLocked(mLastWakeupReason); timer.add(deltaUptimeMs * 1000, 1, elapsedRealtimeMs); // time in in microseconds FrameworkStatsLog.write(FrameworkStatsLog.KERNEL_WAKEUP_REPORTED, mLastWakeupReason, - /* duration_usec */ deltaUptimeMs * 1000); + /* duration_usec */ deltaUptimeMs * 1000, mLastWakeupElapsedTimeMs); mLastWakeupReason = null; } } @@ -5059,6 +5060,7 @@ public class BatteryStatsImpl extends BatteryStats { mHistory.recordWakeupEvent(elapsedRealtimeMs, uptimeMs, reason); mLastWakeupReason = reason; mLastWakeupUptimeMs = uptimeMs; + mLastWakeupElapsedTimeMs = elapsedRealtimeMs; } @GuardedBy("this") diff --git a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java index 73ab7822ac39..712a6964e82d 100644 --- a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java +++ b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java @@ -23,6 +23,7 @@ import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_UNKNOWN; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_WIFI; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; import android.os.Handler; @@ -48,6 +49,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -62,15 +64,14 @@ public class CpuWakeupStats { private static final String SUBSYSTEM_SENSOR_STRING = "Sensor"; private static final String SUBSYSTEM_CELLULAR_DATA_STRING = "Cellular_data"; private static final String TRACE_TRACK_WAKEUP_ATTRIBUTION = "wakeup_attribution"; - @VisibleForTesting - static final long WAKEUP_REASON_HALF_WINDOW_MS = 500; + private static final long WAKEUP_WRITE_DELAY_MS = TimeUnit.SECONDS.toMillis(30); private final Handler mHandler; private final IrqDeviceMap mIrqDeviceMap; @VisibleForTesting final Config mConfig = new Config(); - private final WakingActivityHistory mRecentWakingActivity = new WakingActivityHistory(); + private final WakingActivityHistory mRecentWakingActivity; @VisibleForTesting final TimeSparseArray<Wakeup> mWakeupEvents = new TimeSparseArray<>(); @@ -85,6 +86,8 @@ public class CpuWakeupStats { public CpuWakeupStats(Context context, int mapRes, Handler handler) { mIrqDeviceMap = IrqDeviceMap.getInstance(context, mapRes); + mRecentWakingActivity = new WakingActivityHistory( + () -> mConfig.WAKING_ACTIVITY_RETENTION_MS); mHandler = handler; } @@ -202,15 +205,14 @@ public class CpuWakeupStats { /** Notes a wakeup reason as reported by SuspendControlService to battery stats. */ public synchronized void noteWakeupTimeAndReason(long elapsedRealtime, long uptime, String rawReason) { - final Wakeup parsedWakeup = Wakeup.parseWakeup(rawReason, elapsedRealtime, uptime); + final Wakeup parsedWakeup = Wakeup.parseWakeup(rawReason, elapsedRealtime, uptime, + mIrqDeviceMap); if (parsedWakeup == null) { + // This wakeup is unsupported for attribution. Exit. return; } mWakeupEvents.put(elapsedRealtime, parsedWakeup); attemptAttributionFor(parsedWakeup); - // Assuming that wakeups always arrive in monotonically increasing elapsedRealtime order, - // we can delete all history that will not be useful in attributing future wakeups. - mRecentWakingActivity.clearAllBefore(elapsedRealtime - WAKEUP_REASON_HALF_WINDOW_MS); // Limit history of wakeups and their attribution to the last retentionDuration. Note that // the last wakeup and its attribution (if computed) is always stored, even if that wakeup @@ -244,25 +246,22 @@ public class CpuWakeupStats { } private synchronized void attemptAttributionFor(Wakeup wakeup) { - final SparseBooleanArray subsystems = getResponsibleSubsystemsForWakeup(wakeup); - if (subsystems == null) { - // We don't support attribution for this kind of wakeup yet. - return; - } + final SparseBooleanArray subsystems = wakeup.mResponsibleSubsystems; SparseArray<SparseIntArray> attribution = mWakeupAttribution.get(wakeup.mElapsedMillis); if (attribution == null) { attribution = new SparseArray<>(); mWakeupAttribution.put(wakeup.mElapsedMillis, attribution); } + final long matchingWindowMillis = mConfig.WAKEUP_MATCHING_WINDOW_MS; for (int subsystemIdx = 0; subsystemIdx < subsystems.size(); subsystemIdx++) { final int subsystem = subsystems.keyAt(subsystemIdx); - // Blame all activity that happened WAKEUP_REASON_HALF_WINDOW_MS before or after + // Blame all activity that happened matchingWindowMillis before or after // the wakeup from each responsible subsystem. - final long startTime = wakeup.mElapsedMillis - WAKEUP_REASON_HALF_WINDOW_MS; - final long endTime = wakeup.mElapsedMillis + WAKEUP_REASON_HALF_WINDOW_MS; + final long startTime = wakeup.mElapsedMillis - matchingWindowMillis; + final long endTime = wakeup.mElapsedMillis + matchingWindowMillis; final SparseIntArray uidsToBlame = mRecentWakingActivity.removeBetween(subsystem, startTime, endTime); @@ -272,18 +271,16 @@ public class CpuWakeupStats { private synchronized boolean attemptAttributionWith(int subsystem, long activityElapsed, SparseIntArray uidProcStates) { + final long matchingWindowMillis = mConfig.WAKEUP_MATCHING_WINDOW_MS; + final int startIdx = mWakeupEvents.closestIndexOnOrAfter( - activityElapsed - WAKEUP_REASON_HALF_WINDOW_MS); + activityElapsed - matchingWindowMillis); final int endIdx = mWakeupEvents.closestIndexOnOrBefore( - activityElapsed + WAKEUP_REASON_HALF_WINDOW_MS); + activityElapsed + matchingWindowMillis); for (int wakeupIdx = startIdx; wakeupIdx <= endIdx; wakeupIdx++) { final Wakeup wakeup = mWakeupEvents.valueAt(wakeupIdx); - final SparseBooleanArray subsystems = getResponsibleSubsystemsForWakeup(wakeup); - if (subsystems == null) { - // Unsupported for attribution - continue; - } + final SparseBooleanArray subsystems = wakeup.mResponsibleSubsystems; if (subsystems.get(subsystem)) { // We don't expect more than one wakeup to be found within such a short window, so // just attribute this one and exit @@ -405,11 +402,13 @@ public class CpuWakeupStats { */ @VisibleForTesting static final class WakingActivityHistory { - private static final long WAKING_ACTIVITY_RETENTION_MS = TimeUnit.MINUTES.toMillis(10); - + private LongSupplier mRetentionSupplier; @VisibleForTesting - final SparseArray<TimeSparseArray<SparseIntArray>> mWakingActivity = - new SparseArray<>(); + final SparseArray<TimeSparseArray<SparseIntArray>> mWakingActivity = new SparseArray<>(); + + WakingActivityHistory(LongSupplier retentionSupplier) { + mRetentionSupplier = retentionSupplier; + } void recordActivity(int subsystem, long elapsedRealtime, SparseIntArray uidProcStates) { if (uidProcStates == null) { @@ -433,27 +432,13 @@ public class CpuWakeupStats { } } } - // Limit activity history per subsystem to the last WAKING_ACTIVITY_RETENTION_MS. - // Note that the last activity is always present, even if it occurred before - // WAKING_ACTIVITY_RETENTION_MS. + // Limit activity history per subsystem to the last retention period as supplied by + // mRetentionSupplier. Note that the last activity is always present, even if it + // occurred before the retention period. final int endIdx = wakingActivity.closestIndexOnOrBefore( - elapsedRealtime - WAKING_ACTIVITY_RETENTION_MS); + elapsedRealtime - mRetentionSupplier.getAsLong()); for (int i = endIdx; i >= 0; i--) { - wakingActivity.removeAt(endIdx); - } - } - - void clearAllBefore(long elapsedRealtime) { - for (int subsystemIdx = mWakingActivity.size() - 1; subsystemIdx >= 0; subsystemIdx--) { - final TimeSparseArray<SparseIntArray> activityPerSubsystem = - mWakingActivity.valueAt(subsystemIdx); - final int endIdx = activityPerSubsystem.closestIndexOnOrBefore(elapsedRealtime); - for (int removeIdx = endIdx; removeIdx >= 0; removeIdx--) { - activityPerSubsystem.removeAt(removeIdx); - } - // Generally waking activity is a high frequency occurrence for any subsystem, so we - // don't delete the TimeSparseArray even if it is now empty, to avoid object churn. - // This will leave one TimeSparseArray per subsystem, which are few right now. + wakingActivity.removeAt(i); } } @@ -515,33 +500,6 @@ public class CpuWakeupStats { } } - private SparseBooleanArray getResponsibleSubsystemsForWakeup(Wakeup wakeup) { - if (ArrayUtils.isEmpty(wakeup.mDevices)) { - return null; - } - final SparseBooleanArray result = new SparseBooleanArray(); - for (final Wakeup.IrqDevice device : wakeup.mDevices) { - final List<String> rawSubsystems = mIrqDeviceMap.getSubsystemsForDevice(device.mDevice); - - boolean anyKnownSubsystem = false; - if (rawSubsystems != null) { - for (int i = 0; i < rawSubsystems.size(); i++) { - final int subsystem = stringToKnownSubsystem(rawSubsystems.get(i)); - if (subsystem != CPU_WAKEUP_SUBSYSTEM_UNKNOWN) { - // Just in case the xml had arbitrary subsystem names, we want to make sure - // that we only put the known ones into our attribution map. - result.put(subsystem, true); - anyKnownSubsystem = true; - } - } - } - if (!anyKnownSubsystem) { - result.put(CPU_WAKEUP_SUBSYSTEM_UNKNOWN, true); - } - } - return result; - } - static int stringToKnownSubsystem(String rawSubsystem) { switch (rawSubsystem) { case SUBSYSTEM_ALARM_STRING: @@ -598,15 +556,19 @@ public class CpuWakeupStats { long mElapsedMillis; long mUptimeMillis; IrqDevice[] mDevices; + SparseBooleanArray mResponsibleSubsystems; - private Wakeup(int type, IrqDevice[] devices, long elapsedMillis, long uptimeMillis) { + private Wakeup(int type, IrqDevice[] devices, long elapsedMillis, long uptimeMillis, + SparseBooleanArray responsibleSubsystems) { mType = type; mDevices = devices; mElapsedMillis = elapsedMillis; mUptimeMillis = uptimeMillis; + mResponsibleSubsystems = responsibleSubsystems; } - static Wakeup parseWakeup(String rawReason, long elapsedMillis, long uptimeMillis) { + static Wakeup parseWakeup(String rawReason, long elapsedMillis, long uptimeMillis, + IrqDeviceMap deviceMap) { final String[] components = rawReason.split(":"); if (ArrayUtils.isEmpty(components) || components[0].startsWith(ABORT_REASON_PREFIX)) { // Accounting of aborts is not supported yet. @@ -616,6 +578,7 @@ public class CpuWakeupStats { int type = TYPE_IRQ; int parsedDeviceCount = 0; final IrqDevice[] parsedDevices = new IrqDevice[components.length]; + final SparseBooleanArray responsibleSubsystems = new SparseBooleanArray(); for (String component : components) { final Matcher matcher = sIrqPattern.matcher(component.trim()); @@ -635,13 +598,35 @@ public class CpuWakeupStats { continue; } parsedDevices[parsedDeviceCount++] = new IrqDevice(line, device); + + final List<String> rawSubsystems = deviceMap.getSubsystemsForDevice(device); + boolean anyKnownSubsystem = false; + if (rawSubsystems != null) { + for (int i = 0; i < rawSubsystems.size(); i++) { + final int subsystem = stringToKnownSubsystem(rawSubsystems.get(i)); + if (subsystem != CPU_WAKEUP_SUBSYSTEM_UNKNOWN) { + // Just in case the xml had arbitrary subsystem names, we want to + // make sure that we only put the known ones into our map. + responsibleSubsystems.put(subsystem, true); + anyKnownSubsystem = true; + } + } + } + if (!anyKnownSubsystem) { + responsibleSubsystems.put(CPU_WAKEUP_SUBSYSTEM_UNKNOWN, true); + } } } if (parsedDeviceCount == 0) { return null; } + if (responsibleSubsystems.size() == 1 && responsibleSubsystems.get( + CPU_WAKEUP_SUBSYSTEM_UNKNOWN, false)) { + // There is no attributable subsystem here, so we do not support it. + return null; + } return new Wakeup(type, Arrays.copyOf(parsedDevices, parsedDeviceCount), elapsedMillis, - uptimeMillis); + uptimeMillis, responsibleSubsystems); } @Override @@ -651,6 +636,7 @@ public class CpuWakeupStats { + ", mElapsedMillis=" + mElapsedMillis + ", mUptimeMillis=" + mUptimeMillis + ", mDevices=" + Arrays.toString(mDevices) + + ", mResponsibleSubsystems=" + mResponsibleSubsystems + '}'; } @@ -672,18 +658,28 @@ public class CpuWakeupStats { static final class Config implements DeviceConfig.OnPropertiesChangedListener { static final String KEY_WAKEUP_STATS_RETENTION_MS = "wakeup_stats_retention_ms"; + static final String KEY_WAKEUP_MATCHING_WINDOW_MS = "wakeup_matching_window_ms"; + static final String KEY_WAKING_ACTIVITY_RETENTION_MS = "waking_activity_retention_ms"; private static final String[] PROPERTY_NAMES = { KEY_WAKEUP_STATS_RETENTION_MS, + KEY_WAKEUP_MATCHING_WINDOW_MS, + KEY_WAKING_ACTIVITY_RETENTION_MS, }; static final long DEFAULT_WAKEUP_STATS_RETENTION_MS = TimeUnit.DAYS.toMillis(3); + private static final long DEFAULT_WAKEUP_MATCHING_WINDOW_MS = TimeUnit.SECONDS.toMillis(1); + private static final long DEFAULT_WAKING_ACTIVITY_RETENTION_MS = + TimeUnit.MINUTES.toMillis(5); /** * Wakeup stats are retained only for this duration. */ public volatile long WAKEUP_STATS_RETENTION_MS = DEFAULT_WAKEUP_STATS_RETENTION_MS; + public volatile long WAKEUP_MATCHING_WINDOW_MS = DEFAULT_WAKEUP_MATCHING_WINDOW_MS; + public volatile long WAKING_ACTIVITY_RETENTION_MS = DEFAULT_WAKING_ACTIVITY_RETENTION_MS; + @SuppressLint("MissingPermission") void register(Executor executor) { DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_BATTERY_STATS, executor, this); @@ -702,6 +698,15 @@ public class CpuWakeupStats { WAKEUP_STATS_RETENTION_MS = properties.getLong( KEY_WAKEUP_STATS_RETENTION_MS, DEFAULT_WAKEUP_STATS_RETENTION_MS); break; + case KEY_WAKEUP_MATCHING_WINDOW_MS: + WAKEUP_MATCHING_WINDOW_MS = properties.getLong( + KEY_WAKEUP_MATCHING_WINDOW_MS, DEFAULT_WAKEUP_MATCHING_WINDOW_MS); + break; + case KEY_WAKING_ACTIVITY_RETENTION_MS: + WAKING_ACTIVITY_RETENTION_MS = properties.getLong( + KEY_WAKING_ACTIVITY_RETENTION_MS, + DEFAULT_WAKING_ACTIVITY_RETENTION_MS); + break; } } } @@ -711,7 +716,19 @@ public class CpuWakeupStats { pw.increaseIndent(); - pw.print(KEY_WAKEUP_STATS_RETENTION_MS, WAKEUP_STATS_RETENTION_MS); + pw.print(KEY_WAKEUP_STATS_RETENTION_MS); + pw.print("="); + TimeUtils.formatDuration(WAKEUP_STATS_RETENTION_MS, pw); + pw.println(); + + pw.print(KEY_WAKEUP_MATCHING_WINDOW_MS); + pw.print("="); + TimeUtils.formatDuration(WAKEUP_MATCHING_WINDOW_MS, pw); + pw.println(); + + pw.print(KEY_WAKING_ACTIVITY_RETENTION_MS); + pw.print("="); + TimeUtils.formatDuration(WAKING_ACTIVITY_RETENTION_MS, pw); pw.println(); pw.decreaseIndent(); diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java index a694f5f1b629..ecd5bd22cd03 100644 --- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java +++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java @@ -274,13 +274,14 @@ public final class SensorPrivacyService extends SystemService { mHandler = new SensorPrivacyHandler(FgThread.get().getLooper(), mContext); mSensorPrivacyStateController = SensorPrivacyStateController.getInstance(); + correctStateIfNeeded(); + int[] micAndCameraOps = new int[]{OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_CAMERA, OP_PHONE_CALL_CAMERA, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO}; mAppOpsManager.startWatchingNoted(micAndCameraOps, this); mAppOpsManager.startWatchingStarted(micAndCameraOps, this); - mContext.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -313,6 +314,20 @@ public final class SensorPrivacyService extends SystemService { userId, toggleType, sensor, state.isEnabled())); } + // If sensor privacy is enabled for a sensor, but the device doesn't support sensor privacy + // for that sensor, then disable privacy + private void correctStateIfNeeded() { + mSensorPrivacyStateController.forEachState((type, user, sensor, state) -> { + if (type != TOGGLE_TYPE_SOFTWARE) { + return; + } + if (!supportsSensorToggle(TOGGLE_TYPE_SOFTWARE, sensor) && state.isEnabled()) { + setToggleSensorPrivacyUnchecked( + TOGGLE_TYPE_SOFTWARE, user, OTHER, sensor, false); + } + }); + } + @Override public void onUserRestrictionsChanged(int userId, Bundle newRestrictions, Bundle prevRestrictions) { @@ -721,15 +736,30 @@ public final class SensorPrivacyService extends SystemService { if (userId == UserHandle.USER_CURRENT) { userId = mCurrentUser; } + if (!canChangeToggleSensorPrivacy(userId, sensor)) { return; } + if (enable && !supportsSensorToggle(TOGGLE_TYPE_SOFTWARE, sensor)) { + // Do not enable sensor privacy if the device doesn't support it + return; + } setToggleSensorPrivacyUnchecked(TOGGLE_TYPE_SOFTWARE, userId, source, sensor, enable); } private void setToggleSensorPrivacyUnchecked(int toggleType, int userId, int source, int sensor, boolean enable) { + if (DEBUG) { + Log.d(TAG, "callingUid=" + Binder.getCallingUid() + + " callingPid=" + Binder.getCallingPid() + + " setToggleSensorPrivacyUnchecked(" + + "userId=" + userId + + " source=" + source + + " sensor=" + sensor + + " enable=" + enable + + ")"); + } final long[] lastChange = new long[1]; mSensorPrivacyStateController.atomic(() -> { SensorState sensorState = mSensorPrivacyStateController diff --git a/services/core/java/com/android/server/tv/TvInputHal.java b/services/core/java/com/android/server/tv/TvInputHal.java index 4bbca335b8e3..87ebdbfbf4e8 100644 --- a/services/core/java/com/android/server/tv/TvInputHal.java +++ b/services/core/java/com/android/server/tv/TvInputHal.java @@ -19,6 +19,7 @@ package com.android.server.tv; import android.hardware.tv.input.V1_0.Constants; import android.media.tv.TvInputHardwareInfo; import android.media.tv.TvStreamConfig; +import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.MessageQueue; @@ -45,12 +46,15 @@ final class TvInputHal implements Handler.Callback { Constants.EVENT_STREAM_CONFIGURATIONS_CHANGED; public static final int EVENT_FIRST_FRAME_CAPTURED = 4; + public static final int EVENT_TV_MESSAGE = 5; + public interface Callback { void onDeviceAvailable(TvInputHardwareInfo info, TvStreamConfig[] configs); void onDeviceUnavailable(int deviceId); void onStreamConfigurationChanged(int deviceId, TvStreamConfig[] configs, int cableConnectionStatus); void onFirstFrameCaptured(int deviceId, int streamId); + void onTvMessage(int deviceId, int type, Bundle data); } private native long nativeOpen(MessageQueue queue); @@ -171,6 +175,10 @@ final class TvInputHal implements Handler.Callback { mHandler.obtainMessage(EVENT_STREAM_CONFIGURATION_CHANGED, deviceId, streamId)); } + private void tvMessageReceivedFromNative(int deviceId, int type, Bundle data) { + mHandler.obtainMessage(EVENT_TV_MESSAGE, deviceId, type, data).sendToTarget(); + } + // Handler.Callback implementation @Override @@ -221,6 +229,14 @@ final class TvInputHal implements Handler.Callback { break; } + case EVENT_TV_MESSAGE: { + int deviceId = msg.arg1; + int type = msg.arg2; + Bundle data = (Bundle) msg.obj; + mCallback.onTvMessage(deviceId, type, data); + break; + } + default: Slog.e(TAG, "Unknown event: " + msg); return false; diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java index 077f8d527ab5..9cdceef006d9 100755 --- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java +++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java @@ -48,6 +48,7 @@ import android.media.tv.TvInputService.PriorityHintUseCaseType; import android.media.tv.TvStreamConfig; import android.media.tv.tunerresourcemanager.ResourceClientProfile; import android.media.tv.tunerresourcemanager.TunerResourceManager; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; @@ -59,6 +60,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.Surface; +import com.android.internal.os.SomeArgs; import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.server.SystemService; @@ -264,6 +266,17 @@ class TvInputHardwareManager implements TvInputHal.Callback { } } + @Override + public void onTvMessage(int deviceId, int type, Bundle data) { + synchronized (mLock) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = mHardwareInputIdMap.get(deviceId); + args.arg2 = data; + mHandler.obtainMessage(ListenerHandler.TV_MESSAGE_RECEIVED, type, 0, args) + .sendToTarget(); + } + } + public List<TvInputHardwareInfo> getHardwareList() { synchronized (mLock) { return Collections.unmodifiableList(mHardwareList); @@ -1224,6 +1237,7 @@ class TvInputHardwareManager implements TvInputHal.Callback { void onHdmiDeviceAdded(HdmiDeviceInfo device); void onHdmiDeviceRemoved(HdmiDeviceInfo device); void onHdmiDeviceUpdated(String inputId, HdmiDeviceInfo device); + void onTvMessage(String inputId, int type, Bundle data); } private class ListenerHandler extends Handler { @@ -1234,6 +1248,7 @@ class TvInputHardwareManager implements TvInputHal.Callback { private static final int HDMI_DEVICE_REMOVED = 5; private static final int HDMI_DEVICE_UPDATED = 6; private static final int TVINPUT_INFO_ADDED = 7; + private static final int TV_MESSAGE_RECEIVED = 8; @Override public final void handleMessage(Message msg) { @@ -1303,6 +1318,15 @@ class TvInputHardwareManager implements TvInputHal.Callback { } break; } + case TV_MESSAGE_RECEIVED: { + SomeArgs args = (SomeArgs) msg.obj; + String inputId = (String) args.arg1; + Bundle data = (Bundle) args.arg2; + int type = msg.arg1; + mListener.onTvMessage(inputId, type, data); + args.recycle(); + break; + } default: { Slog.w(TAG, "Unhandled message: " + msg); break; diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 1f5c1cf6a959..9cfdd5fb42e7 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -4146,6 +4146,28 @@ public final class TvInputManagerService extends SystemService { } } } + + @Override + public void onTvMessage(String inputId, int type, Bundle data) { + synchronized (mLock) { + UserState userState = getOrCreateUserStateLocked(mCurrentUserId); + TvInputState inputState = userState.inputMap.get(inputId); + ServiceState serviceState = userState.serviceStateMap.get(inputState.info + .getComponent()); + for (IBinder token : serviceState.sessionTokens) { + try { + SessionState sessionState = getSessionStateLocked(token, + Binder.getCallingUid(), mCurrentUserId); + if (!sessionState.isRecordingSession + && sessionState.hardwareSessionToken != null) { + sessionState.session.notifyTvMessage(type, data); + } + } catch (RemoteException e) { + Slog.e(TAG, "error in onTvMessage", e); + } + } + } + } } private static class SessionNotFoundException extends IllegalArgumentException { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 8d392a1bfe4c..9add53751e2f 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -181,6 +181,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub private final Object mLock = new Object(); /** True to enable a second engine for lock screen wallpaper when different from system wp. */ private final boolean mIsLockscreenLiveWallpaperEnabled; + /** True to support different crops for different display dimensions */ + private final boolean mIsMultiCropEnabled; /** Tracks wallpaper being migrated from system+lock to lock when setting static wp. */ WallpaperDestinationChangeHandler mPendingMigrationViaStatic; @@ -1602,6 +1604,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mIsLockscreenLiveWallpaperEnabled = SystemProperties.getBoolean("persist.wm.debug.lockscreen_live_wallpaper", false); + mIsMultiCropEnabled = + SystemProperties.getBoolean("persist.wm.debug.wallpaper_multi_crop", false); LocalServices.addService(WallpaperManagerInternal.class, new LocalService()); } @@ -3027,9 +3031,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } - final boolean fromForegroundApp = Binder.withCleanCallingIdentity(() -> - mActivityManager.getPackageImportance(callingPackage) == IMPORTANCE_FOREGROUND); - synchronized (mLock) { if (DEBUG) Slog.v(TAG, "setWallpaper which=0x" + Integer.toHexString(which)); WallpaperData wallpaper; @@ -3062,7 +3063,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub wallpaper.mSystemWasBoth = systemIsBoth; wallpaper.mWhich = which; wallpaper.setComplete = completion; - wallpaper.fromForegroundApp = fromForegroundApp; + wallpaper.fromForegroundApp = isFromForegroundApp(callingPackage); wallpaper.cropHint.set(cropHint); wallpaper.allowBackup = allowBackup; wallpaper.mWallpaperDimAmount = getWallpaperDimAmount(); @@ -3149,27 +3150,28 @@ public class WallpaperManagerService extends IWallpaperManager.Stub @SetWallpaperFlags int which, int userId) { if (isWallpaperSupported(callingPackage) && isSetWallpaperAllowed(callingPackage)) { - setWallpaperComponent(name, which, userId); + setWallpaperComponent(name, callingPackage, which, userId); } } // ToDo: Remove this version of the function @Override public void setWallpaperComponent(ComponentName name) { - setWallpaperComponent(name, UserHandle.getCallingUserId(), FLAG_SYSTEM); + setWallpaperComponent(name, "", UserHandle.getCallingUserId(), FLAG_SYSTEM); } @VisibleForTesting - void setWallpaperComponent(ComponentName name, @SetWallpaperFlags int which, int userId) { + void setWallpaperComponent(ComponentName name, String callingPackage, + @SetWallpaperFlags int which, int userId) { if (mIsLockscreenLiveWallpaperEnabled) { - setWallpaperComponentInternal(name, which, userId); + setWallpaperComponentInternal(name, callingPackage, which, userId); } else { - setWallpaperComponentInternalLegacy(name, which, userId); + setWallpaperComponentInternalLegacy(name, callingPackage, which, userId); } } - private void setWallpaperComponentInternal(ComponentName name, @SetWallpaperFlags int which, - int userIdIn) { + private void setWallpaperComponentInternal(ComponentName name, String callingPackage, + @SetWallpaperFlags int which, int userIdIn) { if (DEBUG) { Slog.v(TAG, "Setting new live wallpaper: which=" + which + ", component: " + name); } @@ -3205,6 +3207,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub newWallpaper.imageWallpaperPending = false; newWallpaper.mWhich = which; newWallpaper.mSystemWasBoth = systemIsBoth; + newWallpaper.fromForegroundApp = isFromForegroundApp(callingPackage); final WallpaperDestinationChangeHandler liveSync = new WallpaperDestinationChangeHandler( newWallpaper); @@ -3276,7 +3279,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } // TODO(b/266818039) Remove this method - private void setWallpaperComponentInternalLegacy(ComponentName name, + private void setWallpaperComponentInternalLegacy(ComponentName name, String callingPackage, @SetWallpaperFlags int which, int userId) { userId = ActivityManager.handleIncomingUser(getCallingPid(), getCallingUid(), userId, false /* all */, true /* full */, "changing live wallpaper", null /* pkg */); @@ -3316,6 +3319,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub try { wallpaper.imageWallpaperPending = false; wallpaper.mWhich = which; + wallpaper.fromForegroundApp = isFromForegroundApp(callingPackage); boolean same = changingToSame(name, wallpaper); if (bindWallpaperComponentLocked(name, false, true, wallpaper, null)) { if (!same) { @@ -3675,6 +3679,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } + private boolean isFromForegroundApp(String callingPackage) { + return Binder.withCleanCallingIdentity(() -> + mActivityManager.getPackageImportance(callingPackage) == IMPORTANCE_FOREGROUND); + } + /** * Certain user types do not support wallpapers (e.g. managed profiles). The check is * implemented through through the OP_WRITE_WALLPAPER AppOp. @@ -3723,6 +3732,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return mIsLockscreenLiveWallpaperEnabled; } + @Override + public boolean isMultiCropEnabled() { + return mIsMultiCropEnabled; + } + private void onDisplayReadyInternal(int displayId) { synchronized (mLock) { if (mLastWallpaper == null) { @@ -3973,6 +3987,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (mFallbackWallpaper != null) { dumpWallpaper(mFallbackWallpaper, pw); } + pw.print("mIsLockscreenLiveWallpaperEnabled="); + pw.println(mIsLockscreenLiveWallpaperEnabled); } } } diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index f71f3b128557..c5f63ced989c 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -294,6 +294,8 @@ class ActivityMetricsLogger { final int mProcessState; /** The oom adj score of the launching activity prior to the launch */ final int mProcessOomAdj; + /** Whether the activity is launched above a visible activity in the same task. */ + final boolean mIsInTaskActivityStart; /** Whether the last launched activity has reported drawn. */ boolean mIsDrawn; /** The latest activity to have been launched. */ @@ -330,7 +332,7 @@ class ActivityMetricsLogger { static TransitionInfo create(@NonNull ActivityRecord r, @NonNull LaunchingState launchingState, @Nullable ActivityOptions options, boolean processRunning, boolean processSwitch, int processState, int processOomAdj, - boolean newActivityCreated, int startResult) { + boolean newActivityCreated, boolean isInTaskActivityStart, int startResult) { if (startResult != START_SUCCESS && startResult != START_TASK_TO_FRONT) { return null; } @@ -345,19 +347,21 @@ class ActivityMetricsLogger { transitionType = TYPE_TRANSITION_COLD_LAUNCH; } return new TransitionInfo(r, launchingState, options, transitionType, processRunning, - processSwitch, processState, processOomAdj); + processSwitch, processState, processOomAdj, isInTaskActivityStart); } /** Use {@link TransitionInfo#create} instead to ensure the transition type is valid. */ private TransitionInfo(ActivityRecord r, LaunchingState launchingState, ActivityOptions options, int transitionType, boolean processRunning, - boolean processSwitch, int processState, int processOomAdj) { + boolean processSwitch, int processState, int processOomAdj, + boolean isInTaskActivityStart) { mLaunchingState = launchingState; mTransitionType = transitionType; mProcessRunning = processRunning; mProcessSwitch = processSwitch; mProcessState = processState; mProcessOomAdj = processOomAdj; + mIsInTaskActivityStart = isInTaskActivityStart; setLatestLaunchedActivity(r); // The launching state can be reused by consecutive launch. Its original association // shouldn't be changed by a separated transition. @@ -515,7 +519,7 @@ class ActivityMetricsLogger { } } - boolean isIntresetedToEventLog() { + boolean isInterestedToEventLog() { return type == TYPE_TRANSITION_WARM_LAUNCH || type == TYPE_TRANSITION_COLD_LAUNCH; } @@ -728,9 +732,10 @@ class ActivityMetricsLogger { return; } + final boolean isInTaskActivityStart = launchedActivity.getTask().isVisible(); final TransitionInfo newInfo = TransitionInfo.create(launchedActivity, launchingState, options, processRunning, processSwitch, processState, processOomAdj, - newActivityCreated, resultCode); + newActivityCreated, isInTaskActivityStart, resultCode); if (newInfo == null) { abort(launchingState, "unrecognized launch"); return; @@ -1042,18 +1047,23 @@ class ActivityMetricsLogger { // Take a snapshot of the transition info before sending it to the handler for logging. // This will avoid any races with other operations that modify the ActivityRecord. final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info); - if (info.isInterestingToLoggerAndObserver()) { - final long uptimeNs = info.mLaunchingState.mStartUptimeNs; - final int transitionDelay = info.mCurrentTransitionDelayMs; - final int processState = info.mProcessState; - final int processOomAdj = info.mProcessOomAdj; - mLoggerHandler.post(() -> logAppTransition( - uptimeNs, transitionDelay, infoSnapshot, isHibernating, - processState, processOomAdj)); - } - if (infoSnapshot.isIntresetedToEventLog()) { - mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot)); - } + final boolean isOpaque = info.mLastLaunchedActivity.mStyleFillsParent; + final long uptimeNs = info.mLaunchingState.mStartUptimeNs; + final int transitionDelay = info.mCurrentTransitionDelayMs; + final int processState = info.mProcessState; + final int processOomAdj = info.mProcessOomAdj; + mLoggerHandler.post(() -> { + if (info.isInterestingToLoggerAndObserver()) { + logAppTransition(uptimeNs, transitionDelay, infoSnapshot, isHibernating, + processState, processOomAdj); + } + if (info.mIsInTaskActivityStart) { + logInTaskActivityStart(infoSnapshot, isOpaque, transitionDelay); + } + if (infoSnapshot.isInterestedToEventLog()) { + logAppDisplayed(infoSnapshot); + } + }); if (info.mPendingFullyDrawn != null) { info.mPendingFullyDrawn.run(); } @@ -1158,6 +1168,22 @@ class ActivityMetricsLogger { return info != null && info.isLoading(); } + @VisibleForTesting + void logInTaskActivityStart(TransitionInfoSnapshot info, boolean isOpaque, + int transitionDelayMs) { + if (DEBUG_METRICS) { + Slog.i(TAG, "IN_TASK_ACTIVITY_STARTED " + info.launchedActivityName + + " transitionDelayMs=" + transitionDelayMs + "ms"); + } + FrameworkStatsLog.write(FrameworkStatsLog.IN_TASK_ACTIVITY_STARTED, + info.applicationInfo.uid, + getAppStartTransitionType(info.type, info.relaunched), + isOpaque, + transitionDelayMs, + info.windowsDrawnDelayMs, + TimeUnit.NANOSECONDS.toMillis(info.timestampNs)); + } + private void logAppDisplayed(TransitionInfoSnapshot info) { EventLog.writeEvent(WM_ACTIVITY_LAUNCH_TIME, info.userId, info.activityRecordIdHashCode, info.launchedActivityShortComponentName, @@ -1228,7 +1254,7 @@ class ActivityMetricsLogger { currentTimestampNs - info.mLaunchingState.mStartUptimeNs); final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info, r, (int) startupTimeMs); - if (infoSnapshot.isIntresetedToEventLog()) { + if (infoSnapshot.isInterestedToEventLog()) { mLoggerHandler.post(() -> logAppFullyDrawn(infoSnapshot)); } mLastTransitionInfo.remove(r); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 1e50f3d2b67e..839a0395b967 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -78,7 +78,6 @@ import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_ALWAYS; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_IF_ALLOWLISTED; import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_NEVER; -import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_EXCLUDE_PORTRAIT_FULLSCREEN; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM; @@ -94,6 +93,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_METADATA; import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_OVERRIDE; +import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_METADATA; import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_OVERRIDE; import static android.content.res.Configuration.ASSETS_SEQ_UNDEFINED; import static android.content.res.Configuration.EMPTY; @@ -712,7 +712,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * when running ANIM_SCENE_TRANSITION. * @see WindowContainer#providesOrientation() */ - private final boolean mStyleFillsParent; + final boolean mStyleFillsParent; // The input dispatching timeout for this application token in milliseconds. long mInputDispatchingTimeoutMillis = DEFAULT_DISPATCHING_TIMEOUT_MILLIS; @@ -1299,7 +1299,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A + info.getManifestMinAspectRatio()); } pw.println(prefix + "supportsSizeChanges=" - + ActivityInfo.sizeChangesSupportModeToString(info.supportsSizeChanges())); + + ActivityInfo.sizeChangesSupportModeToString(supportsSizeChanges())); if (info.configChanges != 0) { pw.println(prefix + "configChanges=0x" + Integer.toHexString(info.configChanges)); } @@ -8128,7 +8128,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * aspect ratio. */ boolean shouldCreateCompatDisplayInsets() { - switch (info.supportsSizeChanges()) { + switch (supportsSizeChanges()) { case SIZE_CHANGES_SUPPORTED_METADATA: case SIZE_CHANGES_SUPPORTED_OVERRIDE: return false; @@ -8155,6 +8155,26 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A && isActivityTypeStandardOrUndefined(); } + /** + * Returns whether the activity supports size changes. + */ + @ActivityInfo.SizeChangesSupportMode + private int supportsSizeChanges() { + if (mLetterboxUiController.shouldOverrideForceNonResizeApp()) { + return SIZE_CHANGES_UNSUPPORTED_OVERRIDE; + } + + if (info.supportsSizeChanges) { + return SIZE_CHANGES_SUPPORTED_METADATA; + } + + if (mLetterboxUiController.shouldOverrideForceResizeApp()) { + return SIZE_CHANGES_SUPPORTED_OVERRIDE; + } + + return SIZE_CHANGES_UNSUPPORTED_METADATA; + } + @Override boolean hasSizeCompatBounds() { return mSizeCompatBounds != null; @@ -8446,21 +8466,23 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A private void updateResolvedBoundsPosition(Configuration newParentConfiguration) { final Configuration resolvedConfig = getResolvedOverrideConfiguration(); final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); + if (resolvedBounds.isEmpty()) { + return; + } final Rect screenResolvedBounds = mSizeCompatBounds != null ? mSizeCompatBounds : resolvedBounds; final Rect parentAppBounds = newParentConfiguration.windowConfiguration.getAppBounds(); final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds(); - if (resolvedBounds.isEmpty()) { - return; - } + final float screenResolvedBoundsWidth = screenResolvedBounds.width(); + final float parentAppBoundsWidth = parentAppBounds.width(); // Horizontal position int offsetX = 0; - if (parentBounds.width() != screenResolvedBounds.width()) { - if (screenResolvedBounds.width() <= parentAppBounds.width()) { + if (parentBounds.width() != screenResolvedBoundsWidth) { + if (screenResolvedBoundsWidth <= parentAppBoundsWidth) { float positionMultiplier = mLetterboxUiController.getHorizontalPositionMultiplier( newParentConfiguration); - offsetX = Math.max(0, (int) Math.ceil((parentAppBounds.width() - - screenResolvedBounds.width()) * positionMultiplier) + offsetX = Math.max(0, (int) Math.ceil((parentAppBoundsWidth + - screenResolvedBoundsWidth) * positionMultiplier) // This is added to make sure that insets added inside // CompatDisplayInsets#getContainerBounds() do not break the alignment // provided by the positionMultiplier @@ -8468,14 +8490,21 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + final float parentAppBoundsHeight = parentAppBounds.height(); + final float parentBoundsHeight = parentBounds.height(); + final float screenResolvedBoundsHeight = screenResolvedBounds.height(); // Vertical position int offsetY = 0; - if (parentBounds.height() != screenResolvedBounds.height()) { - if (screenResolvedBounds.height() <= parentAppBounds.height()) { + if (parentBoundsHeight != screenResolvedBoundsHeight) { + if (screenResolvedBoundsHeight <= parentAppBoundsHeight) { float positionMultiplier = mLetterboxUiController.getVerticalPositionMultiplier( newParentConfiguration); - offsetY = Math.max(0, (int) Math.ceil((parentAppBounds.height() - - screenResolvedBounds.height()) * positionMultiplier) + // If in immersive mode, always align to bottom and overlap bottom insets (nav bar, + // task bar) as they are transient and hidden. This removes awkward bottom spacing. + final float newHeight = mDisplayContent.getDisplayPolicy().isImmersiveMode() + ? parentBoundsHeight : parentAppBoundsHeight; + offsetY = Math.max(0, (int) Math.ceil((newHeight + - screenResolvedBoundsHeight) * positionMultiplier) // This is added to make sure that insets added inside // CompatDisplayInsets#getContainerBounds() do not break the alignment // provided by the positionMultiplier @@ -9323,8 +9352,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (info.applicationInfo == null) { return info.getMinAspectRatio(); } - - if (!info.isChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO)) { + if (!mLetterboxUiController.shouldOverrideMinAspectRatio()) { return info.getMinAspectRatio(); } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index e8acbe43689c..3e3eb570d0e3 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2929,7 +2929,8 @@ class ActivityStarter { // the adjacent task as its launch target. So the existing task will be launched into the // closer one and won't be reparent redundantly. final Task adjacentTargetTask = mTargetRootTask.getAdjacentTask(); - if (adjacentTargetTask != null && intentActivity.isDescendantOf(adjacentTargetTask)) { + if (adjacentTargetTask != null && intentActivity.isDescendantOf(adjacentTargetTask) + && intentTask.isOnTop()) { mTargetRootTask = adjacentTargetTask; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 7926216fe15d..e07c65426278 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -262,12 +262,6 @@ public abstract class ActivityTaskManagerInternal { */ public abstract void setVr2dDisplayId(int vr2dDisplayId); - /** - * Set focus on an activity. - * @param token The activity token. - */ - public abstract void setFocusedActivity(IBinder token); - public abstract void registerScreenObserver(ScreenObserver observer); /** @@ -585,6 +579,11 @@ public abstract class ActivityTaskManagerInternal { public abstract void setDeviceOwnerUid(int uid); /** + * Called by DevicePolicyManagerService to set the uids of the profile owners. + */ + public abstract void setProfileOwnerUids(Set<Integer> uids); + + /** * Set all associated companion app that belongs to a userId. * @param userId * @param companionAppUids ActivityTaskManager will take ownership of this Set, the caller diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index f35343c98711..cff65547c673 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -158,6 +158,7 @@ import android.app.PictureInPictureUiState; import android.app.ProfilerInfo; import android.app.WaitResult; import android.app.admin.DevicePolicyCache; +import android.app.admin.DeviceStateCache; import android.app.assist.ActivityId; import android.app.assist.AssistContent; import android.app.assist.AssistStructure; @@ -783,6 +784,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { private int mDeviceOwnerUid = Process.INVALID_UID; + private Set<Integer> mProfileOwnerUids = new ArraySet<Integer>(); + private final class SettingObserver extends ContentObserver { private final Uri mFontScaleUri = Settings.System.getUriFor(FONT_SCALE); private final Uri mHideErrorDialogsUri = Settings.Global.getUriFor(HIDE_ERROR_DIALOGS); @@ -5360,6 +5363,15 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mDeviceOwnerUid = uid; } + boolean isAffiliatedProfileOwner(int uid) { + return uid >= 0 && mProfileOwnerUids.contains(uid) + && DeviceStateCache.getInstance().hasAffiliationWithDevice(UserHandle.getUserId(uid)); + } + + void setProfileOwnerUids(Set<Integer> uids) { + mProfileOwnerUids = uids; + } + /** * Saves the current activity manager state and includes the saved state in the next dump of * activity manager. @@ -5806,26 +5818,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public void setFocusedActivity(IBinder token) { - synchronized (mGlobalLock) { - final ActivityRecord r = ActivityRecord.forTokenLocked(token); - if (r == null) { - throw new IllegalArgumentException( - "setFocusedActivity: No activity record matching token=" + token); - } - if (r.moveFocusableActivityToTop("setFocusedActivity")) { - mRootWindowContainer.resumeFocusedTasksTopActivities(); - } - } - } - - @Override public int getDisplayId(IBinder token) { synchronized (mGlobalLock) { ActivityRecord r = ActivityRecord.forTokenLocked(token); if (r == null) { throw new IllegalArgumentException( - "setFocusedActivity: No activity record matching token=" + token); + "getDisplayId: No activity record matching token=" + token); } return r.getDisplayId(); } @@ -6916,6 +6914,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override + public void setProfileOwnerUids(Set<Integer> uids) { + synchronized (mGlobalLock) { + ActivityTaskManagerService.this.setProfileOwnerUids(uids); + } + } + + @Override public void setCompanionAppUids(int userId, Set<Integer> companionAppUids) { synchronized (mGlobalLock) { mCompanionAppUidsMap.put(userId, companionAppUids); diff --git a/services/core/java/com/android/server/wm/AnrController.java b/services/core/java/com/android/server/wm/AnrController.java index 90ec964e2f0f..2df601fd1d3b 100644 --- a/services/core/java/com/android/server/wm/AnrController.java +++ b/services/core/java/com/android/server/wm/AnrController.java @@ -34,6 +34,7 @@ import android.util.SparseArray; import android.view.InputApplicationHandle; import com.android.internal.os.TimeoutRecord; +import com.android.server.FgThread; import com.android.server.am.StackTracesDumpHelper; import com.android.server.criticalevents.CriticalEventLog; @@ -68,7 +69,9 @@ class AnrController { TimeoutRecord timeoutRecord) { try { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "notifyAppUnresponsive()"); + timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowStarted(); preDumpIfLockTooSlow(); + timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowEnded(); final ActivityRecord activity; timeoutRecord.mLatencyTracker.waitingOnGlobalLockStarted(); boolean blamePendingFocusRequest = false; @@ -108,7 +111,7 @@ class AnrController { if (!blamePendingFocusRequest) { Slog.i(TAG_WM, "ANR in " + activity.getName() + ". Reason: " + timeoutRecord.mReason); - dumpAnrStateLocked(activity, null /* windowState */, timeoutRecord.mReason); + dumpAnrStateAsync(activity, null /* windowState */, timeoutRecord.mReason); mUnresponsiveAppByDisplay.put(activity.getDisplayId(), activity); } } @@ -159,7 +162,9 @@ class AnrController { */ private boolean notifyWindowUnresponsive(@NonNull IBinder inputToken, TimeoutRecord timeoutRecord) { + timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowStarted(); preDumpIfLockTooSlow(); + timeoutRecord.mLatencyTracker.preDumpIfLockTooSlowEnded(); final int pid; final boolean aboveSystem; final ActivityRecord activity; @@ -178,7 +183,7 @@ class AnrController { ? windowState.mActivityRecord : null; Slog.i(TAG_WM, "ANR in " + target + ". Reason:" + timeoutRecord.mReason); aboveSystem = isWindowAboveSystem(windowState); - dumpAnrStateLocked(activity, windowState, timeoutRecord.mReason); + dumpAnrStateAsync(activity, windowState, timeoutRecord.mReason); } if (activity != null) { activity.inputDispatchingTimedOut(timeoutRecord, pid); @@ -197,7 +202,7 @@ class AnrController { timeoutRecord.mLatencyTracker.waitingOnGlobalLockStarted(); synchronized (mService.mGlobalLock) { timeoutRecord.mLatencyTracker.waitingOnGlobalLockEnded(); - dumpAnrStateLocked(null /* activity */, null /* windowState */, timeoutRecord.mReason); + dumpAnrStateAsync(null /* activity */, null /* windowState */, timeoutRecord.mReason); } // We cannot determine the z-order of the window, so place the anr dialog as high @@ -351,15 +356,23 @@ class AnrController { } - private void dumpAnrStateLocked(ActivityRecord activity, WindowState windowState, - String reason) { - try { - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "dumpAnrStateLocked()"); - mService.saveANRStateLocked(activity, windowState, reason); - mService.mAtmService.saveANRState(reason); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - } + /** + * Executes asynchronously on the fg thread not to block the stack dump for + * the ANRing processes. + */ + private void dumpAnrStateAsync(ActivityRecord activity, WindowState windowState, + String reason) { + FgThread.getExecutor().execute(() -> { + try { + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "dumpAnrStateLocked()"); + synchronized (mService.mGlobalLock) { + mService.saveANRStateLocked(activity, windowState, reason); + mService.mAtmService.saveANRState(reason); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + } + }); } private boolean isWindowAboveSystem(@NonNull WindowState windowState) { diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index 778951a545fa..98ee98ba67f7 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -248,7 +248,10 @@ class BLASTSyncEngine { Slog.e(TAG, "WM sent Transaction to organized, but never received" + " commit callback. Application ANR likely to follow."); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - onCommitted(merged); + synchronized (mWm.mGlobalLock) { + onCommitted(merged.mNativeObject != 0 + ? merged : mWm.mTransactionFactory.get()); + } } }; CommitCallback callback = new CommitCallback(); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 1a3d6730fe20..dc49e8cea18b 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -342,6 +342,14 @@ public class BackgroundActivityStartController { /*background*/ true, callingUid, realCallingUid, intent, "Device Owner"); } + // don't abort if the callingUid is a affiliated profile owner + if (mService.isAffiliatedProfileOwner(callingUid)) { + return logStartAllowedAndReturnCode( + BAL_ALLOW_ALLOWLISTED_COMPONENT, + resultIfPiSenderAllowsBal, balAllowedByPiSender, + /*background*/ true, callingUid, realCallingUid, + intent, "Affiliated Profile Owner"); + } // don't abort if the callingUid has companion device final int callingUserId = UserHandle.getUserId(callingUid); if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) { diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index b808a55d2952..6a7e76411fee 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -276,6 +276,12 @@ final class ContentRecorder implements WindowContainerListener { return; } + if (mContentRecordingSession.isWaitingForConsent()) { + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: waiting to record, so do " + + "nothing"); + return; + } + mRecordedWindowContainer = retrieveRecordedWindowContainer(); if (mRecordedWindowContainer == null) { // Either the token is missing, or the window associated with the token is missing. diff --git a/services/core/java/com/android/server/wm/ContentRecordingController.java b/services/core/java/com/android/server/wm/ContentRecordingController.java index 040da8862c74..f24ba5a45885 100644 --- a/services/core/java/com/android/server/wm/ContentRecordingController.java +++ b/services/core/java/com/android/server/wm/ContentRecordingController.java @@ -56,8 +56,8 @@ final class ContentRecordingController { * Updates the current recording session. * <p>Handles the following scenarios: * <ul> - * <li>Invalid scenarios: The incoming session is malformed, or the incoming session is - * identical to the current session</li> + * <li>Invalid scenarios: The incoming session is malformed.</li> + * <li>Ignored scenario: the incoming session is identical to the current session.</li> * <li>Start Scenario: Starting a new session. Recording begins immediately.</li> * <li>Takeover Scenario: Occurs during a Start Scenario, if a pre-existing session was * in-progress. For example, recording on VirtualDisplay "app_foo" was ongoing. A @@ -66,6 +66,8 @@ final class ContentRecordingController { * begin.</li> * <li>Stopping scenario: The incoming session is null and there is currently an ongoing * session. The controller stops recording.</li> + * <li>Updating scenario: There is an update for the same display, where recording + * was previously not taking place but is now permitted to go ahead.</li> * </ul> * * @param incomingSession The incoming recording session (either an update to a current session @@ -78,32 +80,50 @@ final class ContentRecordingController { if (incomingSession != null && !ContentRecordingSession.isValid(incomingSession)) { return; } - // Invalid scenario: ignore identical incoming session. + final boolean hasSessionUpdatedWithConsent = + mSession != null && incomingSession != null && mSession.isWaitingForConsent() + && !incomingSession.isWaitingForConsent(); if (ContentRecordingSession.isProjectionOnSameDisplay(mSession, incomingSession)) { - // TODO(242833866): if incoming session is no longer waiting to record, allow - // the update through. - - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Content Recording: Ignoring session on same display %d, with an existing " - + "session %s", - incomingSession.getVirtualDisplayId(), mSession.getVirtualDisplayId()); - return; + if (hasSessionUpdatedWithConsent) { + // Updating scenario: accept an incoming session updating the current display. + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Content Recording: Accept session updating same display %d with granted " + + "consent, with an existing session %s", + incomingSession.getVirtualDisplayId(), mSession.getVirtualDisplayId()); + } else { + // Ignored scenario: ignore identical incoming session. + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Content Recording: Ignoring session on same display %d, with an existing " + + "session %s", + incomingSession.getVirtualDisplayId(), mSession.getVirtualDisplayId()); + return; + } } DisplayContent incomingDisplayContent = null; - // Start scenario: recording begins immediately. if (incomingSession != null) { + // Start scenario: recording begins immediately. ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: Handle incoming session on display %d, with a " + "pre-existing session %s", incomingSession.getVirtualDisplayId(), mSession == null ? null : mSession.getVirtualDisplayId()); incomingDisplayContent = wmService.mRoot.getDisplayContentOrCreate( incomingSession.getVirtualDisplayId()); + if (incomingDisplayContent == null) { + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Content Recording: Incoming session on display %d can't be set since it " + + "is already null; the corresponding VirtualDisplay must have " + + "already been removed.", incomingSession.getVirtualDisplayId()); + return; + } incomingDisplayContent.setContentRecordingSession(incomingSession); - // TODO(b/270118861): When user grants consent to re-use, explicitly ask ContentRecorder - // to update, since no config/display change arrives. Mark recording as black. + // Updating scenario: Explicitly ask ContentRecorder to update, since no config or + // display change will trigger an update from the DisplayContent. + if (hasSessionUpdatedWithConsent) { + incomingDisplayContent.updateRecording(); + } } // Takeover and stopping scenario: stop recording on the pre-existing session. - if (mSession != null) { + if (mSession != null && !hasSessionUpdatedWithConsent) { ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: Pause the recording session on display %s", mDisplayContent.getDisplayId()); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index fa5da306d5f5..fb6d036b9eb9 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2962,6 +2962,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded(mDisplayId, mInitialDisplayWidth, mInitialDisplayHeight, newWidth, newHeight); mDisplayRotation.physicalDisplayChanged(); + mDisplayPolicy.physicalDisplayChanged(); } // If there is an override set for base values - use it, otherwise use new values. @@ -2993,6 +2994,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp reconfigureDisplayLocked(); if (physicalDisplayChanged) { + mDisplayPolicy.physicalDisplayUpdated(); mDisplaySwitchTransitionLauncher.onDisplayUpdated(currentRotation, getRotation(), getDisplayAreaInfo()); } @@ -3042,7 +3044,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp + mBaseDisplayHeight + " on display:" + getDisplayId()); } } - if (mDisplayReady) { + if (mDisplayReady && !mDisplayPolicy.shouldKeepCurrentDecorInsets()) { mDisplayPolicy.mDecorInsets.invalidate(); } } @@ -6691,9 +6693,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** * Start recording if this DisplayContent no longer has content. Stop recording if it now - * has content or the display is not on. + * has content or the display is not on. Update recording if the content has changed (for + * example, the user has granted consent to token re-use, so we can now start mirroring). */ - @VisibleForTesting void updateRecording() { + void updateRecording() { if (mContentRecorder == null || !mContentRecorder.isContentRecordingSessionSet()) { if (!setDisplayMirroring()) { return; diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index db64b43bdf8c..77e70a25d497 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -174,6 +174,10 @@ public class DisplayPolicy { private static final int INSETS_OVERRIDE_INDEX_INVALID = -1; + // TODO(b/266197298): Remove this by a more general protocol from the insets providers. + private static final boolean USE_CACHED_INSETS_FOR_DISPLAY_SWITCH = + SystemProperties.getBoolean("persist.wm.debug.cached_insets_switch", false); + private final WindowManagerService mService; private final Context mContext; private final Context mUiContext; @@ -218,6 +222,8 @@ public class DisplayPolicy { private SystemGesturesPointerEventListener mSystemGestures; final DecorInsets mDecorInsets; + /** Currently it can only be non-null when physical display switch happens. */ + private DecorInsets.Cache mCachedDecorInsets; private volatile int mLidState = LID_ABSENT; private volatile int mDockMode = Intent.EXTRA_DOCK_STATE_UNDOCKED; @@ -1248,6 +1254,10 @@ public class DisplayPolicy { return mNavigationBar; } + boolean isImmersiveMode() { + return mIsImmersiveMode; + } + /** * Control the animation to run when a window's state changes. Return a positive number to * force the animation to a specific resource ID, {@link #ANIMATION_STYLEABLE} to use the @@ -1707,11 +1717,7 @@ public class DisplayPolicy { * Called when the configuration has changed, and it's safe to load new values from resources. */ public void onConfigurationChanged() { - final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation(); - final Resources res = getCurrentUserResources(); - final int portraitRotation = displayRotation.getPortraitRotation(); - mNavBarOpacityMode = res.getInteger(R.integer.config_navBarOpacityMode); mLeftGestureInset = mGestureNavigationSettingsObserver.getLeftSensitivity(res); mRightGestureInset = mGestureNavigationSettingsObserver.getRightSensitivity(res); @@ -1882,10 +1888,11 @@ public class DisplayPolicy { @Override public String toString() { - return "{nonDecorInsets=" + mNonDecorInsets - + ", configInsets=" + mConfigInsets - + ", nonDecorFrame=" + mNonDecorFrame - + ", configFrame=" + mConfigFrame + '}'; + final StringBuilder tmpSb = new StringBuilder(32); + return "{nonDecorInsets=" + mNonDecorInsets.toShortString(tmpSb) + + ", configInsets=" + mConfigInsets.toShortString(tmpSb) + + ", nonDecorFrame=" + mNonDecorFrame.toShortString(tmpSb) + + ", configFrame=" + mConfigFrame.toShortString(tmpSb) + '}'; } } @@ -1923,6 +1930,39 @@ public class DisplayPolicy { info.mNeedUpdate = true; } } + + void setTo(DecorInsets src) { + for (int i = mInfoForRotation.length - 1; i >= 0; i--) { + mInfoForRotation[i].set(src.mInfoForRotation[i]); + } + } + + void dump(String prefix, PrintWriter pw) { + for (int rotation = 0; rotation < mInfoForRotation.length; rotation++) { + final DecorInsets.Info info = mInfoForRotation[rotation]; + pw.println(prefix + Surface.rotationToString(rotation) + "=" + info); + } + } + + private static class Cache { + /** + * If {@link #mPreserveId} is this value, it is in the middle of updating display + * configuration before a transition is started. Then the active cache should be used. + */ + static final int ID_UPDATING_CONFIG = -1; + final DecorInsets mDecorInsets; + int mPreserveId; + boolean mActive; + + Cache(DisplayContent dc) { + mDecorInsets = new DecorInsets(dc); + } + + boolean canPreserve() { + return mPreserveId == ID_UPDATING_CONFIG || mDecorInsets.mDisplayContent + .mTransitionController.inTransition(mPreserveId); + } + } } /** @@ -1930,6 +1970,9 @@ public class DisplayPolicy { * call {@link DisplayContent#sendNewConfiguration()} if this method returns {@code true}. */ boolean updateDecorInsetsInfo() { + if (shouldKeepCurrentDecorInsets()) { + return false; + } final DisplayFrames displayFrames = mDisplayContent.mDisplayFrames; final int rotation = displayFrames.mRotation; final int dw = displayFrames.mWidth; @@ -1940,6 +1983,10 @@ public class DisplayPolicy { if (newInfo.mConfigFrame.equals(currentInfo.mConfigFrame)) { return false; } + if (mCachedDecorInsets != null && !mCachedDecorInsets.canPreserve() + && !mDisplayContent.isSleeping()) { + mCachedDecorInsets = null; + } mDecorInsets.invalidate(); mDecorInsets.mInfoForRotation[rotation].set(newInfo); return true; @@ -1949,6 +1996,71 @@ public class DisplayPolicy { return mDecorInsets.get(rotation, w, h); } + /** Returns {@code true} to trust that {@link #mDecorInsets} already has the expected state. */ + boolean shouldKeepCurrentDecorInsets() { + return mCachedDecorInsets != null && mCachedDecorInsets.mActive + && mCachedDecorInsets.canPreserve(); + } + + void physicalDisplayChanged() { + if (USE_CACHED_INSETS_FOR_DISPLAY_SWITCH) { + updateCachedDecorInsets(); + } + } + + /** + * Caches the current insets and switches current insets to previous cached insets. This is to + * reduce multiple display configuration changes if there are multiple insets provider windows + * which may trigger {@link #updateDecorInsetsInfo()} individually. + */ + @VisibleForTesting + void updateCachedDecorInsets() { + DecorInsets prevCache = null; + if (mCachedDecorInsets == null) { + mCachedDecorInsets = new DecorInsets.Cache(mDisplayContent); + } else { + prevCache = new DecorInsets(mDisplayContent); + prevCache.setTo(mCachedDecorInsets.mDecorInsets); + } + // Set a special id to preserve it before a real id is available from transition. + mCachedDecorInsets.mPreserveId = DecorInsets.Cache.ID_UPDATING_CONFIG; + // Cache the current insets. + mCachedDecorInsets.mDecorInsets.setTo(mDecorInsets); + // Switch current to previous cache. + if (prevCache != null) { + mDecorInsets.setTo(prevCache); + mCachedDecorInsets.mActive = true; + } + } + + /** + * Called after the display configuration is updated according to the physical change. Suppose + * there should be a display change transition, so associate the cached decor insets with the + * transition to limit the lifetime of using the cache. + */ + void physicalDisplayUpdated() { + if (mCachedDecorInsets == null) { + return; + } + if (!mDisplayContent.mTransitionController.isCollecting()) { + // Unable to know when the display switch is finished. + mCachedDecorInsets = null; + return; + } + mCachedDecorInsets.mPreserveId = + mDisplayContent.mTransitionController.getCollectingTransitionId(); + // The validator will run after the transition is finished. So if the insets are changed + // during the transition, it can update to the latest state. + mDisplayContent.mTransitionController.mStateValidators.add(() -> { + // The insets provider client may defer to change its window until screen is on. So + // only validate when awake to avoid the cache being always dropped. + if (!mDisplayContent.isSleeping() && updateDecorInsetsInfo()) { + Slog.d(TAG, "Insets changed after display switch transition"); + mDisplayContent.sendNewConfiguration(); + } + }); + } + @NavigationBarPosition int navigationBarPosition(int displayRotation) { if (mNavigationBar != null) { @@ -2626,9 +2738,10 @@ public class DisplayPolicy { pw.print(prefix); pw.print("mRemoteInsetsControllerControlsSystemBars="); pw.println(mRemoteInsetsControllerControlsSystemBars); pw.print(prefix); pw.println("mDecorInsetsInfo:"); - for (int rotation = 0; rotation < mDecorInsets.mInfoForRotation.length; rotation++) { - final DecorInsets.Info info = mDecorInsets.mInfoForRotation[rotation]; - pw.println(prefixInner + Surface.rotationToString(rotation) + "=" + info); + mDecorInsets.dump(prefixInner, pw); + if (mCachedDecorInsets != null) { + pw.print(prefix); pw.println("mCachedDecorInsets:"); + mCachedDecorInsets.mDecorInsets.dump(prefixInner, pw); } if (!CLIENT_TRANSIENT) { mSystemGestures.dump(pw, prefix); diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index ae93a9496f7c..1fbf59301191 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -223,7 +223,7 @@ final class DisplayRotationCompatPolicy { try { activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true); ProtoLog.v(WM_DEBUG_STATES, - "Refershing activity for camera compatibility treatment, " + "Refreshing activity for camera compatibility treatment, " + "activityRecord=%s", activity); final ClientTransaction transaction = ClientTransaction.obtain( activity.app.getThread(), activity.token); @@ -245,7 +245,7 @@ final class DisplayRotationCompatPolicy { } /** - * Notifies that animation in {@link ScreenAnimationRotation} has finished. + * Notifies that animation in {@link ScreenRotationAnimation} has finished. * * <p>This class uses this signal as a trigger for notifying the user about forced rotation * reason with the {@link Toast}. @@ -311,11 +311,14 @@ final class DisplayRotationCompatPolicy { } } - // Refreshing only when configuration changes after rotation. + // Refreshing only when configuration changes after rotation or camera split screen aspect ratio + // treatment is enabled private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig, Configuration lastReportedConfig) { - return newConfig.windowConfiguration.getDisplayRotation() - != lastReportedConfig.windowConfiguration.getDisplayRotation() + final boolean displayRotationChanged = (newConfig.windowConfiguration.getDisplayRotation() + != lastReportedConfig.windowConfiguration.getDisplayRotation()); + return (displayRotationChanged + || activity.mLetterboxUiController.isCameraCompatSplitScreenAspectRatioAllowed()) && isTreatmentEnabledForActivity(activity) && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat(); } diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index 0b960ec2a583..b67ccd2a7672 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -439,7 +439,9 @@ final class InputMonitor { ? mDisplayContent.getImeInputTarget().getActivityRecord() : null; if (app != null) { mDisplayContent.removeImeSurfaceImmediately(); - mDisplayContent.mAtmService.takeTaskSnapshot(app.getTask().mTaskId); + if (app.getTask() != null) { + mDisplayContent.mAtmService.takeTaskSnapshot(app.getTask().mTaskId); + } } } else { // Disable IME icon explicitly when IME attached to the app in case diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index f492bab0728f..a93cb8ad2d97 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -214,7 +214,7 @@ final class LetterboxConfiguration { // otherwise the apps get blacked out when they are resumed and do not have focus yet. private boolean mIsCompatFakeFocusEnabled; - // Whether should use split screen aspect ratio for the activity when camera compat treatment + // Whether we should use split screen aspect ratio for the activity when camera compat treatment // is enabled and activity is connected to the camera in fullscreen. private final boolean mIsCameraCompatSplitScreenAspectRatioEnabled; @@ -1118,7 +1118,7 @@ final class LetterboxConfiguration { } /** - * Whether should use split screen aspect ratio for the activity when camera compat treatment + * Whether we should use split screen aspect ratio for the activity when camera compat treatment * is enabled and activity is connected to the camera in fullscreen. */ boolean isCameraCompatSplitScreenAspectRatioEnabled() { diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 0288e4bbb26e..d83c8612b9e9 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -17,6 +17,8 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP; +import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP; import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; @@ -25,6 +27,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE; +import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR; @@ -49,7 +52,9 @@ import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; @@ -185,10 +190,21 @@ final class LetterboxUiController { // when dealing with translucent activities. private final List<LetterboxUiController> mDestroyListeners = new ArrayList<>(); + // Corresponds to OVERRIDE_MIN_ASPECT_RATIO + private final boolean mIsOverrideMinAspectRatio; + // Corresponds to FORCE_RESIZE_APP + private final boolean mIsOverrideForceResizeApp; + // Corresponds to FORCE_NON_RESIZE_APP + private final boolean mIsOverrideForceNonResizeApp; + @Nullable private final Boolean mBooleanPropertyAllowOrientationOverride; @Nullable private final Boolean mBooleanPropertyAllowDisplayOrientationOverride; + @Nullable + private final Boolean mBooleanPropertyAllowMinAspectRatioOverride; + @Nullable + private final Boolean mBooleanPropertyAllowForceResizeOverride; /* * WindowContainerListener responsible to make translucent activities inherit @@ -300,6 +316,14 @@ final class LetterboxUiController { readComponentProperty(packageManager, mActivityRecord.packageName, /* gatingCondition */ null, PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE); + mBooleanPropertyAllowMinAspectRatioOverride = + readComponentProperty(packageManager, mActivityRecord.packageName, + /* gatingCondition */ null, + PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); + mBooleanPropertyAllowForceResizeOverride = + readComponentProperty(packageManager, mActivityRecord.packageName, + /* gatingCondition */ null, + PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES); mIsOverrideAnyOrientationEnabled = isCompatChangeEnabled(OVERRIDE_ANY_ORIENTATION); mIsOverrideToPortraitOrientationEnabled = @@ -330,6 +354,9 @@ final class LetterboxUiController { mIsOverrideEnableCompatFakeFocusEnabled = isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS); + mIsOverrideMinAspectRatio = isCompatChangeEnabled(OVERRIDE_MIN_ASPECT_RATIO); + mIsOverrideForceResizeApp = isCompatChangeEnabled(FORCE_RESIZE_APP); + mIsOverrideForceNonResizeApp = isCompatChangeEnabled(FORCE_NON_RESIZE_APP); } /** @@ -502,6 +529,61 @@ final class LetterboxUiController { } /** + * Whether we should apply the min aspect ratio per-app override. When this override is applied + * the min aspect ratio given in the app's manifest will be overridden to the largest enabled + * aspect ratio treatment unless the app's manifest value is higher. The treatment will also + * apply if no value is provided in the manifest. + * + * <p>This method returns {@code true} when the following conditions are met: + * <ul> + * <li>Opt-out component property isn't enabled + * <li>Per-app override is enabled + * </ul> + */ + boolean shouldOverrideMinAspectRatio() { + return shouldEnableWithOptInOverrideAndOptOutProperty( + /* gatingCondition */ () -> true, + mIsOverrideMinAspectRatio, + mBooleanPropertyAllowMinAspectRatioOverride); + } + + /** + * Whether we should apply the force resize per-app override. When this override is applied it + * forces the packages it is applied to to be resizable. It won't change whether the app can be + * put into multi-windowing mode, but allow the app to resize without going into size-compat + * mode when the window container resizes, such as display size change or screen rotation. + * + * <p>This method returns {@code true} when the following conditions are met: + * <ul> + * <li>Opt-out component property isn't enabled + * <li>Per-app override is enabled + * </ul> + */ + boolean shouldOverrideForceResizeApp() { + return shouldEnableWithOptInOverrideAndOptOutProperty( + /* gatingCondition */ () -> true, + mIsOverrideForceResizeApp, + mBooleanPropertyAllowForceResizeOverride); + } + + /** + * Whether we should apply the force non resize per-app override. When this override is applied + * it forces the packages it is applied to to be non-resizable. + * + * <p>This method returns {@code true} when the following conditions are met: + * <ul> + * <li>Opt-out component property isn't enabled + * <li>Per-app override is enabled + * </ul> + */ + boolean shouldOverrideForceNonResizeApp() { + return shouldEnableWithOptInOverrideAndOptOutProperty( + /* gatingCondition */ () -> true, + mIsOverrideForceNonResizeApp, + mBooleanPropertyAllowForceResizeOverride); + } + + /** * Sets whether an activity is relaunching after the app has called {@link * android.app.Activity#setRequestedOrientation}. */ @@ -958,7 +1040,7 @@ final class LetterboxUiController { * Whether we use split screen aspect ratio for the activity when camera compat treatment * is active because the corresponding config is enabled and activity supports resizing. */ - private boolean isCameraCompatSplitScreenAspectRatioAllowed() { + boolean isCameraCompatSplitScreenAspectRatioAllowed() { return mLetterboxConfiguration.isCameraCompatSplitScreenAspectRatioEnabled() && !mActivityRecord.shouldCreateCompatDisplayInsets(); } @@ -1280,6 +1362,16 @@ final class LetterboxUiController { final Rect cropBounds = new Rect(mActivityRecord.getBounds()); + // In case of translucent activities we check if the requested size is different from + // the size provided using inherited bounds. In that case we decide to not apply rounded + // corners because we assume the specific layout would. This is the case when the layout + // of the translucent activity uses only a part of all the bounds because of the use of + // LayoutParams.WRAP_CONTENT. + if (hasInheritedLetterboxBehavior() && (cropBounds.width() != mainWindow.mRequestedWidth + || cropBounds.height() != mainWindow.mRequestedHeight)) { + return null; + } + // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo} // because taskbar bounds used in {@link #adjustBoundsIfNeeded} // are in screen coordinates diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index b7c29bf071d8..bb6f8056acda 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5673,6 +5673,7 @@ class Task extends TaskFragment { } mTransitionController.requestStartTransition(transition, tr, null /* remoteTransition */, null /* displayChange */); + mTransitionController.collect(tr); moveTaskToBackInner(tr); }); } else { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 5e60e92150a9..c763cfac4e88 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -21,6 +21,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; @@ -959,8 +961,20 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { true /* beforeStopping */)) { return false; } - return mController.mAtm.enterPictureInPictureMode(ar, ar.pictureInPictureArgs, - false /* fromClient */, true /* isAutoEnter */); + final int prevMode = ar.getTask().getWindowingMode(); + final boolean inPip = mController.mAtm.enterPictureInPictureMode(ar, + ar.pictureInPictureArgs, false /* fromClient */, true /* isAutoEnter */); + final int currentMode = ar.getTask().getWindowingMode(); + if (prevMode == WINDOWING_MODE_FULLSCREEN && currentMode == WINDOWING_MODE_PINNED + && mTransientLaunches != null + && ar.mDisplayContent.hasTopFixedRotationLaunchingApp()) { + // There will be a display configuration change after finishing this transition. + // Skip dispatching the change for PiP task to avoid its activity drawing for the + // intermediate state which will cause flickering. The final PiP bounds in new + // rotation will be applied by PipTransition. + ar.mDisplayContent.mPinnedTaskController.setEnterPipTransaction(null); + } + return inPip; } // Legacy pip-entry (not via isAutoEnterEnabled). @@ -1194,6 +1208,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (asyncRotationController != null && containsChangeFor(dc, mTargets)) { asyncRotationController.onTransitionFinished(); } + if (hasParticipatedDisplay && dc.mDisplayRotationCompatPolicy != null) { + final ChangeInfo changeInfo = mChanges.get(dc); + if (changeInfo != null + && changeInfo.mRotation != dc.getWindowConfiguration().getRotation()) { + dc.mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished(); + } + } if (mTransientLaunches != null) { InsetsControlTarget prevImeTarget = dc.getImeTarget( DisplayContent.IME_TARGET_CONTROL); @@ -1449,6 +1470,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord(); if (ar == null || ar.getTask() == null || ar.getTask().isVisibleRequested()) continue; + final ChangeInfo change = mChanges.get(ar); + // Intentionally skip record snapshot for changes originated from PiP. + if (change != null && change.mWindowingMode == WINDOWING_MODE_PINNED) continue; mController.mSnapshotController.mTaskSnapshotController.recordSnapshot( ar.getTask(), false /* allowSnapshotHome */); } @@ -1509,7 +1533,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.mLoggerHandler.post(mLogger::logOnSend); if (mLogger.mInfo != null) { - mController.mTransitionTracer.logSentTransition(this, mTargets, info); + mController.mTransitionTracer.logSentTransition(this, mTargets); } } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index b697ab158003..1c6a412e5450 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -431,6 +431,19 @@ class TransitionController { return inCollectingTransition(wc) || inPlayingTransition(wc); } + /** Returns {@code true} if the id matches a collecting or playing transition. */ + boolean inTransition(int syncId) { + if (mCollectingTransition != null && mCollectingTransition.getSyncId() == syncId) { + return true; + } + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + if (mPlayingTransitions.get(i).getSyncId() == syncId) { + return true; + } + } + return false; + } + /** @return {@code true} if wc is in a participant subtree */ boolean isTransitionOnDisplay(@NonNull DisplayContent dc) { if (mCollectingTransition != null && mCollectingTransition.isOnDisplay(dc)) { @@ -503,8 +516,14 @@ class TransitionController { * playing, but can be "opened-up" for certain transition operations like calculating layers * for finishTransaction. */ - boolean canAssignLayers() { - return mBuildingFinishLayers || !isPlaying(); + boolean canAssignLayers(@NonNull WindowContainer wc) { + // Don't build window state into finish transaction in case another window is added or + // removed during transition playing. + if (mBuildingFinishLayers) { + return wc.asWindowState() == null; + } + // Always allow WindowState to assign layers since it won't affect transition. + return wc.asWindowState() != null || !isPlaying(); } @WindowConfiguration.WindowingMode diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java index a002fba0877e..af8fb0252675 100644 --- a/services/core/java/com/android/server/wm/TransitionTracer.java +++ b/services/core/java/com/android/server/wm/TransitionTracer.java @@ -29,7 +29,6 @@ import android.os.SystemClock; import android.os.Trace; import android.util.Log; import android.util.proto.ProtoOutputStream; -import android.window.TransitionInfo; import com.android.internal.util.TraceBuffer; import com.android.server.wm.Transition.ChangeInfo; @@ -49,6 +48,12 @@ public class TransitionTracer { private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB + + // This will be the size the proto output streams are initialized to. + // Ideally this should fit most or all the proto objects we will create and be no bigger than + // that to ensure to don't use excessive amounts of memory. + private static final int CHUNK_SIZE = 64; + static final String WINSCOPE_EXT = ".winscope"; private static final String TRACE_FILE = "/data/misc/wmtrace/wm_transition_trace" + WINSCOPE_EXT; @@ -69,26 +74,29 @@ public class TransitionTracer { * * @param transition The transition that has been sent to Shell. * @param targets Information about the target windows of the transition. - * @param info The TransitionInfo send over to Shell to execute the transition. */ - public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets, - TransitionInfo info) { - final ProtoOutputStream outputStream = new ProtoOutputStream(); - final long protoToken = outputStream - .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); - outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); - outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS, - transition.mLogger.mCreateTimeNs); - outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS, - transition.mLogger.mSendTimeNs); - outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID, - transition.getStartTransaction().getId()); - outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID, - transition.getFinishTransaction().getId()); - dumpTransitionTargetsToProto(outputStream, transition, targets); - outputStream.end(protoToken); - - mTraceBuffer.add(outputStream); + public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets) { + try { + final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); + final long protoToken = outputStream + .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); + outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); + outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS, + transition.mLogger.mCreateTimeNs); + outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS, + transition.mLogger.mSendTimeNs); + outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID, + transition.getStartTransaction().getId()); + outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID, + transition.getFinishTransaction().getId()); + dumpTransitionTargetsToProto(outputStream, transition, targets); + outputStream.end(protoToken); + + mTraceBuffer.add(outputStream); + } catch (Exception e) { + // Don't let any errors in the tracing cause the transition to fail + Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); + } } /** @@ -98,15 +106,20 @@ public class TransitionTracer { * @param transition The transition that has finished. */ public void logFinishedTransition(Transition transition) { - final ProtoOutputStream outputStream = new ProtoOutputStream(); - final long protoToken = outputStream - .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); - outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); - outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS, - transition.mLogger.mFinishTimeNs); - outputStream.end(protoToken); - - mTraceBuffer.add(outputStream); + try { + final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); + final long protoToken = outputStream + .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); + outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); + outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS, + transition.mLogger.mFinishTimeNs); + outputStream.end(protoToken); + + mTraceBuffer.add(outputStream); + } catch (Exception e) { + // Don't let any errors in the tracing cause the transition to fail + Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); + } } /** @@ -116,15 +129,20 @@ public class TransitionTracer { * @param transition The transition that has been aborted */ public void logAbortedTransition(Transition transition) { - final ProtoOutputStream outputStream = new ProtoOutputStream(); - final long protoToken = outputStream - .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); - outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); - outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS, - transition.mLogger.mAbortTimeNs); - outputStream.end(protoToken); - - mTraceBuffer.add(outputStream); + try { + final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); + final long protoToken = outputStream + .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); + outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); + outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS, + transition.mLogger.mAbortTimeNs); + outputStream.end(protoToken); + + mTraceBuffer.add(outputStream); + } catch (Exception e) { + // Don't let any errors in the tracing cause the transition to fail + Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); + } } private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream, @@ -240,7 +258,7 @@ public class TransitionTracer { private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) { Trace.beginSection("TransitionTracer#writeTraceToFileLocked"); try { - ProtoOutputStream proto = new ProtoOutputStream(); + ProtoOutputStream proto = new ProtoOutputStream(CHUNK_SIZE); proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); long timeOffsetNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index be5f141b3762..0152666a830d 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -2596,7 +2596,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< void assignLayer(Transaction t, int layer) { // Don't assign layers while a transition animation is playing // TODO(b/173528115): establish robust best-practices around z-order fighting. - if (!mTransitionController.canAssignLayers()) return; + if (!mTransitionController.canAssignLayers(this)) return; final boolean changed = layer != mLastLayer || mLastRelativeToLayer != null; if (mSurfaceControl != null && changed) { setLayer(t, layer); diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 792ec2e92083..92d4cec80cc9 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -449,6 +449,19 @@ public abstract class WindowManagerInternal { public abstract void moveDisplayToTopIfAllowed(int displayId); /** + * Request to move window input focus to the window with the provided window token. + * + * <p> + * It is necessary to move window input focus before certain actions on views in a window can + * be performed, such as opening an IME. Input normally requests to move focus on window touch + * so this method should not be necessary in most cases; only features that bypass normal touch + * behavior (like Accessibility actions) require this method. + * </p> + * @param windowToken The window token. + */ + public abstract void requestWindowFocus(IBinder windowToken); + + /** * @return Whether the keyguard is engaged. */ public abstract boolean isKeyguardLocked(); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 25b7df4eda08..322c11a7cc4b 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -5652,7 +5652,7 @@ public class WindowManagerService extends IWindowManager.Stub case ON_POINTER_DOWN_OUTSIDE_FOCUS: { synchronized (mGlobalLock) { final IBinder touchedToken = (IBinder) msg.obj; - onPointerDownOutsideFocusLocked(touchedToken); + onPointerDownOutsideFocusLocked(getInputTargetFromToken(touchedToken)); } break; } @@ -7752,6 +7752,15 @@ public class WindowManagerService extends IWindowManager.Stub } @Override + public void requestWindowFocus(IBinder windowToken) { + synchronized (mGlobalLock) { + final InputTarget inputTarget = + WindowManagerService.this.getInputTargetFromWindowTokenLocked(windowToken); + WindowManagerService.this.onPointerDownOutsideFocusLocked(inputTarget); + } + } + + @Override public boolean isKeyguardLocked() { return WindowManagerService.this.isKeyguardLocked(); } @@ -8590,8 +8599,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - private void onPointerDownOutsideFocusLocked(IBinder touchedToken) { - InputTarget t = getInputTargetFromToken(touchedToken); + private void onPointerDownOutsideFocusLocked(InputTarget t) { if (t == null || !t.receiveFocusFromTapOutside()) { // If the window that received the input event cannot receive keys, don't move the // display it's on to the top since that window won't be able to get focus anyway. @@ -9014,7 +9022,7 @@ public class WindowManagerService extends IWindowManager.Stub final SurfaceControl mirror = SurfaceControl.mirrorSurface(displaySc); outSurfaceControl.copyFrom(mirror, "WMS.mirrorDisplay"); - + mirror.release(); return true; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index ba942821f244..7cd7f6975bbd 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1932,7 +1932,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final ActivityRecord atoken = mActivityRecord; if (atoken != null) { return ((!isParentWindowHidden() && atoken.isVisible()) - || isAnimating(TRANSITION | PARENTS)); + || isAnimationRunningSelfOrParent()); } final WallpaperWindowToken wtoken = mToken.asWallpaperToken(); if (wtoken != null) { diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 2ce86add4e58..d5217c8295bd 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -109,6 +109,7 @@ cc_defaults { "libchrome", "libcutils", "libcrypto", + "libfmq", "liblog", "libgraphicsenv", "libgralloctypes", @@ -157,6 +158,7 @@ cc_defaults { "android.hardware.broadcastradio@1.0", "android.hardware.broadcastradio@1.1", "android.hardware.contexthub@1.0", + "android.hardware.common.fmq-V1-ndk", "android.hardware.gnss-V3-cpp", "android.hardware.gnss@1.0", "android.hardware.gnss@1.1", diff --git a/services/core/jni/com_android_server_display_DisplayControl.cpp b/services/core/jni/com_android_server_display_DisplayControl.cpp index eb8b6b82ec25..e65b9030195a 100644 --- a/services/core/jni/com_android_server_display_DisplayControl.cpp +++ b/services/core/jni/com_android_server_display_DisplayControl.cpp @@ -99,7 +99,10 @@ static jintArray nativeGetSupportedHdrOutputTypes(JNIEnv* env, jclass clazz) { // Extract unique HDR output types. std::set<int> hdrOutputTypes; for (const auto& hdrConversionCapability : hdrConversionCapabilities) { - hdrOutputTypes.insert(hdrConversionCapability.outputType); + // Filter out the value for SDR which is 0. + if (hdrConversionCapability.outputType > 0) { + hdrOutputTypes.insert(hdrConversionCapability.outputType); + } } jintArray array = env->NewIntArray(hdrOutputTypes.size()); if (array == nullptr) { diff --git a/services/core/jni/com_android_server_tv_TvInputHal.cpp b/services/core/jni/com_android_server_tv_TvInputHal.cpp index a8806b5ef643..1e6384031f9a 100644 --- a/services/core/jni/com_android_server_tv_TvInputHal.cpp +++ b/services/core/jni/com_android_server_tv_TvInputHal.cpp @@ -20,6 +20,7 @@ #include "tvinput/JTvInputHal.h" +gBundleClassInfoType gBundleClassInfo; gTvInputHalClassInfoType gTvInputHalClassInfo; gTvStreamConfigClassInfoType gTvStreamConfigClassInfo; gTvStreamConfigBuilderClassInfoType gTvStreamConfigBuilderClassInfo; @@ -133,10 +134,22 @@ int register_android_server_tv_TvInputHal(JNIEnv* env) { GET_METHOD_ID( gTvInputHalClassInfo.firstFrameCaptured, clazz, "firstFrameCapturedFromNative", "(II)V"); + GET_METHOD_ID(gTvInputHalClassInfo.tvMessageReceived, clazz, "tvMessageReceivedFromNative", + "(IILandroid/os/Bundle;)V"); FIND_CLASS(gTvStreamConfigClassInfo.clazz, "android/media/tv/TvStreamConfig"); gTvStreamConfigClassInfo.clazz = jclass(env->NewGlobalRef(gTvStreamConfigClassInfo.clazz)); + FIND_CLASS(gBundleClassInfo.clazz, "android/os/Bundle"); + gBundleClassInfo.clazz = jclass(env->NewGlobalRef(gBundleClassInfo.clazz)); + GET_METHOD_ID(gBundleClassInfo.constructor, gBundleClassInfo.clazz, "<init>", "()V"); + GET_METHOD_ID(gBundleClassInfo.putByteArray, gBundleClassInfo.clazz, "putByteArray", + "(Ljava/lang/String;[B)V"); + GET_METHOD_ID(gBundleClassInfo.putString, gBundleClassInfo.clazz, "putString", + "(Ljava/lang/String;Ljava/lang/String;)V"); + GET_METHOD_ID(gBundleClassInfo.putInt, gBundleClassInfo.clazz, "putInt", + "(Ljava/lang/String;I)V"); + FIND_CLASS(gTvStreamConfigBuilderClassInfo.clazz, "android/media/tv/TvStreamConfig$Builder"); gTvStreamConfigBuilderClassInfo.clazz = jclass(env->NewGlobalRef(gTvStreamConfigBuilderClassInfo.clazz)); diff --git a/services/core/jni/tvinput/JTvInputHal.cpp b/services/core/jni/tvinput/JTvInputHal.cpp index 6bb52174fcb2..6782b5c8d784 100644 --- a/services/core/jni/tvinput/JTvInputHal.cpp +++ b/services/core/jni/tvinput/JTvInputHal.cpp @@ -16,6 +16,8 @@ #include "JTvInputHal.h" +#include <nativehelper/ScopedLocalRef.h> + namespace android { JTvInputHal::JTvInputHal(JNIEnv* env, jobject thiz, std::shared_ptr<ITvInputWrapper> tvInput, @@ -275,6 +277,32 @@ void JTvInputHal::onStreamConfigurationsChanged(int deviceId, int cableConnectio cableConnectionStatus); } +void JTvInputHal::onTvMessage(int deviceId, int streamId, AidlTvMessageEventType type, + AidlTvMessage& message, signed char data[], int dataLength) { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + ScopedLocalRef<jobject> bundle(env, + env->NewObject(gBundleClassInfo.clazz, + gBundleClassInfo.constructor)); + ScopedLocalRef<jbyteArray> convertedData(env, env->NewByteArray(dataLength)); + env->SetByteArrayRegion(convertedData.get(), 0, dataLength, reinterpret_cast<jbyte*>(data)); + std::string key = "android.media.tv.TvInputManager.raw_data"; + ScopedLocalRef<jstring> jkey(env, env->NewStringUTF(key.c_str())); + env->CallVoidMethod(bundle.get(), gBundleClassInfo.putByteArray, jkey.get(), + convertedData.get()); + ScopedLocalRef<jstring> subtype(env, env->NewStringUTF(message.subType.c_str())); + key = "android.media.tv.TvInputManager.subtype"; + jkey = ScopedLocalRef<jstring>(env, env->NewStringUTF(key.c_str())); + env->CallVoidMethod(bundle.get(), gBundleClassInfo.putString, jkey.get(), subtype.get()); + key = "android.media.tv.TvInputManager.group_id"; + jkey = ScopedLocalRef<jstring>(env, env->NewStringUTF(key.c_str())); + env->CallVoidMethod(bundle.get(), gBundleClassInfo.putInt, jkey.get(), message.groupId); + key = "android.media.tv.TvInputManager.stream_id"; + jkey = ScopedLocalRef<jstring>(env, env->NewStringUTF(key.c_str())); + env->CallVoidMethod(bundle.get(), gBundleClassInfo.putInt, jkey.get(), streamId); + env->CallVoidMethod(mThiz, gTvInputHalClassInfo.tvMessageReceived, deviceId, + static_cast<jint>(type), bundle.get()); +} + void JTvInputHal::onCaptured(int deviceId, int streamId, uint32_t seq, bool succeeded) { sp<BufferProducerThread> thread; { @@ -316,6 +344,17 @@ JTvInputHal::TvInputEventWrapper JTvInputHal::TvInputEventWrapper::createEventWr return event; } +JTvInputHal::TvMessageEventWrapper JTvInputHal::TvMessageEventWrapper::createEventWrapper( + const AidlTvMessageEvent& aidlTvMessageEvent) { + TvMessageEventWrapper event; + event.messages.insert(event.messages.begin(), std::begin(aidlTvMessageEvent.messages) + 1, + std::end(aidlTvMessageEvent.messages)); + event.streamId = aidlTvMessageEvent.streamId; + event.deviceId = aidlTvMessageEvent.messages[0].groupId; + event.type = aidlTvMessageEvent.type; + return event; +} + JTvInputHal::NotifyHandler::NotifyHandler(JTvInputHal* hal, const TvInputEventWrapper& event) { mHal = hal; mEvent = event; @@ -338,6 +377,41 @@ void JTvInputHal::NotifyHandler::handleMessage(const Message& message) { } } +JTvInputHal::NotifyTvMessageHandler::NotifyTvMessageHandler(JTvInputHal* hal, + const TvMessageEventWrapper& event) { + mHal = hal; + mEvent = event; +} + +void JTvInputHal::NotifyTvMessageHandler::handleMessage(const Message& message) { + std::shared_ptr<AidlMessageQueue<int8_t, SynchronizedReadWrite>> queue = + mHal->mQueueMap[mEvent.deviceId][mEvent.streamId]; + for (AidlTvMessage item : mEvent.messages) { + if (queue == NULL || !queue->isValid() || queue->availableToRead() < item.dataLengthBytes) { + MQDescriptor<int8_t, SynchronizedReadWrite> queueDesc; + if (mHal->mTvInput->getTvMessageQueueDesc(&queueDesc, mEvent.deviceId, mEvent.streamId) + .isOk()) { + queue = std::make_shared<AidlMessageQueue<int8_t, SynchronizedReadWrite>>(queueDesc, + false); + } + if (queue == NULL || !queue->isValid() || + queue->availableToRead() < item.dataLengthBytes) { + ALOGE("Incomplete TvMessageQueue data or missing queue"); + return; + } + mHal->mQueueMap[mEvent.deviceId][mEvent.streamId] = queue; + } + signed char* buffer = new signed char[item.dataLengthBytes]; + if (queue->read(buffer, item.dataLengthBytes)) { + mHal->onTvMessage(mEvent.deviceId, mEvent.streamId, mEvent.type, item, buffer, + item.dataLengthBytes); + } else { + ALOGE("Failed to read from TvMessageQueue"); + } + delete[] buffer; + } +} + JTvInputHal::TvInputCallback::TvInputCallback(JTvInputHal* hal) { mHal = hal; } @@ -351,7 +425,15 @@ JTvInputHal::TvInputCallback::TvInputCallback(JTvInputHal* hal) { ::ndk::ScopedAStatus JTvInputHal::TvInputCallback::notifyTvMessageEvent( const AidlTvMessageEvent& event) { - // TODO: Implement this + const std::string DEVICE_ID_SUBTYPE = "device_id"; + if (event.messages.size() > 1 && event.messages[0].subType == DEVICE_ID_SUBTYPE) { + mHal->mLooper + ->sendMessage(new NotifyTvMessageHandler(mHal, + TvMessageEventWrapper::createEventWrapper( + event)), + static_cast<int>(event.type)); + } + return ::ndk::ScopedAStatus::ok(); } @@ -406,4 +488,14 @@ JTvInputHal::ITvInputWrapper::ITvInputWrapper(std::shared_ptr<AidlITvInput>& aid } } +::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::getTvMessageQueueDesc( + MQDescriptor<int8_t, SynchronizedReadWrite>* out_queue, int32_t in_deviceId, + int32_t in_streamId) { + if (mIsHidl) { + return ::ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION); + } else { + return mAidlTvInput->getTvMessageQueueDesc(out_queue, in_deviceId, in_streamId); + } +} + } // namespace android diff --git a/services/core/jni/tvinput/JTvInputHal.h b/services/core/jni/tvinput/JTvInputHal.h index e29da79d62dd..e8b1c9931372 100644 --- a/services/core/jni/tvinput/JTvInputHal.h +++ b/services/core/jni/tvinput/JTvInputHal.h @@ -24,6 +24,7 @@ #include <aidl/android/media/audio/common/AudioDevice.h> #include <aidlcommonsupport/NativeHandle.h> #include <android/binder_manager.h> +#include <fmq/AidlMessageQueue.h> #include <nativehelper/JNIHelp.h> #include <utils/Errors.h> #include <utils/KeyedVector.h> @@ -40,6 +41,10 @@ #include "android_runtime/android_view_Surface.h" #include "tvinput/jstruct.h" +using ::android::AidlMessageQueue; + +using ::aidl::android::hardware::common::fmq::MQDescriptor; +using ::aidl::android::hardware::common::fmq::SynchronizedReadWrite; using ::aidl::android::hardware::tv::input::BnTvInputCallback; using ::aidl::android::hardware::tv::input::CableConnectionStatus; using ::aidl::android::hardware::tv::input::TvInputEventType; @@ -54,10 +59,16 @@ using AidlITvInput = ::aidl::android::hardware::tv::input::ITvInput; using AidlNativeHandle = ::aidl::android::hardware::common::NativeHandle; using AidlTvInputDeviceInfo = ::aidl::android::hardware::tv::input::TvInputDeviceInfo; using AidlTvInputEvent = ::aidl::android::hardware::tv::input::TvInputEvent; +using AidlTvMessage = ::aidl::android::hardware::tv::input::TvMessage; using AidlTvMessageEvent = ::aidl::android::hardware::tv::input::TvMessageEvent; using AidlTvMessageEventType = ::aidl::android::hardware::tv::input::TvMessageEventType; using AidlTvStreamConfig = ::aidl::android::hardware::tv::input::TvStreamConfig; +using AidlMessageQueueMap = std::unordered_map< + int, + std::unordered_map<int, std::shared_ptr<AidlMessageQueue<int8_t, SynchronizedReadWrite>>>>; + +extern gBundleClassInfoType gBundleClassInfo; extern gTvInputHalClassInfoType gTvInputHalClassInfo; extern gTvStreamConfigClassInfoType gTvStreamConfigClassInfo; extern gTvStreamConfigBuilderClassInfoType gTvStreamConfigBuilderClassInfo; @@ -121,6 +132,19 @@ private: TvInputDeviceInfoWrapper deviceInfo; }; + class TvMessageEventWrapper { + public: + TvMessageEventWrapper() {} + + static TvMessageEventWrapper createEventWrapper( + const AidlTvMessageEvent& aidlTvMessageEvent); + + int streamId; + int deviceId; + std::vector<AidlTvMessage> messages; + AidlTvMessageEventType type; + }; + class NotifyHandler : public MessageHandler { public: NotifyHandler(JTvInputHal* hal, const TvInputEventWrapper& event); @@ -132,6 +156,17 @@ private: JTvInputHal* mHal; }; + class NotifyTvMessageHandler : public MessageHandler { + public: + NotifyTvMessageHandler(JTvInputHal* hal, const TvMessageEventWrapper& event); + + void handleMessage(const Message& message) override; + + private: + TvMessageEventWrapper mEvent; + JTvInputHal* mHal; + }; + class TvInputCallback : public HidlITvInputCallback, public BnTvInputCallback { public: explicit TvInputCallback(JTvInputHal* hal); @@ -156,6 +191,9 @@ private: ::ndk::ScopedAStatus closeStream(int32_t in_deviceId, int32_t in_streamId); ::ndk::ScopedAStatus setTvMessageEnabled(int32_t deviceId, int32_t streamId, TvMessageEventType in_type, bool enabled); + ::ndk::ScopedAStatus getTvMessageQueueDesc( + MQDescriptor<int8_t, SynchronizedReadWrite>* out_queue, int32_t in_deviceId, + int32_t in_streamId); private: ::ndk::ScopedAStatus hidlSetCallback(const std::shared_ptr<TvInputCallback>& in_callback); @@ -178,11 +216,14 @@ private: void onDeviceUnavailable(int deviceId); void onStreamConfigurationsChanged(int deviceId, int cableConnectionStatus); void onCaptured(int deviceId, int streamId, uint32_t seq, bool succeeded); + void onTvMessage(int deviceId, int streamId, AidlTvMessageEventType type, + AidlTvMessage& message, signed char data[], int dataLength); Mutex mLock; Mutex mStreamLock; jweak mThiz; sp<Looper> mLooper; + AidlMessageQueueMap mQueueMap; KeyedVector<int, KeyedVector<int, Connection> > mConnections; diff --git a/services/core/jni/tvinput/jstruct.h b/services/core/jni/tvinput/jstruct.h index 0a4a64dbb40c..23cf4aeaffa9 100644 --- a/services/core/jni/tvinput/jstruct.h +++ b/services/core/jni/tvinput/jstruct.h @@ -23,6 +23,7 @@ typedef struct { jmethodID deviceUnavailable; jmethodID streamConfigsChanged; jmethodID firstFrameCaptured; + jmethodID tvMessageReceived; } gTvInputHalClassInfoType; typedef struct { @@ -31,6 +32,14 @@ typedef struct { typedef struct { jclass clazz; + jmethodID constructor; + jmethodID putByteArray; + jmethodID putString; + jmethodID putInt; +} gBundleClassInfoType; + +typedef struct { + jclass clazz; jmethodID constructor; jmethodID streamId; diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java index fe8a8c8979f7..69a5e5c3a901 100644 --- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -93,6 +93,7 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta public void onFinalResponseReceived( ComponentName componentName, Void response) { + mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); mRequestSessionMetric.updateMetricsOnResponseReceived(mProviders, componentName, isPrimaryProviderViaProviderInfo(componentName)); respondToClientWithResponseAndFinish(null); diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 20dd1816bf44..c37a038d8ef7 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -64,7 +64,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR RequestInfo.TYPE_CREATE, callingAppInfo, enabledProviders, cancellationSignal, startedTimestamp); mRequestSessionMetric.collectCreateFlowInitialMetricInfo( - /*origin=*/request.getOrigin() != null); + /*origin=*/request.getOrigin() != null, request); mPrimaryProviders = primaryProviders; } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index 25561ed25f20..272452e1b2ae 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -33,7 +33,6 @@ import android.os.IBinder; import android.os.Looper; import android.os.ResultReceiver; import android.service.credentials.CredentialProviderInfoFactory; -import android.util.Log; import android.util.Slog; import java.util.ArrayList; @@ -72,7 +71,6 @@ public class CredentialManagerUi { }; private void handleUiResult(int resultCode, Bundle resultData) { - Log.i("reemademo", "handleUiResult with resultCOde: " + resultCode); switch (resultCode) { case UserSelectionDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION: @@ -86,13 +84,11 @@ public class CredentialManagerUi { } break; case UserSelectionDialogResult.RESULT_CODE_DIALOG_USER_CANCELED: - Log.i("reemademo", "RESULT_CODE_DIALOG_USER_CANCELED"); mStatus = UiStatus.TERMINATED; mCallbacks.onUiCancellation(/* isUserCancellation= */ true); break; case UserSelectionDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS: - Log.i("reemademo", "RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS"); mStatus = UiStatus.TERMINATED; mCallbacks.onUiCancellation(/* isUserCancellation= */ false); @@ -102,7 +98,6 @@ public class CredentialManagerUi { mCallbacks.onUiSelectorInvocationFailure(); break; default: - Log.i("reemademo", "Unknown error code returned from the UI"); mStatus = UiStatus.IN_PROGRESS; mCallbacks.onUiSelectorInvocationFailure(); break; diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index f95ed6adc8f6..aee4f583eec9 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -133,6 +133,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, public void onFinalResponseReceived(ComponentName componentName, @Nullable GetCredentialResponse response) { Slog.i(TAG, "onFinalResponseReceived from: " + componentName.flattenToString()); + mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); mRequestSessionMetric.updateMetricsOnResponseReceived(mProviders, componentName, isPrimaryProviderViaProviderInfo(componentName)); if (response != null) { @@ -158,7 +159,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, @Override public void onUiCancellation(boolean isUserCancellation) { - String exception = GetCredentialException.TYPE_NO_CREDENTIAL; + String exception = GetCredentialException.TYPE_USER_CANCELED; String message = "User cancelled the selector"; if (!isUserCancellation) { exception = GetCredentialException.TYPE_INTERRUPTED; diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index 64a73c92ca1f..b36de0b03fa8 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -39,7 +39,6 @@ import java.util.Map; /** * For all future metric additions, this will contain their names for local usage after importing * from {@link com.android.internal.util.FrameworkStatsLog}. - * TODO(b/271135048) - Emit all atoms, including all V4 atoms (specifically the rest of track 1). */ public class MetricUtilities { private static final boolean LOG_FLAG = true; @@ -101,7 +100,9 @@ public class MetricUtilities { */ protected static int getMetricTimestampDifferenceMicroseconds(long t2, long t1) { if (t2 - t1 > Integer.MAX_VALUE) { - throw new ArithmeticException("Input timestamps are too far apart and unsupported"); + Slog.i(TAG, "Input timestamps are too far apart and unsupported, " + + "falling back to default int"); + return DEFAULT_INT_32; } if (t2 < t1) { Slog.i(TAG, "The timestamps aren't in expected order, falling back to default int"); @@ -229,7 +230,7 @@ public class MetricUtilities { authenticationMetric.isAuthReturned() ); } catch (Exception e) { - Slog.w(TAG, "Unexpected error during candidate get metric logging: " + e); + Slog.w(TAG, "Unexpected error during candidate auth metric logging: " + e); } } @@ -252,22 +253,18 @@ public class MetricUtilities { } var sessions = providers.values(); for (var session : sessions) { - try { - var metric = session.getProviderSessionMetric() - .getCandidatePhasePerProviderMetric(); - FrameworkStatsLog.write( - FrameworkStatsLog.CREDENTIAL_MANAGER_GET_REPORTED, - /* session_id */ metric.getSessionIdProvider(), - /* sequence_num */ emitSequenceId, - /* candidate_provider_uid */ metric.getCandidateUid(), - /* response_unique_classtypes */ - metric.getResponseCollective().getUniqueResponseStrings(), - /* per_classtype_counts */ - metric.getResponseCollective().getUniqueResponseCounts() - ); - } catch (Exception e) { - Slog.w(TAG, "Unexpected exception during get metric logging" + e); - } + var metric = session.getProviderSessionMetric() + .getCandidatePhasePerProviderMetric(); + FrameworkStatsLog.write( + FrameworkStatsLog.CREDENTIAL_MANAGER_GET_REPORTED, + /* session_id */ metric.getSessionIdProvider(), + /* sequence_num */ emitSequenceId, + /* candidate_provider_uid */ metric.getCandidateUid(), + /* response_unique_classtypes */ + metric.getResponseCollective().getUniqueResponseStrings(), + /* per_classtype_counts */ + metric.getResponseCollective().getUniqueResponseCounts() + ); } } catch (Exception e) { Slog.w(TAG, "Unexpected error during candidate get metric logging: " + e); @@ -399,7 +396,7 @@ public class MetricUtilities { /* caller_uid */ callingUid, /* api_status */ apiStatus.getMetricCode()); } catch (Exception e) { - Slog.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during simple v2 metric logging: " + e); } } @@ -505,7 +502,7 @@ public class MetricUtilities { candidateAggregateMetric.isAuthReturned() ); } catch (Exception e) { - Slog.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during total candidate metric logging: " + e); } } @@ -570,7 +567,7 @@ public class MetricUtilities { /* primary_indicated */ finalPhaseMetric.isPrimary() ); } catch (Exception e) { - Slog.w(TAG, "Unexpected error during metric logging: " + e); + Slog.w(TAG, "Unexpected error during final no uid metric logging: " + e); } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 25f20caee16d..6f79852df02f 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -54,7 +54,7 @@ public final class ProviderCreateSession extends ProviderSession< private static final String TAG = "ProviderCreateSession"; // Key to be used as an entry key for a save entry - private static final String SAVE_ENTRY_KEY = "save_entry_key"; + public static final String SAVE_ENTRY_KEY = "save_entry_key"; // Key to be used as an entry key for a remote entry private static final String REMOTE_ENTRY_KEY = "remote_entry_key"; @@ -193,11 +193,13 @@ public final class ProviderCreateSession extends ProviderSession< mProviderResponseDataHandler.addResponseContent(response.getCreateEntries(), response.getRemoteCreateEntry()); if (mProviderResponseDataHandler.isEmptyResponse(response)) { - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, + ((RequestSession) mCallbacks).mRequestSessionMetric.getInitialPhaseMetric()); updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE, /*source=*/ CredentialsSource.REMOTE_PROVIDER); } else { - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, + ((RequestSession) mCallbacks).mRequestSessionMetric.getInitialPhaseMetric()); updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED, /*source=*/ CredentialsSource.REMOTE_PROVIDER); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 14260f0e1ad4..3c1432a320e7 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -150,7 +150,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential List<CredentialOption> filteredOptions = new ArrayList<>(); for (CredentialOption option : clientRequest.getCredentialOptions()) { if (providerCapabilities.contains(option.getType()) - && isProviderAllowed(option, info.getComponentName()) + && isProviderAllowed(option, info) && checkSystemProviderRequirement(option, info.isSystemProvider())) { Slog.i(TAG, "Option of type: " + option.getType() + " meets all filtering" + "conditions"); @@ -167,9 +167,14 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential return null; } - private static boolean isProviderAllowed(CredentialOption option, ComponentName componentName) { + private static boolean isProviderAllowed(CredentialOption option, + CredentialProviderInfo providerInfo) { + if (providerInfo.isSystemProvider()) { + // Always allow system providers , including the remote provider + return true; + } if (!option.getAllowedProviders().isEmpty() && !option.getAllowedProviders().contains( - componentName)) { + providerInfo.getComponentName())) { Slog.i(TAG, "Provider allow list specified but does not contain this provider"); return false; } @@ -435,7 +440,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential BeginGetCredentialResponse response = PendingIntentResultHandler .extractResponseContent(providerPendingIntentResponse .getResultData()); - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/true); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/true, null); if (response != null && !mProviderResponseDataHandler.isEmptyResponse(response)) { addToInitialRemoteResponse(response, /*isInitialResponse=*/ false); // Additional content received is in the form of new response content. @@ -473,12 +478,14 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential addToInitialRemoteResponse(response, /*isInitialResponse=*/true); // Log the data. if (mProviderResponseDataHandler.isEmptyResponse(response)) { - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, + null); updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE, /*source=*/ CredentialsSource.REMOTE_PROVIDER); return; } - mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false); + mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false, + null); updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED, /*source=*/ CredentialsSource.REMOTE_PROVIDER); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 83f21afa1a32..f2055d0595be 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -215,8 +215,10 @@ public abstract class ProviderSession<T, R> CredentialsSource source) { setStatus(status); boolean isPrimary = mProviderInfo != null && mProviderInfo.isPrimary(); - mProviderSessionMetric.collectCandidateMetricUpdate(isTerminatingStatus(status), - isCompletionStatus(status), mProviderSessionUid, + mProviderSessionMetric.collectCandidateMetricUpdate(isTerminatingStatus(status) + || isStatusWaitingForRemoteResponse(status), + isCompletionStatus(status) || isUiInvokingStatus(status), + mProviderSessionUid, /*isAuthEntry*/source == CredentialsSource.AUTH_ENTRY, /*isPrimary*/isPrimary); mCallbacks.onProviderStatusChanged(status, mComponentName, source); diff --git a/services/credentials/java/com/android/server/credentials/metrics/BrowsedAuthenticationMetric.java b/services/credentials/java/com/android/server/credentials/metrics/BrowsedAuthenticationMetric.java index 62b8f24eb201..1ac8a71e745e 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/BrowsedAuthenticationMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/BrowsedAuthenticationMetric.java @@ -43,9 +43,6 @@ public class BrowsedAuthenticationMetric { // Indicates if this provider returned from the authentication entry query, default false private boolean mAuthReturned = false; - // TODO(b/271135048) - Match the atom and provide a clean per provider session metric - // encapsulation. - public BrowsedAuthenticationMetric(int sessionIdProvider) { mSessionIdProvider = sessionIdProvider; } diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidateAggregateMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidateAggregateMetric.java index 339c221b0ccc..91547788a23b 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/CandidateAggregateMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/CandidateAggregateMetric.java @@ -16,6 +16,7 @@ package com.android.server.credentials.metrics; +import com.android.server.credentials.MetricUtilities; import com.android.server.credentials.ProviderSession; import com.android.server.credentials.metrics.shared.ResponseCollective; @@ -29,7 +30,7 @@ import java.util.Map; */ public class CandidateAggregateMetric { - private static final String TAG = "CandidateProviderMetric"; + private static final String TAG = "CandidateTotalMetric"; // The session id of this provider metric private final int mSessionIdProvider; // Indicates if this provider returned from the candidate query phase, @@ -74,8 +75,6 @@ public class CandidateAggregateMetric { /** * This will take all the candidate data captured and aggregate that information. - * TODO(b/271135048) : Add on authentication entry outputs from track 2 here as well once - * generated * @param providers the providers associated with the candidate flow */ public void collectAverages(Map<String, ProviderSession> providers) { @@ -88,11 +87,15 @@ public class CandidateAggregateMetric { Map<String, Integer> responseCountQuery = new LinkedHashMap<>(); Map<EntryEnum, Integer> entryCountQuery = new LinkedHashMap<>(); var providerSessions = providers.values(); - long min_query_start = Integer.MAX_VALUE; - long max_query_end = Integer.MIN_VALUE; + long min_query_start = Long.MAX_VALUE; + long max_query_end = Long.MIN_VALUE; for (var session : providerSessions) { var sessionMetric = session.getProviderSessionMetric(); var candidateMetric = sessionMetric.getCandidatePhasePerProviderMetric(); + if (candidateMetric.getCandidateUid() == MetricUtilities.DEFAULT_INT_32) { + mNumProviders--; + continue; // Do not aggregate this one and reduce the size of actual candidates + } if (mServiceBeganTimeNanoseconds == -1) { mServiceBeganTimeNanoseconds = candidateMetric.getServiceBeganTimeNanoseconds(); } @@ -119,15 +122,17 @@ public class CandidateAggregateMetric { } private void collectAuthAggregates(Map<String, ProviderSession> providers) { - mNumProviders = providers.size(); Map<String, Integer> responseCountAuth = new LinkedHashMap<>(); Map<EntryEnum, Integer> entryCountAuth = new LinkedHashMap<>(); var providerSessions = providers.values(); for (var session : providerSessions) { var sessionMetric = session.getProviderSessionMetric(); var authMetrics = sessionMetric.getBrowsedAuthenticationMetric(); - mNumAuthEntriesTapped += authMetrics.size(); for (var authMetric : authMetrics) { + if (authMetric.getProviderUid() == MetricUtilities.DEFAULT_INT_32) { + continue; // skip this unfilled base auth entry + } + mNumAuthEntriesTapped++; mAuthReturned = mAuthReturned || authMetric.isAuthReturned(); ResponseCollective authCollective = authMetric.getAuthEntryCollective(); ResponseCollective.combineTypeCountMaps(responseCountAuth, diff --git a/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java index 530f01cbdfc5..226cd2c11b15 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java +++ b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java @@ -28,6 +28,8 @@ import static com.android.server.credentials.ProviderGetSession.REMOTE_ENTRY_KEY import android.util.Slog; +import com.android.server.credentials.ProviderCreateSession; + import java.util.AbstractMap; import java.util.Map; @@ -52,6 +54,8 @@ public enum EntryEnum { new AbstractMap.SimpleEntry<>(REMOTE_ENTRY_KEY, REMOTE_ENTRY.mInnerMetricCode), new AbstractMap.SimpleEntry<>(CREDENTIAL_ENTRY_KEY, + CREDENTIAL_ENTRY.mInnerMetricCode), + new AbstractMap.SimpleEntry<>(ProviderCreateSession.SAVE_ENTRY_KEY, CREDENTIAL_ENTRY.mInnerMetricCode) ); diff --git a/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java index 44d845eaaf43..c1f6b4779ed4 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java @@ -20,6 +20,7 @@ import static com.android.server.credentials.MetricUtilities.DELTA_RESPONSES_CUT import static com.android.server.credentials.MetricUtilities.generateMetricKey; import android.annotation.NonNull; +import android.annotation.Nullable; import android.service.credentials.BeginCreateCredentialResponse; import android.service.credentials.BeginGetCredentialResponse; import android.service.credentials.CredentialEntry; @@ -205,18 +206,22 @@ public class ProviderSessionMetric { * * @param response contains entries and data from the candidate provider responses * @param isAuthEntry indicates if this is an auth entry collection or not + * @param initialPhaseMetric for create flows, this helps identify the response type, which + * will identify the *type* of create flow, especially important in + * track 2. This is expected to be null in get flows. * @param <R> the response type associated with the API flow in progress */ - public <R> void collectCandidateEntryMetrics(R response, boolean isAuthEntry) { + public <R> void collectCandidateEntryMetrics(R response, boolean isAuthEntry, + @Nullable InitialPhaseMetric initialPhaseMetric) { try { if (response instanceof BeginGetCredentialResponse) { beginGetCredentialResponseCollectionCandidateEntryMetrics( (BeginGetCredentialResponse) response, isAuthEntry); } else if (response instanceof BeginCreateCredentialResponse) { beginCreateCredentialResponseCollectionCandidateEntryMetrics( - (BeginCreateCredentialResponse) response); + (BeginCreateCredentialResponse) response, initialPhaseMetric); } else { - Slog.i(TAG, "Your response type is unsupported for metric logging"); + Slog.i(TAG, "Your response type is unsupported for candidate metric logging"); } } catch (Exception e) { Slog.i(TAG, "Unexpected error during candidate entry metric logging: " + e); @@ -245,7 +250,6 @@ public class ProviderSessionMetric { String entryKey = generateMetricKey(entry.getType(), DELTA_RESPONSES_CUT); responseCounts.put(entryKey, responseCounts.getOrDefault(entryKey, 0) + 1); }); - ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts); mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective); } @@ -262,19 +266,21 @@ public class ProviderSessionMetric { } private void beginCreateCredentialResponseCollectionCandidateEntryMetrics( - BeginCreateCredentialResponse response) { + BeginCreateCredentialResponse response, InitialPhaseMetric initialPhaseMetric) { Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>(); var createEntries = response.getCreateEntries(); - int numRemoteEntry = response.getRemoteCreateEntry() != null ? MetricUtilities.ZERO : + int numRemoteEntry = response.getRemoteCreateEntry() == null ? MetricUtilities.ZERO : MetricUtilities.UNIT; int numCreateEntries = createEntries.size(); entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry); entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCreateEntries); Map<String, Integer> responseCounts = new LinkedHashMap<>(); - responseCounts.put(MetricUtilities.DEFAULT_STRING, numCreateEntries); - // We don't store create response because it's directly related to the request - // We do still store the count, however + String[] requestStrings = initialPhaseMetric == null ? new String[0] : + initialPhaseMetric.getUniqueRequestStrings(); + if (requestStrings.length > 0) { + responseCounts.put(requestStrings[0], initialPhaseMetric.getUniqueRequestCounts()[0]); + } ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts); mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective); diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java index 281f3cc705be..83b57c4f78c2 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -26,13 +26,17 @@ import static com.android.server.credentials.MetricUtilities.logApiCalledCandida import static com.android.server.credentials.MetricUtilities.logApiCalledCandidatePhase; import static com.android.server.credentials.MetricUtilities.logApiCalledFinalPhase; import static com.android.server.credentials.MetricUtilities.logApiCalledNoUidFinal; +import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL; +import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL_VIA_REGISTRY; import android.annotation.NonNull; import android.content.ComponentName; +import android.credentials.CreateCredentialRequest; import android.credentials.GetCredentialRequest; import android.credentials.ui.UserSelectionDialogResult; import android.util.Slog; +import com.android.server.credentials.MetricUtilities; import com.android.server.credentials.ProviderSession; import java.util.ArrayList; @@ -112,7 +116,7 @@ public class RequestSessionMetric { mInitialPhaseMetric.setCallerUid(mCallingUid); mInitialPhaseMetric.setApiName(metricCode); } catch (Exception e) { - Slog.i(TAG, "Unexpected error collecting initial metrics: " + e); + Slog.i(TAG, "Unexpected error collecting initial phase metric start info: " + e); } } @@ -177,9 +181,12 @@ public class RequestSessionMetric { * * @param origin indicates if an origin was passed in or not */ - public void collectCreateFlowInitialMetricInfo(boolean origin) { + public void collectCreateFlowInitialMetricInfo(boolean origin, + CreateCredentialRequest request) { try { mInitialPhaseMetric.setOriginSpecified(origin); + mInitialPhaseMetric.setRequestCounts(Map.of(generateMetricKey(request.getType(), + DELTA_RESPONSES_CUT), MetricUtilities.UNIT)); } catch (Exception e) { Slog.i(TAG, "Unexpected error collecting create flow metric: " + e); } @@ -195,7 +202,7 @@ public class RequestSessionMetric { 0) + 1); }); } catch (Exception e) { - Slog.i(TAG, "Unexpected error during get request metric logging: " + e); + Slog.i(TAG, "Unexpected error during get request count map metric logging: " + e); } return uniqueRequestCounts; } @@ -210,7 +217,7 @@ public class RequestSessionMetric { mInitialPhaseMetric.setOriginSpecified(request.getOrigin() != null); mInitialPhaseMetric.setRequestCounts(getRequestCountMap(request)); } catch (Exception e) { - Slog.i(TAG, "Unexpected error collecting get flow metric: " + e); + Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e); } } @@ -277,7 +284,7 @@ public class RequestSessionMetric { mChosenProviderFinalPhaseMetric.setChosenProviderStatus( finalStatus.getMetricCode()); } catch (Exception e) { - Slog.i(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during final phase provider status metric logging: " + e); } } @@ -367,7 +374,11 @@ public class RequestSessionMetric { public void logCandidatePhaseMetrics(Map<String, ProviderSession> providers) { try { logApiCalledCandidatePhase(providers, ++mSequenceCounter, mInitialPhaseMetric); - logApiCalledCandidateGetMetric(providers, mSequenceCounter); + if (mInitialPhaseMetric.getApiName() == GET_CREDENTIAL.getMetricCode() + || mInitialPhaseMetric.getApiName() == GET_CREDENTIAL_VIA_REGISTRY + .getMetricCode()) { + logApiCalledCandidateGetMetric(providers, mSequenceCounter); + } } catch (Exception e) { Slog.i(TAG, "Unexpected error during candidate metric emit: " + e); } @@ -405,7 +416,7 @@ public class RequestSessionMetric { } logApiCalledAuthenticationMetric(browsedAuthenticationMetric, ++mSequenceCounter); } catch (Exception e) { - Slog.i(TAG, "Unexpected error during metric logging: " + e); + Slog.i(TAG, "Unexpected error during auth entry metric emit: " + e); } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/shared/ResponseCollective.java b/services/credentials/java/com/android/server/credentials/metrics/shared/ResponseCollective.java index 0958a841fe7e..ceb957100aec 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/shared/ResponseCollective.java +++ b/services/credentials/java/com/android/server/credentials/metrics/shared/ResponseCollective.java @@ -18,6 +18,7 @@ package com.android.server.credentials.metrics.shared; import android.annotation.NonNull; +import com.android.server.credentials.MetricUtilities; import com.android.server.credentials.metrics.EntryEnum; import java.util.Collections; @@ -121,7 +122,7 @@ public class ResponseCollective { * @return a count of this particular entry enum stored by this provider */ public int getCountForEntry(EntryEnum e) { - return mEntryCounts.get(e); + return mEntryCounts.getOrDefault(e, MetricUtilities.ZERO); } /** @@ -167,7 +168,7 @@ public class ResponseCollective { public static <T> Map<T, Integer> combineTypeCountMaps(Map<T, Integer> first, Map<T, Integer> second) { for (T response : second.keySet()) { - first.merge(response, first.getOrDefault(response, 0), Integer::sum); + first.put(response, first.getOrDefault(response, 0) + second.get(response)); } return first; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java index 6d51bd7c7770..d9604395dd98 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java @@ -16,6 +16,8 @@ package com.android.server.devicepolicy; +import static com.android.server.devicepolicy.DevicePolicyManagerService.DEFAULT_KEEP_PROFILES_RUNNING_FLAG; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -70,6 +72,7 @@ class DevicePolicyData { private static final String TAG_PASSWORD_TOKEN_HANDLE = "password-token"; private static final String TAG_PROTECTED_PACKAGES = "protected-packages"; private static final String TAG_BYPASS_ROLE_QUALIFICATIONS = "bypass-role-qualifications"; + private static final String TAG_KEEP_PROFILES_RUNNING = "keep-profiles-running"; private static final String ATTR_VALUE = "value"; private static final String ATTR_ALIAS = "alias"; private static final String ATTR_ID = "id"; @@ -193,6 +196,12 @@ class DevicePolicyData { // starts. String mNewUserDisclaimer = NEW_USER_DISCLAIMER_NOT_NEEDED; + /** + * Effective state of the feature flag. It is updated to the current configuration value + * during boot and doesn't change value after than unless overridden by test code. + */ + boolean mEffectiveKeepProfilesRunning = DEFAULT_KEEP_PROFILES_RUNNING_FLAG; + DevicePolicyData(@UserIdInt int userId) { mUserId = userId; } @@ -392,6 +401,12 @@ class DevicePolicyData { out.endTag(null, TAG_BYPASS_ROLE_QUALIFICATIONS); } + if (policyData.mEffectiveKeepProfilesRunning != DEFAULT_KEEP_PROFILES_RUNNING_FLAG) { + out.startTag(null, TAG_KEEP_PROFILES_RUNNING); + out.attributeBoolean(null, ATTR_VALUE, policyData.mEffectiveKeepProfilesRunning); + out.endTag(null, TAG_KEEP_PROFILES_RUNNING); + } + out.endTag(null, "policies"); out.endDocument(); @@ -574,7 +589,10 @@ class DevicePolicyData { parser.getAttributeBoolean(null, ATTR_VALUE, false); } else if (TAG_BYPASS_ROLE_QUALIFICATIONS.equals(tag)) { policy.mBypassDevicePolicyManagementRoleQualifications = true; - policy.mCurrentRoleHolder = parser.getAttributeValue(null, ATTR_VALUE); + policy.mCurrentRoleHolder = parser.getAttributeValue(null, ATTR_VALUE); + } else if (TAG_KEEP_PROFILES_RUNNING.equals(tag)) { + policy.mEffectiveKeepProfilesRunning = parser.getAttributeBoolean( + null, ATTR_VALUE, DEFAULT_KEEP_PROFILES_RUNNING_FLAG); // Deprecated tags below } else if (TAG_PROTECTED_PACKAGES.equals(tag)) { if (policy.mUserControlDisabledPackages == null) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index 1392d0232262..cf7c7186e4de 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -27,17 +27,22 @@ import static android.content.pm.UserProperties.INHERIT_DEVICE_POLICY_FROM_PAREN import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.AppGlobals; import android.app.BroadcastOptions; import android.app.admin.DevicePolicyIdentifiers; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyState; +import android.app.admin.IntentFilterPolicyKey; import android.app.admin.PolicyKey; import android.app.admin.PolicyUpdateReceiver; import android.app.admin.PolicyValue; import android.app.admin.TargetUser; import android.app.admin.UserRestrictionPolicyKey; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; @@ -45,6 +50,7 @@ import android.content.pm.UserProperties; import android.os.Binder; import android.os.Bundle; import android.os.Environment; +import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.telephony.TelephonyManager; @@ -56,6 +62,7 @@ import android.util.Xml; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.utils.Slogf; import libcore.io.IoUtils; @@ -598,6 +605,22 @@ final class DevicePolicyEngine { } /** + * Retrieves the values set for the provided {@code policyDefinition} by each admin. + */ + @NonNull + <V> LinkedHashMap<EnforcingAdmin, PolicyValue<V>> getGlobalPoliciesSetByAdmins( + @NonNull PolicyDefinition<V> policyDefinition) { + Objects.requireNonNull(policyDefinition); + + synchronized (mLock) { + if (!hasGlobalPolicyLocked(policyDefinition)) { + return new LinkedHashMap<>(); + } + return getGlobalPolicyStateLocked(policyDefinition).getPoliciesSetByAdmins(); + } + } + + /** * Returns the policies set by the given admin that share the same * {@link PolicyKey#getIdentifier()} as the provided {@code policyDefinition}. * @@ -1014,20 +1037,90 @@ final class DevicePolicyEngine { /** * Handles internal state related to packages getting updated. */ - void handlePackageChanged(@Nullable String updatedPackage, int userId, boolean packageRemoved) { - if (updatedPackage == null) { - return; - } - if (packageRemoved) { + void handlePackageChanged( + @Nullable String updatedPackage, int userId, @Nullable String removedDpcPackage) { + Binder.withCleanCallingIdentity(() -> { Set<EnforcingAdmin> admins = getEnforcingAdminsOnUser(userId); + if (removedDpcPackage != null) { + for (EnforcingAdmin admin : admins) { + if (removedDpcPackage.equals(admin.getPackageName())) { + removePoliciesForAdmin(admin); + return; + } + } + } for (EnforcingAdmin admin : admins) { - if (admin.getPackageName().equals(updatedPackage)) { - // remove policies for the uninstalled package - removePoliciesForAdmin(admin); + if (updatedPackage == null || updatedPackage.equals(admin.getPackageName())) { + if (!isPackageInstalled(admin.getPackageName(), userId)) { + Slogf.i(TAG, String.format( + "Admin package %s not found for user %d, removing admin policies", + admin.getPackageName(), userId)); + // remove policies for the uninstalled package + removePoliciesForAdmin(admin); + return; + } + } + } + if (updatedPackage != null) { + updateDeviceAdminServiceOnPackageChanged(updatedPackage, userId); + removePersistentPreferredActivityPoliciesForPackage(updatedPackage, userId); + } + }); + } + + private void removePersistentPreferredActivityPoliciesForPackage( + @NonNull String packageName, int userId) { + Set<PolicyKey> policyKeys = getLocalPolicyKeysSetByAllAdmins( + PolicyDefinition.GENERIC_PERSISTENT_PREFERRED_ACTIVITY, userId); + for (PolicyKey key : policyKeys) { + if (!(key instanceof IntentFilterPolicyKey)) { + throw new IllegalStateException("PolicyKey for " + + "PERSISTENT_PREFERRED_ACTIVITY is not of type " + + "IntentFilterPolicyKey"); + } + IntentFilterPolicyKey parsedKey = + (IntentFilterPolicyKey) key; + IntentFilter intentFilter = Objects.requireNonNull(parsedKey.getIntentFilter()); + PolicyDefinition<ComponentName> policyDefinition = + PolicyDefinition.PERSISTENT_PREFERRED_ACTIVITY(intentFilter); + LinkedHashMap<EnforcingAdmin, PolicyValue<ComponentName>> policies = + getLocalPoliciesSetByAdmins( + policyDefinition, + userId); + IPackageManager packageManager = AppGlobals.getPackageManager(); + for (EnforcingAdmin admin : policies.keySet()) { + if (policies.get(admin).getValue() != null + && policies.get(admin).getValue().getPackageName().equals(packageName)) { + try { + if (packageManager.getPackageInfo( + packageName, 0, userId) == null + || packageManager.getReceiverInfo(policies.get(admin).getValue(), + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, + userId) == null) { + Slogf.e(TAG, String.format( + "Persistent preferred activity in package %s not found for " + + "user %d, removing policy for admin", + packageName, userId)); + removeLocalPolicy(policyDefinition, admin, userId); + } + } catch (RemoteException re) { + // Shouldn't happen. + Slogf.wtf(TAG, "Error handling package changes", re); + } } } - } else { - updateDeviceAdminServiceOnPackageChanged(updatedPackage, userId); + } + } + + private boolean isPackageInstalled(String packageName, int userId) { + try { + return AppGlobals.getPackageManager().getPackageInfo( + packageName, 0, userId) != null; + } catch (RemoteException re) { + // Shouldn't happen. + Slogf.wtf(TAG, "Error handling package changes", re); + return true; } } @@ -1511,7 +1604,7 @@ final class DevicePolicyEngine { readInner(parser); } catch (XmlPullParserException | IOException | ClassNotFoundException e) { - Log.e(TAG, "Error parsing resources file", e); + Slogf.wtf(TAG, "Error parsing resources file", e); } finally { IoUtils.closeQuietly(input); } @@ -1533,7 +1626,7 @@ final class DevicePolicyEngine { readEnforcingAdminsInner(parser); break; default: - Log.e(TAG, "Unknown tag " + tag); + Slogf.wtf(TAG, "Unknown tag " + tag); } } } @@ -1554,7 +1647,7 @@ final class DevicePolicyEngine { policyState = PolicyState.readFromXml(parser); break; default: - Log.e(TAG, "Unknown tag for local policy entry" + tag); + Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag); } } @@ -1564,7 +1657,9 @@ final class DevicePolicyEngine { } mLocalPolicies.get(userId).put(policyKey, policyState); } else { - Log.e(TAG, "Error parsing local policy"); + Slogf.wtf(TAG, "Error parsing local policy, policyKey is " + + (policyKey == null ? "null" : policyKey) + ", and policyState is " + + (policyState == null ? "null" : policyState) + "."); } } @@ -1583,20 +1678,26 @@ final class DevicePolicyEngine { policyState = PolicyState.readFromXml(parser); break; default: - Log.e(TAG, "Unknown tag for local policy entry" + tag); + Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag); } } if (policyKey != null && policyState != null) { mGlobalPolicies.put(policyKey, policyState); } else { - Log.e(TAG, "Error parsing global policy"); + Slogf.wtf(TAG, "Error parsing global policy, policyKey is " + + (policyKey == null ? "null" : policyKey) + ", and policyState is " + + (policyState == null ? "null" : policyState) + "."); } } private void readEnforcingAdminsInner(TypedXmlPullParser parser) throws XmlPullParserException { EnforcingAdmin admin = EnforcingAdmin.readFromXml(parser); + if (admin == null) { + Slogf.wtf(TAG, "Error parsing enforcingAdmins, EnforcingAdmin is null."); + return; + } if (!mEnforcingAdmins.contains(admin.getUserId())) { mEnforcingAdmins.put(admin.getUserId(), new HashSet<>()); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index ef45ceedb7f2..2846b394e6e7 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -232,7 +232,6 @@ import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPR import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK; import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; import static android.provider.DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER; -import static android.provider.DeviceConfig.NAMESPACE_TELEPHONY; import static android.provider.Settings.Global.PRIVATE_DNS_SPECIFIER; import static android.provider.Settings.Secure.MANAGED_PROVISIONING_DPC_DOWNLOADED; import static android.provider.Settings.Secure.USER_SETUP_COMPLETE; @@ -411,6 +410,7 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; +import android.os.UserManager.EnforcingUser; import android.os.UserManager.UserRestrictionSource; import android.os.storage.StorageManager; import android.permission.AdminPermissionControlParams; @@ -446,7 +446,6 @@ import android.util.AtomicFile; import android.util.DebugUtils; import android.util.IndentingPrintWriter; import android.util.IntArray; -import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -489,6 +488,7 @@ import com.android.server.LockGuard; import com.android.server.PersistentDataBlockManagerInternal; import com.android.server.SystemServerInitThreadPool; import com.android.server.SystemService; +import com.android.server.SystemServiceManager; import com.android.server.devicepolicy.ActiveAdmin.TrustAgentInfo; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.net.NetworkPolicyManagerInternal; @@ -677,7 +677,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // to decide whether an existing policy in the {@link #DEVICE_POLICIES_XML} needs to // be upgraded. See {@link PolicyVersionUpgrader} on instructions how to add an upgrade // step. - static final int DPMS_VERSION = 4; + static final int DPMS_VERSION = 5; static { SECURE_SETTINGS_ALLOWLIST = new ArraySet<>(); @@ -874,11 +874,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // TODO(b/265683382) remove the flag after rollout. private static final String KEEP_PROFILES_RUNNING_FLAG = "enable_keep_profiles_running"; - private static final boolean DEFAULT_KEEP_PROFILES_RUNNING_FLAG = true; - - private static final String ENABLE_WORK_PROFILE_TELEPHONY_FLAG = - "enable_work_profile_telephony"; - private static final boolean DEFAULT_WORK_PROFILE_TELEPHONY_FLAG = false; + public static final boolean DEFAULT_KEEP_PROFILES_RUNNING_FLAG = true; // TODO(b/261999445) remove the flag after rollout. private static final String HEADLESS_FLAG = "headless"; @@ -889,12 +885,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private static final boolean DEFAULT_APPLICATION_EXEMPTIONS_FLAG = true; /** - * This feature flag is checked once after boot and this value us used until the next reboot to - * avoid needing to handle the flag changing on the fly. - */ - private boolean mKeepProfilesRunning = isKeepProfilesRunningFlagEnabled(); - - /** * For apps targeting U+ * Enable multiple admins to coexist on the same device. */ @@ -1395,6 +1385,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private void handlePackagesChanged(@Nullable String packageName, int userHandle) { boolean removedAdmin = false; + String removedAdminPackage = null; if (VERBOSE_LOG) { Slogf.d(LOG_TAG, "Handling package changes package " + packageName + " for user " + userHandle); @@ -1417,6 +1408,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { "Admin package %s not found for user %d, removing active admin", packageName, userHandle)); removedAdmin = true; + removedAdminPackage = adminPackage; policy.mAdminList.remove(i); policy.mAdminMap.remove(aa.info.getComponent()); pushActiveAdminPackagesLocked(userHandle); @@ -1450,7 +1442,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { startOwnerService(userHandle, "package-broadcast"); } if (isPolicyEngineForFinanceFlagEnabled() || isPermissionCheckFlagEnabled()) { - mDevicePolicyEngine.handlePackageChanged(packageName, userHandle, removedAdmin); + mDevicePolicyEngine.handlePackageChanged( + packageName, userHandle, removedAdminPackage); } // Persist updates if the removed package was an admin or delegate. if (removedAdmin || removedDelegate) { @@ -2096,12 +2089,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { performPolicyVersionUpgrade(); } - // TODO(b/265683382) move it into an upgrade step when removing the flag, so that it is - // executed only once on upgrading devices, not every boot. - if (mKeepProfilesRunning) { - suspendAppsForQuietProfiles(); - } - mUserData = new SparseArray<>(); mOwners = makeOwners(injector, pathProvider); @@ -2202,12 +2189,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return packageNameAndSignature; } - private void suspendAppsForQuietProfiles() { + private void suspendAppsForQuietProfiles(boolean toSuspend) { PackageManagerInternal pmi = mInjector.getPackageManagerInternal(); List<UserInfo> users = mUserManager.getUsers(); for (UserInfo user : users) { if (user.isManagedProfile() && user.isQuietModeEnabled()) { - pmi.setPackagesSuspendedForQuietMode(user.id, true); + pmi.setPackagesSuspendedForQuietMode(user.id, toSuspend); } } } @@ -3388,9 +3375,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { onLockSettingsReady(); loadAdminDataAsync(); mOwners.systemReady(); - if (isWorkProfileTelephonyEnabled()) { - applyManagedSubscriptionsPolicyIfRequired(); - } + applyManagedSubscriptionsPolicyIfRequired(); break; case SystemService.PHASE_ACTIVITY_MANAGER_READY: synchronized (getLockObject()) { @@ -3421,9 +3406,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { unregisterOnSubscriptionsChangedListener(); int policyType = getManagedSubscriptionsPolicy().getPolicyType(); if (policyType == ManagedSubscriptionsPolicy.TYPE_ALL_PERSONAL_SUBSCRIPTIONS) { - final int parentUserId = getProfileParentId(copeProfileUserId); - // By default, assign all current and future subs to system user on COPE devices. - registerListenerToAssignSubscriptionsToUser(parentUserId); + clearManagedSubscriptionsPolicy(); } else if (policyType == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) { // Add listener to assign all current and future subs to managed profile. registerListenerToAssignSubscriptionsToUser(copeProfileUserId); @@ -3482,6 +3465,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { revertTransferOwnershipIfNecessaryLocked(); } updateUsbDataSignal(); + + // In case flag value has changed, we apply it during boot to avoid doing it concurrently + // with user toggling quiet mode. + setKeepProfileRunningEnabledUnchecked(isKeepProfilesRunningFlagEnabled()); } // TODO(b/230841522) Make it static. @@ -3549,7 +3536,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { deleteTransferOwnershipBundleLocked(metadata.userId); } updateSystemUpdateFreezePeriodsRecord(/* saveIfChanged */ true); - pushUserControlDisabledPackagesLocked(metadata.userId); + if (!isPolicyEngineForFinanceFlagEnabled()) { + pushUserControlDisabledPackagesLocked(metadata.userId); + } } private void maybeLogStart() { @@ -5561,7 +5550,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (isPermissionCheckFlagEnabled()) { CallerIdentity caller = getCallerIdentity(who, callerPackageName); ap = enforcePermissionAndGetEnforcingAdmin( - who, MANAGE_DEVICE_POLICY_WIPE_DATA, + who, + /*permission=*/ MANAGE_DEVICE_POLICY_WIPE_DATA, + /* adminPolicy=*/ DeviceAdminInfo.USES_POLICY_WIPE_DATA, caller.getPackageName(), affectedUserId).getActiveAdmin(); } else { // This API can only be called by an active device admin, @@ -5843,9 +5834,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { ActiveAdmin ap; if (isPermissionCheckFlagEnabled()) { CallerIdentity caller = getCallerIdentity(who, callerPackageName); - // TODO: Allow use of USES_POLICY_FORCE_LOCK ap = enforcePermissionAndGetEnforcingAdmin( - who, MANAGE_DEVICE_POLICY_LOCK, caller.getPackageName(), + who, + /*permission=*/ MANAGE_DEVICE_POLICY_LOCK, + /*AdminPolicy=*/DeviceAdminInfo.USES_POLICY_FORCE_LOCK, + caller.getPackageName(), affectedUserId).getActiveAdmin(); } else { ap = getActiveAdminForCallerLocked( @@ -7722,11 +7715,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } mLockSettingsInternal.refreshStrongAuthTimeout(parentId); - if (isWorkProfileTelephonyEnabled()) { - clearManagedSubscriptionsPolicy(); - clearLauncherShortcutOverrides(); - updateTelephonyCrossProfileIntentFilters(parentId, UserHandle.USER_NULL, false); - } + clearManagedSubscriptionsPolicy(); + clearLauncherShortcutOverrides(); + updateTelephonyCrossProfileIntentFilters(parentId, UserHandle.USER_NULL, false); + Slogf.i(LOG_TAG, "Cleaning up device-wide policies done."); } @@ -10071,6 +10063,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (!mHasTelephonyFeature) { return; } + if (!LocalServices.getService(SystemServiceManager.class).isBootCompleted()) { + Slogf.i(LOG_TAG, "Skip clearing managed profile Apn before boot completed"); + // Cannot talk to APN content provider before system boots + // Ideally we should delay the cleanup post boot_completed, not just + // skipping it altogether. + return; + } final List<ApnSetting> apns = getOverrideApnsUnchecked(); for (ApnSetting apn : apns) { if (apn.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) { @@ -10111,7 +10110,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { clearUserPoliciesLocked(userId); clearOverrideApnUnchecked(); clearApplicationRestrictions(userId); - mInjector.getPackageManagerInternal().clearBlockUninstallForUser(userId); + if (!isPolicyEngineForFinanceFlagEnabled()) { + mInjector.getPackageManagerInternal().clearBlockUninstallForUser(userId); + } mOwners.clearDeviceOwner(); mOwners.writeDeviceOwner(); @@ -10326,6 +10327,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { policy.mSecondaryLockscreenEnabled = false; policy.mUserProvisioningState = DevicePolicyManager.STATE_USER_UNMANAGED; policy.mAffiliationIds.clear(); + resetAffiliationCacheLocked(); policy.mLockTaskPackages.clear(); if (!isPolicyEngineForFinanceFlagEnabled()) { updateLockTaskPackagesLocked(mContext, policy.mLockTaskPackages, userId); @@ -10962,8 +10964,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { /* deviceOwnerUserId= */ deviceOwnerUserId, /* callingUserId*/ caller.getUserId(), isAdb(caller), hasIncompatibleAccountsOrNonAdb); if (code != STATUS_OK) { - throw new IllegalStateException(computeProvisioningErrorStringLocked(code, - deviceOwnerUserId, owner, showComponentOnError)); + final String provisioningErrorStringLocked = computeProvisioningErrorStringLocked(code, + deviceOwnerUserId, owner, showComponentOnError); + if (code == STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED) { + throw new ServiceSpecificException(code, provisioningErrorStringLocked); + } else { + throw new IllegalStateException(provisioningErrorStringLocked); + } } } @@ -11017,6 +11024,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { case STATUS_HAS_PAIRED: return "Not allowed to set the device owner because this device has already " + "paired."; + case STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED: + return "Cannot provision an unsupported DPC into DO on a" + + " headless device"; default: return "Unexpected @ProvisioningPreCondition: " + code; } @@ -11317,7 +11327,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { (size == 1 ? "" : "s")); } pw.println(); - pw.println("Keep profiles running: " + mKeepProfilesRunning); + pw.println("Keep profiles running: " + + getUserData(UserHandle.USER_SYSTEM).mEffectiveKeepProfilesRunning); pw.println(); mPolicyCache.dump(pw); @@ -11329,11 +11340,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { synchronized (mSubscriptionsChangedListenerLock) { pw.println("Subscription changed listener : " + mSubscriptionsChangedListener); } - pw.println("DPM Flag enable_work_profile_telephony : " - + isWorkProfileTelephonyDevicePolicyManagerFlagEnabled()); - pw.println("Telephony Flag enable_work_profile_telephony : " - + isWorkProfileTelephonySubscriptionManagerFlagEnabled()); + pw.println("DPM global setting ALLOW_WORK_PROFILE_TELEPHONY_FOR_NON_DPM_ROLE_HOLDERS : " + + mInjector.settingsGlobalGetString( + Global.ALLOW_WORK_PROFILE_TELEPHONY_FOR_NON_DPM_ROLE_HOLDERS)); mHandler.post(() -> handleDump(pw)); dumpResources(pw); } @@ -11438,6 +11448,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { || isDefaultDeviceOwner(caller) || isFinancedDeviceOwner(caller)); enforcingAdmin = getEnforcingAdminForCaller(who, callerPackageName); } + if (!isPackageInstalledForUser(activity.getPackageName(), userId)) { + // Fail early as packageManager doesn't persist the activity if its not installed. + return; + } mDevicePolicyEngine.setLocalPolicy( PolicyDefinition.PERSISTENT_PREFERRED_ACTIVITY(filter), enforcingAdmin, @@ -11575,6 +11589,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) || isProfileOwnerOfOrganizationOwnedDevice(caller)); } + + if (!parent && isManagedProfile(caller.getUserId()) + && getManagedSubscriptionsPolicy().getPolicyType() + != ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) { + throw new IllegalStateException( + "Default sms application can only be set on the profile, when " + + "ManagedSubscriptions policy is set"); + } + if (parent) { userId = getProfileParentId(mInjector.userHandleGetCallingUserId()); mInjector.binderWithCleanCallingIdentity(() -> enforcePackageIsSystemPackage( @@ -11802,9 +11825,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (isPermissionCheckFlagEnabled()) { CallerIdentity caller = getCallerIdentity(admin, callerPackageName); int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle; - // TODO: Support USES_POLICY_DISABLE_KEYGUARD_FEATURES ap = enforcePermissionAndGetEnforcingAdmin( - admin, MANAGE_DEVICE_POLICY_KEYGUARD, + admin, + /*permission=*/MANAGE_DEVICE_POLICY_KEYGUARD, + /*adminPolicy=*/DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES, caller.getPackageName(), affectedUserId).getActiveAdmin(); } else { ap = getActiveAdminForCallerLocked(admin, @@ -13350,34 +13374,28 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { caller = getCallerIdentity(who); } int userId = caller.getUserId(); + int affectedUserId = parent ? getProfileParentId(userId) : userId; checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_USER_RESTRICTION); if (isPolicyEngineForFinanceFlagEnabled()) { if (!isDeviceOwner(caller) && !isProfileOwner(caller)) { + EnforcingAdmin admin = enforcePermissionForUserRestriction( + who, + key, + caller.getPackageName(), + affectedUserId); if (!mInjector.isChangeEnabled(ENABLE_COEXISTENCE_CHANGE, callerPackage, userId)) { throw new IllegalStateException("Calling package is not targeting Android U."); } if (!UserRestrictionsUtils.isValidRestriction(key)) { throw new IllegalArgumentException("Invalid restriction key: " + key); } - int affectedUserId = parent ? getProfileParentId(userId) : userId; - EnforcingAdmin admin = enforcePermissionForUserRestriction( - who, - key, - caller.getPackageName(), - affectedUserId); PolicyDefinition<Boolean> policyDefinition = PolicyDefinition.getPolicyDefinitionForUserRestriction(key); if (enabledFromThisOwner) { - // TODO: Remove this special case - replace with breaking change to require - // setGlobally to disable ADB - if (key.equals(UserManager.DISALLOW_DEBUGGING_FEATURES) && parent) { - setGlobalUserRestrictionInternal(admin, key, /* enabled= */ true); - } else { - setLocalUserRestrictionInternal( - admin, key, /* enabled= */ true, affectedUserId); - } + setLocalUserRestrictionInternal( + admin, key, /* enabled= */ true, affectedUserId); } else { // Remove any local and global policy that was set by the admin if (!policyDefinition.isLocalOnlyPolicy()) { @@ -13895,7 +13913,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { CallerIdentity caller = getCallerIdentity(who, callerPackage); final int userId = parent ? getProfileParentId(caller.getUserId()) : caller.getUserId(); if (isPolicyEngineForFinanceFlagEnabled()) { - // TODO: We need to ensure the delegate with DELEGATION_PACKAGE_ACCESS can do this enforcePermission(MANAGE_DEVICE_POLICY_PACKAGE_STATE, caller.getPackageName(), userId); } else { Preconditions.checkCallAuthorization((caller.hasAdminComponent() @@ -15129,7 +15146,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return; } - if ((flags == 0) || ((flags & ~(allowedFlags)) != 0)) { + if ((flags & ~(allowedFlags)) != 0) { throw new SecurityException( "Permitted lock task features when managing a financed device: " + "LOCK_TASK_FEATURE_SYSTEM_INFO, LOCK_TASK_FEATURE_KEYGUARD, " @@ -15191,8 +15208,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void setGlobalSetting(ComponentName who, String setting, String value) { - Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); + if (Global.ALLOW_WORK_PROFILE_TELEPHONY_FOR_NON_DPM_ROLE_HOLDERS.equals(setting)) { + Preconditions.checkCallAuthorization(isCallerDevicePolicyManagementRoleHolder(caller)); + mInjector.binderWithCleanCallingIdentity( + () -> mInjector.settingsGlobalPutString(setting, value)); + return; + } + Objects.requireNonNull(who, "ComponentName is null"); Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)); DevicePolicyEventLogger @@ -16060,8 +16083,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } @Override - public List<String> getAllCrossProfilePackages() { - return DevicePolicyManagerService.this.getAllCrossProfilePackages(); + public List<String> getAllCrossProfilePackages(int userId) { + return DevicePolicyManagerService.this.getAllCrossProfilePackages(userId); } @Override @@ -16267,7 +16290,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public boolean isKeepProfilesRunningEnabled() { - return mKeepProfilesRunning; + return getUserDataUnchecked(UserHandle.USER_SYSTEM).mEffectiveKeepProfilesRunning; } private @Mode int findInteractAcrossProfilesResetMode(String packageName) { @@ -16324,6 +16347,39 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return List.of(bundle); }); } + + public List<EnforcingUser> getUserRestrictionSources(String restriction, + @UserIdInt int userId) { + PolicyDefinition<Boolean> policy = + PolicyDefinition.getPolicyDefinitionForUserRestriction(restriction); + + Set<EnforcingAdmin> localAdmins = + mDevicePolicyEngine.getLocalPoliciesSetByAdmins(policy, userId).keySet(); + + Set<EnforcingAdmin> globalAdmins = + mDevicePolicyEngine.getGlobalPoliciesSetByAdmins(policy).keySet(); + + List<EnforcingUser> enforcingUsers = new ArrayList(); + enforcingUsers.addAll(getEnforcingUsers(localAdmins)); + enforcingUsers.addAll(getEnforcingUsers(globalAdmins)); + return enforcingUsers; + } + + private List<EnforcingUser> getEnforcingUsers(Set<EnforcingAdmin> admins) { + List<EnforcingUser> enforcingUsers = new ArrayList(); + ComponentName deviceOwner = mOwners.getDeviceOwnerComponent(); + for (EnforcingAdmin admin : admins) { + if (deviceOwner != null + && deviceOwner.getPackageName().equals(admin.getPackageName())) { + enforcingUsers.add(new EnforcingUser(admin.getUserId(), + UserManager.RESTRICTION_SOURCE_DEVICE_OWNER)); + } else { + enforcingUsers.add(new EnforcingUser(admin.getUserId(), + UserManager.RESTRICTION_SOURCE_PROFILE_OWNER)); + } + } + return enforcingUsers; + } } private Intent createShowAdminSupportIntent(int userId) { @@ -18017,10 +18073,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { synchronized (getLockObject()) { getUserData(callingUserId).mAffiliationIds = affiliationIds; saveSettingsLocked(callingUserId); - if (callingUserId != UserHandle.USER_SYSTEM && isDeviceOwner(admin, callingUserId)) { + mStateCache.setHasAffiliationWithDevice(callingUserId, + isUserAffiliatedWithDeviceLocked(callingUserId)); + if (callingUserId == UserHandle.USER_SYSTEM) { + resetAffiliationCacheLocked(); + } else if (callingUserId != UserHandle.USER_SYSTEM && isDeviceOwner(admin, + callingUserId)) { // Affiliation ids specified by the device owner are additionally stored in // UserHandle.USER_SYSTEM's DevicePolicyData. getUserData(UserHandle.USER_SYSTEM).mAffiliationIds = affiliationIds; + mStateCache.setHasAffiliationWithDevice(UserHandle.USER_SYSTEM, true); saveSettingsLocked(UserHandle.USER_SYSTEM); } @@ -18034,6 +18096,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + private void resetAffiliationCacheLocked() { + mInjector.binderWithCleanCallingIdentity(() -> { + for (UserInfo user : mUserManager.getUsers()) { + mStateCache.setHasAffiliationWithDevice(user.id, + isUserAffiliatedWithDeviceLocked(user.id)); + } + }); + } + @Override public List<String> getAffiliationIds(ComponentName admin) { if (!mHasFeature) { @@ -20265,7 +20336,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } @Override - public List<String> getAllCrossProfilePackages() { + public List<String> getAllCrossProfilePackages(int userId) { if (!mHasFeature) { return Collections.emptyList(); } @@ -20274,10 +20345,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { isSystemUid(caller) || isRootUid(caller) || hasCallingPermission( permission.INTERACT_ACROSS_USERS) || hasCallingPermission( permission.INTERACT_ACROSS_USERS_FULL) || hasPermissionForPreflight( - caller, permission.INTERACT_ACROSS_PROFILES)); + caller, permission.INTERACT_ACROSS_PROFILES)); synchronized (getLockObject()) { - final List<ActiveAdmin> admins = getProfileOwnerAdminsForCurrentProfileGroup(); + final List<ActiveAdmin> admins = getProfileOwnerAdminsForProfileGroup(userId); final List<String> packages = getCrossProfilePackagesForAdmins(admins); packages.addAll(getDefaultCrossProfilePackages()); @@ -20306,11 +20377,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return new ArrayList<>(crossProfilePackages); } - private List<ActiveAdmin> getProfileOwnerAdminsForCurrentProfileGroup() { + private List<ActiveAdmin> getProfileOwnerAdminsForProfileGroup(int userId) { synchronized (getLockObject()) { final List<ActiveAdmin> admins = new ArrayList<>(); - int[] users = mUserManager.getProfileIdsWithDisabled( - mInjector.userHandleGetCallingUserId()); + int[] users = mUserManager.getProfileIdsWithDisabled(userId); for (int i = 0; i < users.length; i++) { final ComponentName componentName = getProfileOwnerAsUser(users[i]); if (componentName != null) { @@ -22998,6 +23068,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_FACTORY_RESET, MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES, MANAGE_DEVICE_POLICY_KEYGUARD, + MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, MANAGE_DEVICE_POLICY_LOCK_TASK, MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY, MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS, @@ -23005,7 +23076,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_SUPPORT_MESSAGE, MANAGE_DEVICE_POLICY_TIME, MANAGE_DEVICE_POLICY_USERS, - MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS + MANAGE_DEVICE_POLICY_WIPE_DATA ); /** @@ -23525,14 +23596,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { * * @param callerPackageName The package name of the calling application. * @param adminPolicy The admin policy that should grant holders permission. - * @param permission The name of the permission being checked. + * @param permissions The names of the permissions being checked. * @param targetUserId The userId of the user which the caller needs permission to act on. * @throws SecurityException if the caller has not been granted the given permission, * the associated cross-user permission if the caller's user is different to the target user. */ private void enforcePermissions(String[] permissions, int adminPolicy, String callerPackageName, int targetUserId) throws SecurityException { - if (hasAdminPolicy(adminPolicy, callerPackageName)) { + if (hasAdminPolicy(adminPolicy, callerPackageName) + && mInjector.userHandleGetCallingUserId() == targetUserId) { return; } enforcePermissions(permissions, callerPackageName, targetUserId); @@ -23561,8 +23633,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private boolean hasAdminPolicy(int adminPolicy, String callerPackageName) { CallerIdentity caller = getCallerIdentity(callerPackageName); - ActiveAdmin deviceAdmin = getActiveAdminForCaller(null, caller); - return deviceAdmin != null && deviceAdmin.info.usesPolicy(adminPolicy); + ActiveAdmin deviceAdmin = getActiveAdminWithPolicyForUidLocked( + null, adminPolicy, caller.getUid()); + return deviceAdmin != null; } /** @@ -23755,31 +23828,23 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return false; } - private boolean isWorkProfileTelephonyEnabled() { - return isWorkProfileTelephonyDevicePolicyManagerFlagEnabled() - && isWorkProfileTelephonySubscriptionManagerFlagEnabled(); - } - - private boolean isWorkProfileTelephonyDevicePolicyManagerFlagEnabled() { - return DeviceConfig.getBoolean(NAMESPACE_DEVICE_POLICY_MANAGER, - ENABLE_WORK_PROFILE_TELEPHONY_FLAG, DEFAULT_WORK_PROFILE_TELEPHONY_FLAG); - } - - private boolean isWorkProfileTelephonySubscriptionManagerFlagEnabled() { - final long ident = mInjector.binderClearCallingIdentity(); - try { - return DeviceConfig.getBoolean(NAMESPACE_TELEPHONY, ENABLE_WORK_PROFILE_TELEPHONY_FLAG, - false); - } finally { - mInjector.binderRestoreCallingIdentity(ident); + private void setKeepProfileRunningEnabledUnchecked(boolean keepProfileRunning) { + synchronized (getLockObject()) { + DevicePolicyData policyData = getUserDataUnchecked(UserHandle.USER_SYSTEM); + if (policyData.mEffectiveKeepProfilesRunning == keepProfileRunning) { + return; + } + policyData.mEffectiveKeepProfilesRunning = keepProfileRunning; + saveSettingsLocked(UserHandle.USER_SYSTEM); } + suspendAppsForQuietProfiles(keepProfileRunning); } @Override public void setOverrideKeepProfilesRunning(boolean enabled) { Preconditions.checkCallAuthorization( hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS)); - mKeepProfilesRunning = enabled; + setKeepProfileRunningEnabledUnchecked(enabled); Slog.i(LOG_TAG, "Keep profiles running overridden to: " + enabled); } @@ -23885,12 +23950,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public ManagedSubscriptionsPolicy getManagedSubscriptionsPolicy() { - if (isWorkProfileTelephonyEnabled()) { - synchronized (getLockObject()) { - ActiveAdmin admin = getProfileOwnerOfOrganizationOwnedDeviceLocked(); - if (admin != null && admin.mManagedSubscriptionsPolicy != null) { - return admin.mManagedSubscriptionsPolicy; - } + synchronized (getLockObject()) { + ActiveAdmin admin = getProfileOwnerOfOrganizationOwnedDeviceLocked(); + if (admin != null && admin.mManagedSubscriptionsPolicy != null) { + return admin.mManagedSubscriptionsPolicy; } } return new ManagedSubscriptionsPolicy( @@ -23899,10 +23962,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void setManagedSubscriptionsPolicy(ManagedSubscriptionsPolicy policy) { - if (!isWorkProfileTelephonyEnabled()) { + CallerIdentity caller = getCallerIdentity(); + + if (!isCallerDevicePolicyManagementRoleHolder(caller) + && !Objects.equals(mInjector.settingsGlobalGetString( + Global.ALLOW_WORK_PROFILE_TELEPHONY_FOR_NON_DPM_ROLE_HOLDERS), "1")) { throw new UnsupportedOperationException("This api is not enabled"); } - CallerIdentity caller = getCallerIdentity(); + Preconditions.checkCallAuthorization(isProfileOwnerOfOrganizationOwnedDevice(caller), "This policy can only be set by a profile owner on an organization-owned " + "device."); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java index 011a282ead7a..47607d7426e8 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceStateCacheImpl.java @@ -42,6 +42,8 @@ public class DeviceStateCacheImpl extends DeviceStateCache { private AtomicInteger mDeviceOwnerType = new AtomicInteger(NO_DEVICE_OWNER); private Map<Integer, Boolean> mHasProfileOwner = new ConcurrentHashMap<>(); + private Map<Integer, Boolean> mAffiliationWithDevice = new ConcurrentHashMap<>(); + @GuardedBy("mLock") private boolean mIsDeviceProvisioned = false; @@ -70,6 +72,19 @@ public class DeviceStateCacheImpl extends DeviceStateCache { } } + void setHasAffiliationWithDevice(int userId, Boolean hasAffiliateProfileOwner) { + if (hasAffiliateProfileOwner) { + mAffiliationWithDevice.put(userId, true); + } else { + mAffiliationWithDevice.remove(userId); + } + } + + @Override + public boolean hasAffiliationWithDevice(int userId) { + return mAffiliationWithDevice.getOrDefault(userId, false); + } + @Override public boolean isUserOrganizationManaged(@UserIdInt int userHandle) { if (mHasProfileOwner.getOrDefault(userHandle, false) diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java index 3ed2d34be6df..5243d14af1ab 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java @@ -30,6 +30,7 @@ import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.role.RoleManagerLocal; import com.android.server.LocalManagerRegistry; +import com.android.server.utils.Slogf; import org.xmlpull.v1.XmlPullParserException; @@ -51,6 +52,9 @@ import java.util.Set; * */ final class EnforcingAdmin { + + static final String TAG = "EnforcingAdmin"; + static final String ROLE_AUTHORITY_PREFIX = "role:"; static final String DPC_AUTHORITY = "enterprise"; static final String DEVICE_ADMIN_AUTHORITY = "device_admin"; @@ -286,6 +290,7 @@ final class EnforcingAdmin { } } + @Nullable static EnforcingAdmin readFromXml(TypedXmlPullParser parser) throws XmlPullParserException { String packageName = parser.getAttributeValue(/* namespace= */ null, ATTR_PACKAGE_NAME); @@ -294,13 +299,25 @@ final class EnforcingAdmin { int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID); if (isRoleAuthority) { + if (packageName == null) { + Slogf.wtf(TAG, "Error parsing EnforcingAdmin with RoleAuthority, packageName is " + + "null."); + return null; + } // TODO(b/281697976): load active admin return new EnforcingAdmin(packageName, userId, null); } else { + if (packageName == null || authoritiesStr == null) { + Slogf.wtf(TAG, "Error parsing EnforcingAdmin, packageName is " + + (packageName == null ? "null" : packageName) + ", and authorities is " + + (authoritiesStr == null ? "null" : authoritiesStr) + "."); + return null; + } String className = parser.getAttributeValue(/* namespace= */ null, ATTR_CLASS_NAME); ComponentName componentName = className == null ? null : new ComponentName(packageName, className); Set<String> authorities = Set.of(authoritiesStr.split(ATTR_AUTHORITIES_SEPARATOR)); + // TODO(b/281697976): load active admin return new EnforcingAdmin(packageName, componentName, authorities, userId, null); } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java index 194647fda92c..0c1c406dd7e2 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java @@ -131,7 +131,8 @@ class Owners { } notifyChangeLocked(); - pushToActivityTaskManagerLocked(); + pushDeviceOwnerUidToActivityTaskManagerLocked(); + pushProfileOwnerUidsToActivityTaskManagerLocked(); } } @@ -163,11 +164,16 @@ class Owners { } @GuardedBy("mData") - private void pushToActivityTaskManagerLocked() { + private void pushDeviceOwnerUidToActivityTaskManagerLocked() { mActivityTaskManagerInternal.setDeviceOwnerUid(getDeviceOwnerUidLocked()); } @GuardedBy("mData") + private void pushProfileOwnerUidsToActivityTaskManagerLocked() { + mActivityTaskManagerInternal.setProfileOwnerUids(getProfileOwnerUidsLocked()); + } + + @GuardedBy("mData") private void pushToActivityManagerLocked() { mActivityManagerInternal.setDeviceOwnerUid(getDeviceOwnerUidLocked()); @@ -196,6 +202,11 @@ class Owners { } } + @GuardedBy("mData") + Set<Integer> getProfileOwnerUidsLocked() { + return mData.mProfileOwners.keySet(); + } + String getDeviceOwnerPackageName() { synchronized (mData) { return mData.mDeviceOwner != null ? mData.mDeviceOwner.packageName : null; @@ -263,7 +274,7 @@ class Owners { } notifyChangeLocked(); - pushToActivityTaskManagerLocked(); + pushDeviceOwnerUidToActivityTaskManagerLocked(); } } @@ -282,7 +293,7 @@ class Owners { mUserManagerInternal.setDeviceManaged(false); } notifyChangeLocked(); - pushToActivityTaskManagerLocked(); + pushDeviceOwnerUidToActivityTaskManagerLocked(); } } @@ -302,6 +313,7 @@ class Owners { mUserManagerInternal.setUserManaged(userId, true); } notifyChangeLocked(); + pushProfileOwnerUidsToActivityTaskManagerLocked(); } } @@ -317,6 +329,7 @@ class Owners { mUserManagerInternal.setUserManaged(userId, false); } notifyChangeLocked(); + pushProfileOwnerUidsToActivityTaskManagerLocked(); } } @@ -328,6 +341,7 @@ class Owners { ownerInfo.isOrganizationOwnedDevice); mData.mProfileOwners.put(userId, newOwnerInfo); notifyChangeLocked(); + pushProfileOwnerUidsToActivityTaskManagerLocked(); } } @@ -345,7 +359,7 @@ class Owners { mData.mDeviceOwner.packageName, previousDeviceOwnerType); } notifyChangeLocked(); - pushToActivityTaskManagerLocked(); + pushDeviceOwnerUidToActivityTaskManagerLocked(); } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java index 63b250d4acfc..37d4f95cac29 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java @@ -457,6 +457,7 @@ class OwnersData { case TAG_POLICY_ENGINE_MIGRATION: mMigratedToPolicyEngine = parser.getAttributeBoolean( null, ATTR_MIGRATED_TO_POLICY_ENGINE, false); + break; default: Slog.e(TAG, "Unexpected tag: " + tag); return false; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 7e5bb0bb40ba..7e48407fc911 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -42,6 +42,7 @@ import android.os.UserManager; import com.android.internal.util.function.QuadFunction; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.utils.Slogf; import org.xmlpull.v1.XmlPullParserException; @@ -53,6 +54,9 @@ import java.util.Map; import java.util.Set; final class PolicyDefinition<V> { + + static final String TAG = "PolicyDefinition"; + private static final int POLICY_FLAG_NONE = 0; // Only use this flag if a policy can not be applied locally. @@ -596,22 +600,40 @@ final class PolicyDefinition<V> { mPolicyKey.saveToXml(serializer); } + @Nullable static <V> PolicyDefinition<V> readFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { // TODO: can we avoid casting? PolicyKey policyKey = readPolicyKeyFromXml(parser); + if (policyKey == null) { + Slogf.wtf(TAG, "Error parsing PolicyDefinition, PolicyKey is null."); + return null; + } PolicyDefinition<V> genericPolicyDefinition = (PolicyDefinition<V>) POLICY_DEFINITIONS.get(policyKey.getIdentifier()); + if (genericPolicyDefinition == null) { + Slogf.wtf(TAG, "Unknown generic policy key: " + policyKey); + return null; + } return genericPolicyDefinition.createPolicyDefinition(policyKey); } + @Nullable static <V> PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { // TODO: can we avoid casting? PolicyKey policyKey = PolicyKey.readGenericPolicyKeyFromXml(parser); + if (policyKey == null) { + Slogf.wtf(TAG, "Error parsing PolicyKey, GenericPolicyKey is null"); + return null; + } PolicyDefinition<PolicyValue<V>> genericPolicyDefinition = (PolicyDefinition<PolicyValue<V>>) POLICY_DEFINITIONS.get( policyKey.getIdentifier()); + if (genericPolicyDefinition == null) { + Slogf.wtf(TAG, "Error parsing PolicyKey, Unknown generic policy key: " + policyKey); + return null; + } return genericPolicyDefinition.mPolicyKey.readFromXml(parser); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java index dd4c6afdcfb6..599c4a7441c5 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java @@ -19,11 +19,11 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.PolicyValue; -import android.util.Log; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.utils.Slogf; import org.xmlpull.v1.XmlPullParserException; @@ -224,6 +224,7 @@ final class PolicyState<V> { } } + @Nullable static <V> PolicyState<V> readFromXml(TypedXmlPullParser parser) throws IOException, XmlPullParserException { @@ -245,33 +246,55 @@ final class PolicyState<V> { switch (adminPolicyTag) { case TAG_ENFORCING_ADMIN_ENTRY: admin = EnforcingAdmin.readFromXml(parser); + if (admin == null) { + Slogf.wtf(TAG, "Error Parsing TAG_ENFORCING_ADMIN_ENTRY, " + + "EnforcingAdmin is null"); + } break; case TAG_POLICY_VALUE_ENTRY: value = policyDefinition.readPolicyValueFromXml(parser); + if (value == null) { + Slogf.wtf(TAG, "Error Parsing TAG_POLICY_VALUE_ENTRY, " + + "PolicyValue is null"); + } break; } } if (admin != null) { policiesSetByAdmins.put(admin, value); } else { - Log.e(TAG, "Error Parsing TAG_ADMIN_POLICY_ENTRY"); + Slogf.wtf(TAG, "Error Parsing TAG_ADMIN_POLICY_ENTRY, EnforcingAdmin " + + "is null"); } break; case TAG_POLICY_DEFINITION_ENTRY: policyDefinition = PolicyDefinition.readFromXml(parser); + if (policyDefinition == null) { + Slogf.wtf(TAG, "Error Parsing TAG_POLICY_DEFINITION_ENTRY, " + + "PolicyDefinition is null"); + } break; case TAG_RESOLVED_VALUE_ENTRY: + if (policyDefinition == null) { + Slogf.wtf(TAG, "Error Parsing TAG_RESOLVED_VALUE_ENTRY, " + + "policyDefinition is null"); + break; + } currentResolvedPolicy = policyDefinition.readPolicyValueFromXml(parser); + if (currentResolvedPolicy == null) { + Slogf.wtf(TAG, "Error Parsing TAG_RESOLVED_VALUE_ENTRY, " + + "currentResolvedPolicy is null"); + } break; default: - Log.e(TAG, "Unknown tag: " + tag); + Slogf.wtf(TAG, "Unknown tag: " + tag); } } if (policyDefinition != null) { return new PolicyState<V>(policyDefinition, policiesSetByAdmins, currentResolvedPolicy); } else { - Log.e("PolicyState", "Error parsing policyState"); + Slogf.wtf(TAG, "Error parsing policyState, policyDefinition is null"); return null; } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyVersionUpgrader.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyVersionUpgrader.java index 1fe4b571a770..06f11be20245 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyVersionUpgrader.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyVersionUpgrader.java @@ -110,6 +110,12 @@ public class PolicyVersionUpgrader { currentVersion = 4; } + if (currentVersion == 4) { + Slog.i(LOG_TAG, String.format("Upgrading from version %d", currentVersion)); + initializeEffectiveKeepProfilesRunning(allUsersData); + currentVersion = 5; + } + writePoliciesAndVersion(allUsers, allUsersData, ownersData, currentVersion); } @@ -214,6 +220,16 @@ public class PolicyVersionUpgrader { ownerAdmin.suspendedPackages.size(), ownerPackage, ownerUserId)); } + private void initializeEffectiveKeepProfilesRunning( + SparseArray<DevicePolicyData> allUsersData) { + DevicePolicyData systemUserData = allUsersData.get(UserHandle.USER_SYSTEM); + if (systemUserData == null) { + return; + } + systemUserData.mEffectiveKeepProfilesRunning = false; + Slog.i(LOG_TAG, "Keep profile running effective state set to false"); + } + private OwnersData loadOwners(int[] allUsers) { OwnersData ownersData = new OwnersData(mPathProvider); ownersData.load(allUsers); diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java index 8f23ae4ff33b..4007672a0599 100644 --- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java +++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java @@ -34,6 +34,8 @@ import android.os.SystemProperties; import android.os.UpdateEngine; import android.os.UpdateEngineCallback; import android.provider.DeviceConfig; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; import android.util.Log; import com.android.internal.R; @@ -332,8 +334,17 @@ public final class ProfcollectForwardingService extends SystemService { Context context = getContext(); BackgroundThread.get().getThreadHandler().post(() -> { try { + int usageSetting = -1; + try { + // Get "Usage & diagnostics" checkbox status. 1 is for enabled, 0 is for + // disabled. + usageSetting = Settings.Global.getInt(context.getContentResolver(), "multi_cb"); + } catch (SettingNotFoundException e) { + Log.i(LOG_TAG, "Usage setting not found: " + e.getMessage()); + } + // Prepare profile report - String reportName = mIProfcollect.report() + ".zip"; + String reportName = mIProfcollect.report(usageSetting) + ".zip"; if (!context.getResources().getBoolean( R.bool.config_profcollectReportUploaderEnabled)) { diff --git a/services/robotests/src/com/android/server/pm/CrossProfileAppsServiceImplRoboTest.java b/services/robotests/src/com/android/server/pm/CrossProfileAppsServiceImplRoboTest.java index d128e689e01e..4e468362177a 100644 --- a/services/robotests/src/com/android/server/pm/CrossProfileAppsServiceImplRoboTest.java +++ b/services/robotests/src/com/android/server/pm/CrossProfileAppsServiceImplRoboTest.java @@ -201,7 +201,7 @@ public class CrossProfileAppsServiceImplRoboTest { } private void mockCrossProfileAppWhitelisted() { - when(mDevicePolicyManagerInternal.getAllCrossProfilePackages()) + when(mDevicePolicyManagerInternal.getAllCrossProfilePackages(anyInt())) .thenReturn(Lists.newArrayList(CROSS_PROFILE_APP_PACKAGE_NAME)); } @@ -662,7 +662,7 @@ public class CrossProfileAppsServiceImplRoboTest { } private void mockCrossProfileAppNotWhitelisted() { - when(mDevicePolicyManagerInternal.getAllCrossProfilePackages()) + when(mDevicePolicyManagerInternal.getAllCrossProfilePackages(anyInt())) .thenReturn(new ArrayList<>()); } diff --git a/services/tests/PackageManagerServiceTests/host/Android.bp b/services/tests/PackageManagerServiceTests/host/Android.bp index d9467a5e1712..c7a71eeb969c 100644 --- a/services/tests/PackageManagerServiceTests/host/Android.bp +++ b/services/tests/PackageManagerServiceTests/host/Android.bp @@ -39,7 +39,7 @@ java_test_host { "PackageManagerServiceHostTestsIntentVerifyUtils", "block_device_writer_jar", ], - test_suites: ["general-tests"], + test_suites: ["device-tests"], data: [ ":PackageManagerTestApex", ":PackageManagerTestApexApp", diff --git a/services/tests/PackageManagerServiceTests/host/AndroidTest.xml b/services/tests/PackageManagerServiceTests/host/AndroidTest.xml index 2382548192ad..f594f6f06ed2 100644 --- a/services/tests/PackageManagerServiceTests/host/AndroidTest.xml +++ b/services/tests/PackageManagerServiceTests/host/AndroidTest.xml @@ -30,6 +30,7 @@ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true" /> <option name="install-arg" value="-t" /> + <option name="install-arg" value="-g" /> <option name="test-file-name" value="PackageManagerServiceServerTests.apk" /> </target_preparer> diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt index 6d3cdffda837..320087111c50 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt @@ -218,7 +218,6 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::isClearUserDataOnFailedRestoreAllowed, AndroidPackage::isAllowNativeHeapPointerTagging, AndroidPackage::isTaskReparentingAllowed, - AndroidPackage::isAllowUpdateOwnership, AndroidPackage::isBackupInForeground, AndroidPackage::isHardwareAccelerated, AndroidPackage::isSaveStateDisallowed, diff --git a/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-3.xml b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-3.xml new file mode 100644 index 000000000000..1363bf783ac2 --- /dev/null +++ b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-3.xml @@ -0,0 +1,801 @@ +<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> +<!-- + ~ 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. + --> + +<app-ops v="3"> + <uid n="1001"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="15" m="0" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="1002"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="15" m="0" /> + <op n="87" m="1" /> + </uid> + <uid n="10077"> + <op n="87" m="1" /> + </uid> + <uid n="10079"> + <op n="116" m="1" /> + </uid> + <uid n="10080"> + <op n="87" m="1" /> + </uid> + <uid n="10081"> + <op n="87" m="1" /> + </uid> + <uid n="10086"> + <op n="87" m="1" /> + </uid> + <uid n="10087"> + <op n="59" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10090"> + <op n="0" m="4" /> + <op n="1" m="4" /> + </uid> + <uid n="10096"> + <op n="0" m="1" /> + <op n="1" m="1" /> + </uid> + <uid n="10112"> + <op n="11" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="62" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10113"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="51" m="1" /> + <op n="62" m="1" /> + </uid> + <uid n="10114"> + <op n="4" m="1" /> + </uid> + <uid n="10115"> + <op n="0" m="1" /> + </uid> + <uid n="10116"> + <op n="0" m="1" /> + </uid> + <uid n="10117"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="13" m="1" /> + <op n="14" m="1" /> + <op n="16" m="1" /> + <op n="26" m="4" /> + <op n="27" m="4" /> + </uid> + <uid n="10118"> + <op n="51" m="1" /> + </uid> + <uid n="10119"> + <op n="11" m="1" /> + <op n="77" m="1" /> + <op n="111" m="1" /> + </uid> + <uid n="10120"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="6" m="1" /> + <op n="7" m="1" /> + <op n="11" m="1" /> + <op n="13" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="54" m="1" /> + <op n="59" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10121"> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10122"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10123"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10124"> + <op n="11" m="1" /> + <op n="26" m="1" /> + </uid> + <uid n="10125"> + <op n="11" m="1" /> + </uid> + <uid n="10127"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="65" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + <op n="111" m="1" /> + </uid> + <uid n="10129"> + <op n="0" m="1" /> + <op n="1" m="1" /> + </uid> + <uid n="10130"> + <op n="51" m="1" /> + </uid> + <uid n="10131"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10132"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="69" m="1" /> + <op n="79" m="1" /> + </uid> + <uid n="10133"> + <op n="11" m="1" /> + </uid> + <uid n="10136"> + <op n="0" m="1" /> + <op n="4" m="1" /> + <op n="77" m="1" /> + <op n="87" m="1" /> + <op n="111" m="1" /> + <op n="114" m="1" /> + </uid> + <uid n="10137"> + <op n="62" m="1" /> + </uid> + <uid n="10138"> + <op n="26" m="4" /> + </uid> + <uid n="10140"> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10141"> + <op n="11" m="1" /> + <op n="27" m="1" /> + <op n="111" m="1" /> + </uid> + <uid n="10142"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + </uid> + <uid n="10144"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="27" m="4" /> + <op n="111" m="1" /> + </uid> + <uid n="10145"> + <op n="11" m="1" /> + </uid> + <uid n="10149"> + <op n="11" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10150"> + <op n="51" m="1" /> + </uid> + <uid n="10151"> + <op n="11" m="1" /> + </uid> + <uid n="10152"> + <op n="11" m="1" /> + </uid> + <uid n="10154"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="27" m="4" /> + </uid> + <uid n="10155"> + <op n="0" m="1" /> + <op n="1" m="1" /> + </uid> + <uid n="10157"> + <op n="13" m="1" /> + </uid> + <uid n="10158"> + <op n="11" m="1" /> + </uid> + <uid n="10160"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10161"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + <op n="51" m="1" /> + <op n="77" m="1" /> + <op n="87" m="1" /> + <op n="111" m="1" /> + <op n="114" m="1" /> + </uid> + <uid n="10162"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="15" m="0" /> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + <op n="89" m="0" /> + </uid> + <uid n="10163"> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="56" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10164"> + <op n="26" m="1" /> + <op n="27" m="4" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="69" m="1" /> + <op n="79" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10169"> + <op n="11" m="1" /> + </uid> + <uid n="10170"> + <op n="87" m="1" /> + </uid> + <uid n="10171"> + <op n="26" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10172"> + <op n="11" m="1" /> + </uid> + <uid n="10173"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="23" m="0" /> + <op n="26" m="1" /> + <op n="51" m="1" /> + <op n="62" m="1" /> + <op n="65" m="1" /> + </uid> + <uid n="10175"> + <op n="0" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + </uid> + <uid n="10178"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + <op n="27" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10179"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="62" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10180"> + <op n="11" m="1" /> + </uid> + <uid n="10181"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="59" m="1" /> + <op n="62" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="1110181"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + <op n="107" m="2" /> + </uid> + <uid n="10182"> + <op n="27" m="4" /> + </uid> + <uid n="10183"> + <op n="11" m="1" /> + <op n="26" m="4" /> + </uid> + <uid n="10184"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="13" m="1" /> + <op n="26" m="1" /> + <op n="27" m="4" /> + </uid> + <uid n="10185"> + <op n="8" m="1" /> + <op n="59" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10187"> + <op n="11" m="1" /> + </uid> + <uid n="10189"> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="51" m="1" /> + </uid> + <uid n="10190"> + <op n="0" m="1" /> + <op n="13" m="1" /> + </uid> + <uid n="10191"> + <op n="11" m="1" /> + </uid> + <uid n="10192"> + <op n="11" m="1" /> + <op n="13" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10193"> + <op n="11" m="1" /> + </uid> + <uid n="10197"> + <op n="11" m="1" /> + </uid> + <uid n="10198"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="77" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + <op n="90" m="1" /> + <op n="107" m="0" /> + <op n="111" m="1" /> + <op n="133" m="0" /> + </uid> + <uid n="10199"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="62" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10200"> + <op n="11" m="1" /> + <op n="65" m="1" /> + <op n="107" m="1" /> + <op n="133" m="1" /> + </uid> + <uid n="1110200"> + <op n="11" m="1" /> + <op n="65" m="1" /> + <op n="107" m="2" /> + <op n="133" m="2"/> + </uid> + <uid n="10201"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="51" m="1" /> + <op n="62" m="1" /> + <op n="84" m="0" /> + <op n="86" m="0" /> + <op n="87" m="1" /> + </uid> + <uid n="10206"> + <op n="0" m="4" /> + <op n="1" m="4" /> + <op n="26" m="4" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10209"> + <op n="11" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10210"> + <op n="51" m="1" /> + </uid> + <uid n="10212"> + <op n="11" m="1" /> + <op n="62" m="1" /> + </uid> + <uid n="10214"> + <op n="26" m="4" /> + </uid> + <uid n="10216"> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10225"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10229"> + <op n="11" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="62" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10231"> + <op n="51" m="1" /> + </uid> + <uid n="10232"> + <op n="51" m="1" /> + </uid> + <uid n="10234"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="11" m="1" /> + <op n="13" m="1" /> + <op n="20" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10235"> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10237"> + <op n="0" m="4" /> + <op n="1" m="4" /> + </uid> + <uid n="10238"> + <op n="26" m="4" /> + <op n="27" m="4" /> + <op n="87" m="1" /> + </uid> + <uid n="10240"> + <op n="112" m="1" /> + </uid> + <uid n="10241"> + <op n="59" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10245"> + <op n="13" m="1" /> + <op n="51" m="1" /> + </uid> + <uid n="10247"> + <op n="0" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="0" /> + <op n="90" m="1" /> + </uid> + <uid n="10254"> + <op n="11" m="1" /> + </uid> + <uid n="10255"> + <op n="11" m="1" /> + </uid> + <uid n="10256"> + <op n="87" m="1" /> + </uid> + <uid n="10258"> + <op n="11" m="1" /> + </uid> + <uid n="10260"> + <op n="51" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="87" m="1" /> + </uid> + <uid n="10262"> + <op n="15" m="0" /> + </uid> + <uid n="10266"> + <op n="0" m="4" /> + </uid> + <uid n="10267"> + <op n="0" m="1" /> + <op n="1" m="1" /> + <op n="4" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="62" m="1" /> + <op n="77" m="1" /> + <op n="81" m="1" /> + <op n="83" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + <op n="107" m="2" /> + <op n="111" m="1" /> + <op n="133" m="2" /> + </uid> + <uid n="10268"> + <op n="4" m="1" /> + <op n="11" m="1" /> + <op n="62" m="1" /> + </uid> + <uid n="10269"> + <op n="11" m="1" /> + <op n="26" m="1" /> + <op n="27" m="1" /> + <op n="59" m="1" /> + <op n="60" m="1" /> + <op n="85" m="1" /> + <op n="87" m="1" /> + </uid> + <pkg n="com.google.android.iwlan"> + <uid n="0"> + <op n="1" /> + <op n="75" m="0" /> + </uid> + </pkg> + <pkg n="com.android.phone"> + <uid n="0"> + <op n="1" /> + <op n="75" m="0" /> + </uid> + </pkg> + <pkg n="android"> + <uid n="1000"> + <op n="0"> + <st n="214748364801" t="1670287941040" /> + </op> + <op n="4"> + <st n="214748364801" t="1670289665522" /> + </op> + <op n="6"> + <st n="214748364801" t="1670287946650" /> + </op> + <op n="8"> + <st n="214748364801" t="1670289624396" /> + </op> + <op n="14"> + <st n="214748364801" t="1670287951031" /> + </op> + <op n="40"> + <st n="214748364801" t="1670291786337" d="156" /> + </op> + <op n="41"> + <st id="SensorNotificationService" n="214748364801" t="1670287585567" d="4251183" /> + <st id="CountryDetector" n="214748364801" t="1670287583306" d="6700" /> + </op> + <op n="43"> + <st n="214748364801" r="1670291755062" /> + </op> + <op n="61"> + <st n="214748364801" r="1670291754997" /> + </op> + <op n="105"> + <st n="214748364801" r="1670291473903" /> + <st id="GnssService" n="214748364801" r="1670288044920" /> + </op> + <op n="111"> + <st n="214748364801" t="1670291441554" /> + </op> + </uid> + </pkg> + <pkg n="com.android.server.telecom"> + <uid n="1000"> + <op n="6"> + <st n="214748364801" t="1670287609092" /> + </op> + <op n="111"> + <st n="214748364801" t="1670287583728" /> + </op> + </uid> + </pkg> + <pkg n="com.android.settings"> + <uid n="1000"> + <op n="43"> + <st n="214748364801" r="1670291447349" /> + </op> + <op n="105"> + <st n="214748364801" r="1670291399231" /> + </op> + <op n="111"> + <st n="214748364801" t="1670291756910" /> + </op> + </uid> + </pkg> + <pkg n="com.android.phone"> + <uid n="1001"> + <op n="15"> + <st n="214748364801" t="1670287951022" /> + </op> + <op n="40"> + <st n="214748364801" t="1670291786177" /> + </op> + <op n="105"> + <st n="214748364801" r="1670291756403" /> + </op> + </uid> + </pkg> + <pkg n="com.android.bluetooth"> + <uid n="1002"> + <op n="4"> + <st n="214748364801" t="1670289671076" /> + </op> + <op n="40"> + <st n="214748364801" t="1670287585676" d="8" /> + </op> + <op n="43"> + <st n="214748364801" r="1670287585818" /> + </op> + <op n="77"> + <st n="214748364801" t="1670288037629" /> + </op> + <op n="111"> + <st n="214748364801" t="1670287592081" /> + </op> + </uid> + </pkg> + <pkg n="com.android.vending"> + <uid n="10136"> + <op n="40"> + <st n="429496729601" t="1670289621210" d="114" /> + <st n="858993459201" t="1670289879730" d="349" /> + <st n="1288490188801" t="1670287942622" d="937" /> + </op> + <op n="43"> + <st n="429496729601" r="1670289755305" /> + <st n="858993459201" r="1670288019246" /> + <st n="1073741824001" r="1670289571783" /> + <st n="1288490188801" r="1670289373336" /> + </op> + <op n="76"> + <st n="429496729601" t="1670289748735" d="15991" /> + <st n="858993459201" t="1670291395180" d="79201" /> + <st n="1073741824001" t="1670291395168" d="12" /> + <st n="1288490188801" t="1670291526029" d="3" /> + <st n="1503238553601" t="1670291526032" d="310718" /> + </op> + <op n="105"> + <st n="429496729601" r="1670289538910" /> + <st n="858993459201" r="1670288054519" /> + <st n="1073741824001" r="1670287599379" /> + <st n="1288490188801" r="1670289526854" /> + <st n="1503238553601" r="1670289528242" /> + </op> + </uid> + </pkg> + <pkg n="com.android.nfc"> + <uid n="1027"> + <op n="40"> + <st n="214748364801" t="1670291786330" d="22" /> + </op> + </uid> + </pkg> +</app-ops>
\ No newline at end of file diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index f82d246d68bd..a614c4dd1d61 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -37,6 +37,7 @@ import static com.android.server.am.ProcessList.NETWORK_STATE_BLOCK; import static com.android.server.am.ProcessList.NETWORK_STATE_NO_CHANGE; import static com.android.server.am.ProcessList.NETWORK_STATE_UNBLOCK; +import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; @@ -60,6 +61,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import android.Manifest; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.BroadcastOptions; @@ -82,13 +84,16 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; +import android.provider.DeviceConfig; import android.util.IntArray; import android.util.Log; import android.util.Pair; import androidx.test.filters.MediumTest; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService.StickyBroadcast; import com.android.server.am.ProcessList.IsolatedUidRange; @@ -106,6 +111,7 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; import java.io.File; import java.util.ArrayList; @@ -137,7 +143,9 @@ public class ActivityManagerServiceTest { private static final String TEST_EXTRA_KEY1 = "com.android.server.am.TEST_EXTRA_KEY1"; private static final String TEST_EXTRA_VALUE1 = "com.android.server.am.TEST_EXTRA_VALUE1"; - + private static final String PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = + "apply_sdk_sandbox_next_restrictions"; + private static final String APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS = ":isSdkSandboxNext"; private static final int TEST_UID = 11111; private static final int USER_ID = 666; @@ -154,6 +162,7 @@ public class ActivityManagerServiceTest { }; private static PackageManagerInternal sPackageManagerInternal; + private static ProcessList.ProcessListSettingsListener sProcessListSettingsListener; @BeforeClass public static void setUpOnce() { @@ -181,7 +190,6 @@ public class ActivityManagerServiceTest { private ActivityManagerService mAms; private HandlerThread mHandlerThread; private TestHandler mHandler; - @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -203,6 +211,15 @@ public class ActivityManagerServiceTest { mAms.mActivityTaskManager.initialize(null, null, mHandler.getLooper()); mHandler.setRunnablesToIgnore( List.of(mAms.mUidObserverController.getDispatchRunnableForTest())); + + // Required for updating DeviceConfig. + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity( + Manifest.permission.READ_DEVICE_CONFIG, + Manifest.permission.WRITE_DEVICE_CONFIG); + sProcessListSettingsListener = mAms.mProcessList.getProcessListSettingsListener(); + assertThat(sProcessListSettingsListener).isNotNull(); } private void mockNoteOperation() { @@ -216,6 +233,12 @@ public class ActivityManagerServiceTest { @After public void tearDown() { mHandlerThread.quit(); + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + if (sProcessListSettingsListener != null) { + sProcessListSettingsListener.unregisterObserver(); + } } @SuppressWarnings("GuardedBy") @@ -268,6 +291,77 @@ public class ActivityManagerServiceTest { false); // expectNotify } + @SuppressWarnings("GuardedBy") + @SmallTest + @Test + public void defaultSdkSandboxNextRestrictions() throws Exception { + sProcessListSettingsListener.onPropertiesChanged( + new DeviceConfig.Properties( + DeviceConfig.NAMESPACE_ADSERVICES, + Map.of(PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, ""))); + assertThat( + sProcessListSettingsListener.applySdkSandboxRestrictionsNext()) + .isFalse(); + } + + @SuppressWarnings("GuardedBy") + @SmallTest + @Test + public void doNotApplySdkSandboxNextRestrictions() throws Exception { + MockitoSession mockitoSession = + ExtendedMockito.mockitoSession().spyStatic(Process.class).startMocking(); + try { + sProcessListSettingsListener.onPropertiesChanged( + new DeviceConfig.Properties( + DeviceConfig.NAMESPACE_ADSERVICES, + Map.of(PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, "false"))); + assertThat( + sProcessListSettingsListener.applySdkSandboxRestrictionsNext()) + .isFalse(); + ExtendedMockito.doReturn(true).when(() -> Process.isSdkSandboxUid(anyInt())); + ApplicationInfo info = new ApplicationInfo(); + info.packageName = "com.android.sdksandbox"; + info.seInfo = "default:targetSdkVersion=34:complete"; + final ProcessRecord appRec = new ProcessRecord( + mAms, info, TAG, Process.FIRST_SDK_SANDBOX_UID, + /* sdkSandboxClientPackageName= */ "com.example.client", + /* definingUid= */ 0, /* definingProcessName= */ ""); + assertThat(mAms.mProcessList.updateSeInfo(appRec)).doesNotContain( + APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS); + } finally { + mockitoSession.finishMocking(); + } + } + @SuppressWarnings("GuardedBy") + @SmallTest + @Test + public void applySdkSandboxNextRestrictions() throws Exception { + MockitoSession mockitoSession = + ExtendedMockito.mockitoSession().spyStatic(Process.class).startMocking(); + try { + sProcessListSettingsListener.onPropertiesChanged( + new DeviceConfig.Properties( + DeviceConfig.NAMESPACE_ADSERVICES, + Map.of(PROPERTY_APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS, "true"))); + assertThat( + sProcessListSettingsListener.applySdkSandboxRestrictionsNext()) + .isTrue(); + ExtendedMockito.doReturn(true).when(() -> Process.isSdkSandboxUid(anyInt())); + ApplicationInfo info = new ApplicationInfo(); + info.packageName = "com.android.sdksandbox"; + info.seInfo = "default:targetSdkVersion=34:complete"; + final ProcessRecord appRec = new ProcessRecord( + mAms, info, TAG, Process.FIRST_SDK_SANDBOX_UID, + /* sdkSandboxClientPackageName= */ "com.example.client", + /* definingUid= */ 0, /* definingProcessName= */ ""); + assertThat(mAms.mProcessList.updateSeInfo(appRec)).contains( + APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS); + } finally { + mockitoSession.finishMocking(); + } + } + + private UidRecord addUidRecord(int uid) { final UidRecord uidRec = new UidRecord(uid, mAms); uidRec.procStateSeqWaitingForNetwork = 1; @@ -648,24 +742,24 @@ public class ActivityManagerServiceTest { broadcastIntent(intent1, null, true); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION1, TEST_USER), - StickyBroadcast.create(intent1, false)); + StickyBroadcast.create(intent1, false, Process.myUid())); assertNull(mAms.getStickyBroadcasts(TEST_ACTION2, TEST_USER)); assertNull(mAms.getStickyBroadcasts(TEST_ACTION3, TEST_USER)); broadcastIntent(intent2, options.toBundle(), true); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION1, TEST_USER), - StickyBroadcast.create(intent1, false)); + StickyBroadcast.create(intent1, false, Process.myUid())); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION2, TEST_USER), - StickyBroadcast.create(intent2, true)); + StickyBroadcast.create(intent2, true, Process.myUid())); assertNull(mAms.getStickyBroadcasts(TEST_ACTION3, TEST_USER)); broadcastIntent(intent3, null, true); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION1, TEST_USER), - StickyBroadcast.create(intent1, false)); + StickyBroadcast.create(intent1, false, Process.myUid())); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION2, TEST_USER), - StickyBroadcast.create(intent2, true)); + StickyBroadcast.create(intent2, true, Process.myUid())); assertStickyBroadcasts(mAms.getStickyBroadcasts(TEST_ACTION3, TEST_USER), - StickyBroadcast.create(intent3, false)); + StickyBroadcast.create(intent3, false, Process.myUid())); } @SuppressWarnings("GuardedBy") @@ -698,6 +792,9 @@ public class ActivityManagerServiceTest { if (a.deferUntilActive != b.deferUntilActive) { return false; } + if (a.originalCallingUid != b.originalCallingUid) { + return false; + } return true; } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 581fe4acf219..f47954baf649 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -537,6 +537,30 @@ public final class BroadcastQueueModernImplTest { assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason()); } + @Test + public void testRunnableAt_persistentProc() { + final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants, PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); + final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, + List.of(makeMockRegisteredReceiver())); + enqueueOrReplaceBroadcast(queue, timeTickRecord, 0); + + assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason()); + + doReturn(true).when(mProcess).isPersistent(); + queue.setProcessAndUidState(mProcess, false, false); + assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime); + assertEquals(BroadcastProcessQueue.REASON_PERSISTENT, queue.getRunnableAtReason()); + + doReturn(false).when(mProcess).isPersistent(); + queue.setProcessAndUidState(mProcess, false, false); + assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime); + assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason()); + } + /** * Verify that a cached process that would normally be delayed becomes * immediately runnable when the given broadcast is enqueued. diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java index 5474c2092785..92d1118d0f1e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java @@ -17,6 +17,7 @@ package com.android.server.appop; import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM; +import static android.app.AppOpsManager.OP_USE_FULL_SCREEN_INTENT; import static android.app.AppOpsManager._NUM_OP; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; @@ -31,6 +32,7 @@ import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; @@ -44,6 +46,7 @@ import android.content.pm.UserPackage; import android.content.res.AssetManager; import android.os.Handler; import android.os.UserHandle; +import android.permission.PermissionManager; import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; @@ -87,6 +90,10 @@ public class AppOpsUpgradeTest { "AppOpsUpgradeTest/appops-unversioned.xml"; private static final String APP_OPS_VERSION_1_ASSET_PATH = "AppOpsUpgradeTest/appops-version-1.xml"; + + private static final String APP_OPS_VERSION_3_ASSET_PATH = + "AppOpsUpgradeTest/appops-version-3.xml"; + private static final String APP_OPS_FILENAME = "appops-test.xml"; private static final Context sContext = InstrumentationRegistry.getTargetContext(); @@ -105,6 +112,8 @@ public class AppOpsUpgradeTest { private PermissionManagerServiceInternal mPermissionManagerInternal; @Mock private Handler mHandler; + @Mock + private PermissionManager mPermissionManager; private Object mLock = new Object(); private SparseArray<int[]> mSwitchedOps; @@ -211,7 +220,7 @@ public class AppOpsUpgradeTest { } } - private static int getModeInFile(int uid) { + private static int getModeInFile(int uid, int op) { switch (uid) { case 10198: return 0; @@ -222,7 +231,7 @@ public class AppOpsUpgradeTest { case 1110181: return 2; default: - return AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM); + return AppOpsManager.opToDefaultMode(op); } } @@ -258,7 +267,7 @@ public class AppOpsUpgradeTest { for (int userId : userIds) { for (int appId : appIds) { final int uid = UserHandle.getUid(userId, appId); - final int previousMode = getModeInFile(uid); + final int previousMode = getModeInFile(uid, OP_SCHEDULE_EXACT_ALARM); final int expectedMode; if (previousMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) { @@ -281,6 +290,55 @@ public class AppOpsUpgradeTest { } @Test + public void resetUseFullScreenIntent() { + extractAppOpsFile(APP_OPS_VERSION_3_ASSET_PATH); + + String[] packageNames = {"p1", "package2", "pkg3", "package.4", "pkg-5", "pkg.6"}; + int[] appIds = {10267, 10181, 10198, 10199, 10200, 4213}; + int[] userIds = {0, 10, 11}; + int flag = 0; + + doReturn(userIds).when(mUserManagerInternal).getUserIds(); + + doReturn(packageNames).when(mPermissionManagerInternal).getAppOpPermissionPackages( + AppOpsManager.opToPermission(OP_USE_FULL_SCREEN_INTENT)); + + doReturn(mPermissionManager).when(mTestContext).getSystemService(PermissionManager.class); + + doReturn(flag).when(mPackageManager).getPermissionFlags( + anyString(), anyString(), isA(UserHandle.class)); + + doAnswer(invocation -> { + String pkg = invocation.getArgument(0); + int index = ArrayUtils.indexOf(packageNames, pkg); + if (index < 0) { + return index; + } + int userId = invocation.getArgument(2); + return UserHandle.getUid(userId, appIds[index]); + }).when(mPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt()); + + AppOpsCheckingServiceImpl testService = new AppOpsCheckingServiceImpl(sAppOpsFile, mLock, + mHandler, mTestContext, mSwitchedOps); + testService.readState(); + + synchronized (testService) { + testService.resetUseFullScreenIntentLocked(); + } + + for (int userId : userIds) { + for (int appId : appIds) { + final int uid = UserHandle.getUid(userId, appId); + final int expectedMode = AppOpsManager.opToDefaultMode(OP_USE_FULL_SCREEN_INTENT); + synchronized (testService) { + int mode = testService.getUidMode(uid, OP_USE_FULL_SCREEN_INTENT); + assertEquals(expectedMode, mode); + } + } + } + } + + @Test public void upgradeFromNoFile() { assertFalse(sAppOpsFile.exists()); @@ -290,12 +348,14 @@ public class AppOpsUpgradeTest { doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + doNothing().when(testService).resetUseFullScreenIntentLocked(); // trigger upgrade testService.systemReady(); verify(testService, never()).upgradeRunAnyInBackgroundLocked(); verify(testService, never()).upgradeScheduleExactAlarmLocked(); + verify(testService, never()).resetUseFullScreenIntentLocked(); testService.writeState(); @@ -319,12 +379,14 @@ public class AppOpsUpgradeTest { doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + doNothing().when(testService).resetUseFullScreenIntentLocked(); // trigger upgrade testService.systemReady(); verify(testService).upgradeRunAnyInBackgroundLocked(); verify(testService).upgradeScheduleExactAlarmLocked(); + verify(testService).resetUseFullScreenIntentLocked(); testService.writeState(); assertTrue(parser.parse()); @@ -344,12 +406,40 @@ public class AppOpsUpgradeTest { doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + doNothing().when(testService).resetUseFullScreenIntentLocked(); // trigger upgrade testService.systemReady(); verify(testService, never()).upgradeRunAnyInBackgroundLocked(); verify(testService).upgradeScheduleExactAlarmLocked(); + verify(testService).resetUseFullScreenIntentLocked(); + + testService.writeState(); + assertTrue(parser.parse()); + assertEquals(AppOpsCheckingServiceImpl.CURRENT_VERSION, parser.mVersion); + } + + @Test + public void resetFromVersion3() { + extractAppOpsFile(APP_OPS_VERSION_3_ASSET_PATH); + AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile); + assertTrue(parser.parse()); + assertEquals(3, parser.mVersion); + + AppOpsCheckingServiceImpl testService = spy(new AppOpsCheckingServiceImpl(sAppOpsFile, + mLock, mHandler, mTestContext, mSwitchedOps)); + testService.readState(); + + doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); + doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + doNothing().when(testService).resetUseFullScreenIntentLocked(); + + testService.systemReady(); + + verify(testService, never()).upgradeRunAnyInBackgroundLocked(); + verify(testService, never()).upgradeScheduleExactAlarmLocked(); + verify(testService).resetUseFullScreenIntentLocked(); testService.writeState(); assertTrue(parser.parse()); diff --git a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceMockingTest.java new file mode 100644 index 000000000000..a8853071abe8 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceMockingTest.java @@ -0,0 +1,96 @@ +/* + * 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.server.companion.virtual; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Looper; +import android.platform.test.annotations.Presubmit; +import android.testing.TestableContext; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class VirtualDeviceManagerServiceMockingTest { + private static final int UID_1 = 0; + private static final int DEVICE_ID_1 = 42; + private static final int DEVICE_ID_2 = 43; + + @Rule + public final TestableContext mContext = new TestableContext( + InstrumentationRegistry.getContext()); + + private VirtualDeviceManagerService mVdms; + private VirtualDeviceManagerInternal mLocalService; + + @Before + public void setUp() { + mVdms = new VirtualDeviceManagerService(mContext); + mLocalService = mVdms.getLocalServiceInstance(); + } + + @Test + public void onAuthenticationPrompt_noDevices_noCrash() { + // This should not crash + mLocalService.onAuthenticationPrompt(UID_1); + } + + @Test + public void onAuthenticationPrompt_oneDevice_showToastWhereUidIsRunningIsCalled() { + VirtualDeviceImpl device = mock(VirtualDeviceImpl.class); + mVdms.addVirtualDevice(device); + + mLocalService.onAuthenticationPrompt(UID_1); + + verify(device).showToastWhereUidIsRunning(eq(UID_1), + eq(R.string.app_streaming_blocked_message_for_fingerprint_dialog), anyInt(), + any(Looper.class)); + } + + @Test + public void onAuthenticationPrompt_twoDevices_showToastWhereUidIsRunningIsCalledOnBoth() { + VirtualDeviceImpl device1 = mock(VirtualDeviceImpl.class); + VirtualDeviceImpl device2 = mock(VirtualDeviceImpl.class); + when(device1.getDeviceId()).thenReturn(DEVICE_ID_1); + when(device2.getDeviceId()).thenReturn(DEVICE_ID_2); + mVdms.addVirtualDevice(device1); + mVdms.addVirtualDevice(device2); + + mLocalService.onAuthenticationPrompt(UID_1); + + verify(device1).showToastWhereUidIsRunning(eq(UID_1), + eq(R.string.app_streaming_blocked_message_for_fingerprint_dialog), anyInt(), + any(Looper.class)); + verify(device2).showToastWhereUidIsRunning(eq(UID_1), + eq(R.string.app_streaming_blocked_message_for_fingerprint_dialog), anyInt(), + any(Looper.class)); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index 9cd22dd292a5..c5ff8cc7b0d6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -402,13 +402,15 @@ public class JobSchedulerServiceTest { JobStatus rescheduledJob = mService.getRescheduleJobForFailureLocked(originalJob, JobParameters.STOP_REASON_DEVICE_STATE, JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL); - assertEquals(nowElapsed + initialBackoffMs, rescheduledJob.getEarliestRunTime()); + assertEquals(JobStatus.NO_EARLIEST_RUNTIME, rescheduledJob.getEarliestRunTime()); assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); // failure = 0, systemStop = 2 rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, JobParameters.STOP_REASON_DEVICE_STATE, JobParameters.INTERNAL_STOP_REASON_PREEMPT); + assertEquals(JobStatus.NO_EARLIEST_RUNTIME, rescheduledJob.getEarliestRunTime()); + assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); // failure = 0, systemStop = 3 rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, JobParameters.STOP_REASON_CONSTRAINT_CHARGING, @@ -438,6 +440,13 @@ public class JobSchedulerServiceTest { JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); assertEquals(nowElapsed + 4 * initialBackoffMs, rescheduledJob.getEarliestRunTime()); assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); + + // failure = 3, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO + rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.STOP_REASON_UNDEFINED, + JobParameters.INTERNAL_STOP_REASON_ANR); + assertEquals(nowElapsed + 5 * initialBackoffMs, rescheduledJob.getEarliestRunTime()); + assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); } /** diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index c040b1928ce4..05780ebe6c4b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -582,6 +582,49 @@ public class JobStatusTest { } @Test + public void testModifyingInternalFlags() { + final JobInfo jobInfo = + new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setExpedited(true) + .build(); + JobStatus job = createJobStatus(jobInfo); + + assertEquals(0, job.getInternalFlags()); + + // Add single flag + job.addInternalFlags(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION); + assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION, job.getInternalFlags()); + + // Add multiple flags + job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, + job.getInternalFlags()); + + // Add flag that's already set + job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, + job.getInternalFlags()); + + // Remove multiple + job.removeInternalFlags(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION + | JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + assertEquals(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, job.getInternalFlags()); + + // Remove one that isn't set + job.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + assertEquals(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ, job.getInternalFlags()); + + // Remove final flag. + job.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + assertEquals(0, job.getInternalFlags()); + } + + @Test public void testShouldTreatAsUserInitiated() { JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) .setUserInitiated(false) @@ -619,6 +662,9 @@ public class JobStatusTest { rescheduledJob = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, 0, 0, 0, 0, 0); assertFalse(rescheduledJob.shouldTreatAsUserInitiatedJob()); + + rescheduledJob.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob()); } @Test @@ -641,6 +687,9 @@ public class JobStatusTest { rescheduledJob = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, 0, 0, 0, 0, 0); assertFalse(rescheduledJob.shouldTreatAsUserInitiatedJob()); + + rescheduledJob.removeInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ); + assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob()); } /** diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index 3d0163d84929..51e521d8ffe4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -292,7 +292,8 @@ public class WallpaperManagerServiceTests { verifyLastWallpaperData(testUserId, sDefaultWallpaperComponent); verifyCurrentSystemData(testUserId); - mService.setWallpaperComponent(sImageWallpaperComponentName, FLAG_SYSTEM, testUserId); + mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(), + FLAG_SYSTEM, testUserId); verifyLastWallpaperData(testUserId, sImageWallpaperComponentName); verifyCurrentSystemData(testUserId); @@ -321,7 +322,8 @@ public class WallpaperManagerServiceTests { WallpaperManagerService.DisplayConnector connector = mService.mLastWallpaper.connection.getDisplayConnectorOrCreate(DEFAULT_DISPLAY); - mService.setWallpaperComponent(sDefaultWallpaperComponent, FLAG_SYSTEM, testUserId); + mService.setWallpaperComponent(sDefaultWallpaperComponent, sContext.getOpPackageName(), + FLAG_SYSTEM, testUserId); verify(connector.mEngine).dispatchWallpaperCommand( eq(COMMAND_REAPPLY), anyInt(), anyInt(), anyInt(), any()); @@ -465,7 +467,8 @@ public class WallpaperManagerServiceTests { public void testGetAdjustedWallpaperColorsOnDimming() throws RemoteException { final int testUserId = USER_SYSTEM; mService.switchUser(testUserId, null); - mService.setWallpaperComponent(sDefaultWallpaperComponent, FLAG_SYSTEM, testUserId); + mService.setWallpaperComponent(sDefaultWallpaperComponent, sContext.getOpPackageName(), + FLAG_SYSTEM, testUserId); WallpaperData wallpaper = mService.getCurrentWallpaperData(FLAG_SYSTEM, testUserId); // Mock a wallpaper data with color hints that support dark text and dark theme diff --git a/services/tests/servicestests/src/com/android/server/am/FgsLoggerTest.java b/services/tests/servicestests/src/com/android/server/am/FgsLoggerTest.java index 44d6dec2ef2d..f5005fdf1459 100644 --- a/services/tests/servicestests/src/com/android/server/am/FgsLoggerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/FgsLoggerTest.java @@ -26,6 +26,7 @@ import static com.android.server.am.ForegroundServiceTypeLoggerModule.FGS_STATE_ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; @@ -41,7 +42,6 @@ import androidx.test.filters.SmallTest; import org.junit.Before; import org.junit.Test; -import org.mockito.AdditionalMatchers; /** * Test class for {@link ForegroundServiceTypeLoggerModule}. @@ -61,10 +61,10 @@ public class FgsLoggerTest { mFgsLogger = spy(logger); doNothing().when(mFgsLogger) .logFgsApiEvent(any(ServiceRecord.class), - anyInt(), anyInt(), any(int[].class), any(long[].class)); + anyInt(), anyInt(), anyInt(), anyLong()); doNothing().when(mFgsLogger) .logFgsApiEventWithNoFgs(anyInt(), - anyInt(), any(int[].class), any(long[].class)); + anyInt(), anyInt(), anyLong()); } @Test @@ -73,10 +73,10 @@ public class FgsLoggerTest { record.foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA; mFgsLogger.logForegroundServiceStart(1, 1, record); mFgsLogger.logForegroundServiceApiEventBegin(1, 1, 1, "aPackageHasNoName"); - int[] expectedTypes = {1}; + int expectedTypes = 1; verify(mFgsLogger).logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), any(long[].class)); + eq(expectedTypes), anyLong()); reset(mFgsLogger); mFgsLogger.logForegroundServiceApiEventEnd(1, 1, 1); @@ -85,7 +85,7 @@ public class FgsLoggerTest { mFgsLogger.logForegroundServiceStop(1, record); verify(mFgsLogger).logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_END_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), any(long[].class)); + eq(expectedTypes), anyLong()); } @Test @@ -97,10 +97,10 @@ public class FgsLoggerTest { resetAndVerifyZeroInteractions(); mFgsLogger.logForegroundServiceStart(1, 1, record); - int[] expectedTypes = {1}; + int expectedTypes = 1; verify(mFgsLogger).logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), any(long[].class)); + eq(expectedTypes), anyLong()); reset(mFgsLogger); mFgsLogger.logForegroundServiceApiEventEnd(1, 1, 1); @@ -109,7 +109,7 @@ public class FgsLoggerTest { mFgsLogger.logForegroundServiceStop(1, record); verify(mFgsLogger).logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_END_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), any(long[].class)); + eq(expectedTypes), anyLong()); } @Test @@ -122,12 +122,12 @@ public class FgsLoggerTest { resetAndVerifyZeroInteractions(); - int[] expectedTypes = {1}; + int expectedTypes = 1; mFgsLogger.logForegroundServiceStart(1, 1, record); verify(mFgsLogger).logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), any(long[].class)); + eq(expectedTypes), anyLong()); reset(mFgsLogger); mFgsLogger.logForegroundServiceStop(1, record); @@ -135,7 +135,7 @@ public class FgsLoggerTest { mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1); verify(mFgsLogger).logFgsApiEventWithNoFgs(eq(1), eq(FGS_API_END_WITHOUT_FGS), - AdditionalMatchers.aryEq(expectedTypes), any(long[].class)); + eq(expectedTypes), anyLong()); } @Test @@ -176,15 +176,15 @@ public class FgsLoggerTest { // now we should see exactly one call logged // and this should be the very first call // we also try to verify the time as being the very first call - int[] expectedTypes = {1}; - long[] expectedTimestamp = {timeStamp}; + int expectedTypes = 1; + long expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); reset(mFgsLogger); // now we do multiple stops // only the last one should be logged @@ -201,11 +201,11 @@ public class FgsLoggerTest { mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1); timeStamp = mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1); - expectedTimestamp[0] = timeStamp; + expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEventWithNoFgs(eq(1), eq(FGS_API_END_WITHOUT_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); } @Test @@ -239,15 +239,15 @@ public class FgsLoggerTest { // now we should see exactly one call logged // and this should be the very first call // we also try to verify the time as being the very first call - int[] expectedTypes = {1}; - long[] expectedTimestamp = {timeStamp}; + int expectedTypes = 1; + long expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); reset(mFgsLogger); // now we do multiple stops // only the last one should be logged @@ -269,11 +269,11 @@ public class FgsLoggerTest { 1, 1); mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1); mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1); - expectedTimestamp[0] = timeStamp; + expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEventWithNoFgs(eq(1), eq(FGS_API_END_WITHOUT_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); } @Test @@ -304,15 +304,15 @@ public class FgsLoggerTest { // now we should see exactly one call logged // and this should be the very first call // we also try to verify the time as being the very first call - int[] expectedTypes = {1}; - long[] expectedTimestamp = {timeStamp}; + int expectedTypes = 1; + long expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); reset(mFgsLogger); mFgsLogger.logForegroundServiceApiEventBegin(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1, "aPackageHasNoName"); @@ -338,11 +338,11 @@ public class FgsLoggerTest { 1, 1); mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1); mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1); - expectedTimestamp[0] = timeStamp; + expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEventWithNoFgs(eq(1), eq(FGS_API_END_WITHOUT_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); } @Test @@ -375,15 +375,15 @@ public class FgsLoggerTest { // now we should see exactly one call logged // and this should be the very first call // we also try to verify the time as being the very first call - int[] expectedTypes = {1}; - long[] expectedTimestamp = {timeStamp}; + int expectedTypes = 1; + long expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); reset(mFgsLogger); mFgsLogger.logForegroundServiceApiEventBegin(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1, "aPackageHasNoName"); @@ -410,16 +410,16 @@ public class FgsLoggerTest { timeStamp = mFgsLogger.logForegroundServiceApiEventEnd(1, 1, 1); mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1); mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1); - expectedTimestamp[0] = timeStamp; + expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEventWithNoFgs(eq(1), eq(FGS_API_END_WITHOUT_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); } @Test public void testMultipleUid() throws InterruptedException { - int[] expectedTypes = {1}; + int expectedTypes = 1; ServiceRecord record = ServiceRecord.newEmptyInstanceForTest(null); record.foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA; ActivityManagerService ams = mock(ActivityManagerService.class); @@ -477,7 +477,7 @@ public class FgsLoggerTest { .logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), any(long[].class)); + eq(expectedTypes), anyLong()); reset(mFgsLogger); mFgsLogger.logForegroundServiceApiEventBegin(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1, "aPackageHasNoName"); @@ -511,16 +511,16 @@ public class FgsLoggerTest { mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 2, 1); timeStamp2 = mFgsLogger.logForegroundServiceApiEventEnd(FOREGROUND_SERVICE_API_TYPE_CAMERA, 2, 1); - long[] expectedTimestamp = {timeStamp}; - long[] expectedTimestamp2 = {timeStamp2}; + long expectedTimestamp = timeStamp; + long expectedTimestamp2 = timeStamp2; verify(mFgsLogger, times(1)) .logFgsApiEventWithNoFgs(eq(1), eq(FGS_API_END_WITHOUT_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); verify(mFgsLogger, times(1)) .logFgsApiEventWithNoFgs(eq(2), eq(FGS_API_END_WITHOUT_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp2)); + eq(expectedTypes), + eq(expectedTimestamp2)); } @Test @@ -551,15 +551,15 @@ public class FgsLoggerTest { // now we should see exactly one call logged // and this should be the very first call // we also try to verify the time as being the very first call - int[] expectedTypes = {1}; - long[] expectedTimestamp = {timeStamp}; + int expectedTypes = 1; + long expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); reset(mFgsLogger); mFgsLogger.logForegroundServiceApiEventBegin(FOREGROUND_SERVICE_API_TYPE_CAMERA, 1, 1, "aPackageHasNoName"); @@ -590,13 +590,13 @@ public class FgsLoggerTest { // now we do multiple stops // only the last one should be logged mFgsLogger.logForegroundServiceStop(1, record); - expectedTimestamp[0] = timeStamp; + expectedTimestamp = timeStamp; verify(mFgsLogger, times(1)) .logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_END_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); } @Test @@ -610,16 +610,25 @@ public class FgsLoggerTest { long timestamp2 = mFgsLogger.logForegroundServiceApiEventBegin( FOREGROUND_SERVICE_API_TYPE_MICROPHONE, 1, 1, "aPackageHasNoName"); - int[] expectedTypes = {1, FOREGROUND_SERVICE_API_TYPE_MICROPHONE}; - long[] expectedTimestamp = {timestamp1, timestamp2}; + int expectedTypes = 1; + int expectedType2 = FOREGROUND_SERVICE_API_TYPE_MICROPHONE; + long expectedTimestamp = timestamp1; + long expectedTimestamp2 = timestamp2; mFgsLogger.logForegroundServiceStart(1, 1, record); verify(mFgsLogger, times(1)) .logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_BEGIN_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); + + verify(mFgsLogger, times(1)) + .logFgsApiEvent(any(ServiceRecord.class), + eq(FGS_STATE_CHANGED_API_CALL), + eq(FGS_API_BEGIN_WITH_FGS), + eq(expectedType2), + eq(expectedTimestamp2)); reset(mFgsLogger); resetAndVerifyZeroInteractions(); @@ -632,28 +641,34 @@ public class FgsLoggerTest { mFgsLogger.logForegroundServiceStop(1, record); - expectedTimestamp[0] = timestamp1; - expectedTimestamp[1] = timestamp2; + expectedTimestamp = timestamp1; + expectedTimestamp2 = timestamp2; verify(mFgsLogger, times(1)) .logFgsApiEvent(any(ServiceRecord.class), eq(FGS_STATE_CHANGED_API_CALL), eq(FGS_API_END_WITH_FGS), - AdditionalMatchers.aryEq(expectedTypes), - AdditionalMatchers.aryEq(expectedTimestamp)); + eq(expectedTypes), + eq(expectedTimestamp)); + verify(mFgsLogger, times(1)) + .logFgsApiEvent(any(ServiceRecord.class), + eq(FGS_STATE_CHANGED_API_CALL), + eq(FGS_API_END_WITH_FGS), + eq(expectedType2), + eq(expectedTimestamp2)); } private void resetAndVerifyZeroInteractions() { doNothing().when(mFgsLogger) .logFgsApiEvent(any(ServiceRecord.class), - anyInt(), anyInt(), any(int[].class), any(long[].class)); + anyInt(), anyInt(), anyInt(), anyLong()); doNothing().when(mFgsLogger) - .logFgsApiEventWithNoFgs(anyInt(), anyInt(), any(int[].class), any(long[].class)); + .logFgsApiEventWithNoFgs(anyInt(), anyInt(), anyInt(), anyLong()); verify(mFgsLogger, times(0)) .logFgsApiEvent(any(ServiceRecord.class), - anyInt(), anyInt(), any(int[].class), any(long[].class)); + anyInt(), anyInt(), anyInt(), anyLong()); verify(mFgsLogger, times(0)) - .logFgsApiEventWithNoFgs(anyInt(), anyInt(), any(int[].class), any(long[].class)); + .logFgsApiEventWithNoFgs(anyInt(), anyInt(), anyInt(), anyLong()); } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java index 85d159c25be2..f88afe7839c3 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java @@ -58,6 +58,8 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.server.LocalServices; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import org.junit.Before; import org.junit.Rule; @@ -102,6 +104,8 @@ public class AuthServiceTest { IFaceService mFaceService; @Mock AppOpsManager mAppOpsManager; + @Mock + private VirtualDeviceManagerInternal mVdmInternal; @Captor private ArgumentCaptor<List<FingerprintSensorPropertiesInternal>> mFingerprintPropsCaptor; @Captor @@ -115,6 +119,8 @@ public class AuthServiceTest { "1:4:15", // ID1:Iris:Strong "2:8:15", // ID2:Face:Strong }; + LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class); + LocalServices.addService(VirtualDeviceManagerInternal.class, mVdmInternal); when(mResources.getIntArray(eq(R.array.config_udfps_sensor_props))).thenReturn(new int[0]); when(mResources.getBoolean(eq(R.bool.config_is_powerbutton_fps))).thenReturn(false); @@ -152,7 +158,8 @@ public class AuthServiceTest { verify(mBiometricService, never()).registerAuthenticator( anyInt(), - any(), + anyInt(), + anyInt(), any()); } @@ -272,6 +279,47 @@ public class AuthServiceTest { } @Test + public void testAuthenticate_noVdmInternalService_noCrash() throws Exception { + LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class); + mAuthService = new AuthService(mContext, mInjector); + mAuthService.onStart(); + + final Binder token = new Binder(); + + // This should not crash + mAuthService.mImpl.authenticate( + token, + 0, /* sessionId */ + 0, /* userId */ + mReceiver, + TEST_OP_PACKAGE_NAME, + new PromptInfo()); + waitForIdle(); + } + + @Test + public void testAuthenticate_callsVirtualDeviceManagerOnAuthenticationPrompt() + throws Exception { + mAuthService = new AuthService(mContext, mInjector); + mAuthService.onStart(); + + final Binder token = new Binder(); + + mAuthService.mImpl.authenticate( + token, + 0, /* sessionId */ + 0, /* userId */ + mReceiver, + TEST_OP_PACKAGE_NAME, + new PromptInfo()); + waitForIdle(); + + ArgumentCaptor<Integer> uidCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mVdmInternal).onAuthenticationPrompt(uidCaptor.capture()); + assertEquals((int) (uidCaptor.getValue()), Binder.getCallingUid()); + } + + @Test public void testAuthenticate_throwsWhenUsingTestConfigurations() { final PromptInfo promptInfo = mock(PromptInfo.class); when(promptInfo.containsPrivateApiConfigurations()).thenReturn(false); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 26a3ae110525..154aa7d4e78e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -45,14 +45,13 @@ import android.app.trust.ITrustManager; import android.content.Context; import android.content.res.Resources; import android.hardware.biometrics.BiometricManager.Authenticators; +import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricSensorReceiver; import android.hardware.biometrics.IBiometricServiceReceiver; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.SensorProperties; -import android.hardware.face.FaceSensorProperties; -import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; @@ -490,16 +489,9 @@ public class AuthSessionTest { IBiometricAuthenticator fingerprintAuthenticator = mock(IBiometricAuthenticator.class); when(fingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); when(fingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - - final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( - id, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, type, - false /* resetLockoutRequiresHardwareAuthToken */); - mFingerprintSensorProps.add(props); - - mSensors.add(new BiometricSensor(mContext, + mSensors.add(new BiometricSensor(mContext, id, TYPE_FINGERPRINT /* modality */, - props, + Authenticators.BIOMETRIC_STRONG /* strength */, fingerprintAuthenticator) { @Override boolean confirmationAlwaysRequired(int userId) { @@ -512,6 +504,21 @@ public class AuthSessionTest { } }); + final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); + componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */)); + componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */)); + + mFingerprintSensorProps.add(new FingerprintSensorPropertiesInternal(id, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + componentInfo, + type, + false /* resetLockoutRequiresHardwareAuthToken */)); + when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); } @@ -519,13 +526,9 @@ public class AuthSessionTest { IBiometricAuthenticator authenticator) throws RemoteException { when(authenticator.isHardwareDetected(any())).thenReturn(true); when(authenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - mSensors.add(new BiometricSensor(mContext, + mSensors.add(new BiometricSensor(mContext, id, TYPE_FACE /* modality */, - new FaceSensorPropertiesInternal(id, - SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FaceSensorProperties.TYPE_UNKNOWN, - true /* supportsFace Detection */, true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), + Authenticators.BIOMETRIC_STRONG /* strength */, authenticator) { @Override boolean confirmationAlwaysRequired(int userId) { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 46fa3abe5122..520e1c84c74e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -19,7 +19,6 @@ package com.android.server.biometrics; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; -import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI; @@ -70,11 +69,7 @@ import android.hardware.biometrics.IBiometricServiceReceiver; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.display.DisplayManagerGlobal; -import android.hardware.face.FaceSensorProperties; -import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintManager; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; @@ -124,7 +119,6 @@ public class BiometricServiceTest { private static final int SENSOR_ID_FINGERPRINT = 0; private static final int SENSOR_ID_FACE = 1; - private FingerprintSensorPropertiesInternal mFingerprintProps; private BiometricService mBiometricService; @@ -207,11 +201,6 @@ public class BiometricServiceTest { }; when(mInjector.getConfiguration(any())).thenReturn(config); - - mFingerprintProps = new FingerprintSensorPropertiesInternal(SENSOR_ID_FINGERPRINT, - STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */); } @Test @@ -347,7 +336,8 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, mInjector); mBiometricService.onStart(); - mBiometricService.mImpl.registerAuthenticator(TYPE_FINGERPRINT, mFingerprintProps, + mBiometricService.mImpl.registerAuthenticator(0 /* id */, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, @@ -419,7 +409,8 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, mInjector); mBiometricService.onStart(); - mBiometricService.mImpl.registerAuthenticator(TYPE_FINGERPRINT, mFingerprintProps, + mBiometricService.mImpl.registerAuthenticator(0 /* id */, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, @@ -1351,13 +1342,9 @@ public class BiometricServiceTest { for (int i = 0; i < testCases.length; i++) { final BiometricSensor sensor = - new BiometricSensor(mContext, + new BiometricSensor(mContext, 0 /* id */, TYPE_FINGERPRINT, - new FingerprintSensorPropertiesInternal(i /* id */, - Utils.authenticatorStrengthToPropertyStrength(testCases[i][0]), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */), + testCases[i][0], mock(IBiometricAuthenticator.class)) { @Override boolean confirmationAlwaysRequired(int userId) { @@ -1385,7 +1372,8 @@ public class BiometricServiceTest { when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) .thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); - mBiometricService.mImpl.registerAuthenticator(TYPE_FINGERPRINT, mFingerprintProps, + mBiometricService.mImpl.registerAuthenticator(0 /* testId */, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); verify(mBiometricService.mBiometricStrengthController).updateStrengths(); @@ -1396,14 +1384,15 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, mInjector); mBiometricService.onStart(); + final int testId = 0; + when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) .thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); - - final int testId = SENSOR_ID_FINGERPRINT; - mBiometricService.mImpl.registerAuthenticator(TYPE_FINGERPRINT, mFingerprintProps, + mBiometricService.mImpl.registerAuthenticator(testId /* id */, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); // Downgrade the authenticator @@ -1503,9 +1492,11 @@ public class BiometricServiceTest { mBiometricService.onStart(); mBiometricService.mImpl.registerAuthenticator( - 2 /* modality */, mFingerprintProps, mFingerprintAuthenticator); + 0 /* id */, 2 /* modality */, 15 /* strength */, + mFingerprintAuthenticator); mBiometricService.mImpl.registerAuthenticator( - 2 /* modality */, mFingerprintProps, mFingerprintAuthenticator); + 0 /* id */, 2 /* modality */, 15 /* strength */, + mFingerprintAuthenticator); } @Test(expected = IllegalArgumentException.class) @@ -1515,7 +1506,9 @@ public class BiometricServiceTest { mBiometricService.onStart(); mBiometricService.mImpl.registerAuthenticator( - 2 /* modality */, mFingerprintProps, null /* authenticator */); + 0 /* id */, 2 /* modality */, + Authenticators.BIOMETRIC_STRONG /* strength */, + null /* authenticator */); } @Test @@ -1526,13 +1519,8 @@ public class BiometricServiceTest { for (String s : mInjector.getConfiguration(null)) { SensorConfig config = new SensorConfig(s); - mBiometricService.mImpl.registerAuthenticator(config.modality, - new FingerprintSensorPropertiesInternal(config.id, - Utils.authenticatorStrengthToPropertyStrength(config.strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */), - mFingerprintAuthenticator); + mBiometricService.mImpl.registerAuthenticator(config.id, config.modality, + config.strength, mFingerprintAuthenticator); } } @@ -1656,12 +1644,7 @@ public class BiometricServiceTest { when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt())) .thenReturn(LockoutTracker.LOCKOUT_NONE); - mBiometricService.mImpl.registerAuthenticator(modality, - new FingerprintSensorPropertiesInternal(SENSOR_ID_FINGERPRINT, - Utils.authenticatorStrengthToPropertyStrength(strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */), + mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FINGERPRINT, modality, strength, mFingerprintAuthenticator); } @@ -1670,13 +1653,7 @@ public class BiometricServiceTest { when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFaceAuthenticator.getLockoutModeForUser(anyInt())) .thenReturn(LockoutTracker.LOCKOUT_NONE); - mBiometricService.mImpl.registerAuthenticator(modality, - new FaceSensorPropertiesInternal(SENSOR_ID_FACE, - Utils.authenticatorStrengthToPropertyStrength(strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FaceSensorProperties.TYPE_UNKNOWN, true /* supportsFace Detection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), + mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FACE, modality, strength, mFaceAuthenticator); } } @@ -1699,27 +1676,15 @@ public class BiometricServiceTest { when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) .thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); - mBiometricService.mImpl.registerAuthenticator(modality, - new FingerprintSensorPropertiesInternal(SENSOR_ID_FINGERPRINT, - Utils.authenticatorStrengthToPropertyStrength(strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* resetLockoutRequiresHardwareAuthToken */), - mFingerprintAuthenticator); + mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FINGERPRINT, modality, + strength, mFingerprintAuthenticator); } if ((modality & BiometricAuthenticator.TYPE_FACE) != 0) { when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); - mBiometricService.mImpl.registerAuthenticator(modality, - new FaceSensorPropertiesInternal(SENSOR_ID_FACE, - Utils.authenticatorStrengthToPropertyStrength(strength), - 5 /* maxEnrollmentsPerUser */, List.of() /* componentInfo */, - FaceSensorProperties.TYPE_UNKNOWN, - true /* supportsFace Detection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), - mFaceAuthenticator); + mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FACE, modality, + strength, mFaceAuthenticator); } } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/InvalidationTrackerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/InvalidationTrackerTest.java index f7539bd27c9d..ee5ab92065ee 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/InvalidationTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/InvalidationTrackerTest.java @@ -16,9 +16,6 @@ package com.android.server.biometrics; -import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; -import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -30,13 +27,9 @@ import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricManager.Authenticators; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IInvalidationCallback; -import android.hardware.biometrics.SensorPropertiesInternal; -import android.hardware.face.FaceSensorProperties; -import android.hardware.face.FaceSensorPropertiesInternal; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; @@ -49,7 +42,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; -import java.util.List; @Presubmit @SmallTest @@ -67,54 +59,26 @@ public class InvalidationTrackerTest { public void testCallbackReceived_whenAllStrongSensorsInvalidated() throws Exception { final IBiometricAuthenticator authenticator1 = mock(IBiometricAuthenticator.class); when(authenticator1.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - final TestSensor sensor1 = new TestSensor(mContext, - BiometricAuthenticator.TYPE_FINGERPRINT, - new FingerprintSensorPropertiesInternal(0 /* id */, - STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - false /* resetLockoutRequiresHardwareAuthToken */), + final TestSensor sensor1 = new TestSensor(mContext, 0 /* id */, + BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, authenticator1); final IBiometricAuthenticator authenticator2 = mock(IBiometricAuthenticator.class); when(authenticator2.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - final TestSensor sensor2 = new TestSensor(mContext, - BiometricAuthenticator.TYPE_FINGERPRINT, - new FingerprintSensorPropertiesInternal(1 /* id */, - STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, - FingerprintSensorProperties.TYPE_REAR, - false /* resetLockoutRequiresHardwareAuthToken */), + final TestSensor sensor2 = new TestSensor(mContext, 1 /* id */, + BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, authenticator2); final IBiometricAuthenticator authenticator3 = mock(IBiometricAuthenticator.class); when(authenticator3.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - final TestSensor sensor3 = new TestSensor(mContext, - BiometricAuthenticator.TYPE_FACE, - new FaceSensorPropertiesInternal(2 /* id */, - STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, - FaceSensorProperties.TYPE_RGB, - true /* supportsFace Detection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), + final TestSensor sensor3 = new TestSensor(mContext, 2 /* id */, + BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG, authenticator3); final IBiometricAuthenticator authenticator4 = mock(IBiometricAuthenticator.class); when(authenticator4.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); - final TestSensor sensor4 = new TestSensor(mContext, - BiometricAuthenticator.TYPE_FACE, - new FaceSensorPropertiesInternal(3 /* id */, - STRENGTH_WEAK, - 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, - FaceSensorProperties.TYPE_IR, - true /* supportsFace Detection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */), + final TestSensor sensor4 = new TestSensor(mContext, 3 /* id */, + BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_WEAK, authenticator4); final ArrayList<BiometricSensor> sensors = new ArrayList<>(); @@ -149,9 +113,9 @@ public class InvalidationTrackerTest { private static class TestSensor extends BiometricSensor { - TestSensor(@NonNull Context context, int modality, @NonNull SensorPropertiesInternal props, + TestSensor(@NonNull Context context, int id, int modality, int strength, @NonNull IBiometricAuthenticator impl) { - super(context, modality, props, impl); + super(context, id, modality, strength, impl); } @Override diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java index 3c77a3593001..527bc5b0c811 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java @@ -62,18 +62,15 @@ public class BiometricSchedulerOperationTest { super(null, null, null, null, 0, null, 0, 0, mock(BiometricLogger.class), mock(BiometricContext.class)); } - - @Override - public boolean isInterruptable() { - return true; - } } @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Mock - private InterruptableMonitor<FakeHal> mClientMonitor; + private InterruptableMonitor<FakeHal> mInterruptableClientMonitor; + @Mock + private BaseClientMonitor mNonInterruptableClientMonitor; @Mock private ClientMonitorCallback mClientCallback; @Mock @@ -84,149 +81,159 @@ public class BiometricSchedulerOperationTest { ArgumentCaptor<ClientMonitorCallback> mStartedCallbackCaptor; private Handler mHandler; - private BiometricSchedulerOperation mOperation; + private BiometricSchedulerOperation mInterruptableOperation; + private BiometricSchedulerOperation mNonInterruptableOperation; private boolean mIsDebuggable; @Before public void setUp() { mHandler = new Handler(TestableLooper.get(this).getLooper()); mIsDebuggable = false; - mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback, - () -> mIsDebuggable); + mInterruptableOperation = new BiometricSchedulerOperation(mInterruptableClientMonitor, + mClientCallback, () -> mIsDebuggable); + mNonInterruptableOperation = new BiometricSchedulerOperation(mNonInterruptableClientMonitor, + mClientCallback, () -> mIsDebuggable); + + when(mInterruptableClientMonitor.isInterruptable()).thenReturn(true); + when(mNonInterruptableClientMonitor.isInterruptable()).thenReturn(false); } @Test public void testStartWithCookie() { final int cookie = 200; - when(mClientMonitor.getCookie()).thenReturn(cookie); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - assertThat(mOperation.isReadyToStart(mOnStartCallback)).isEqualTo(cookie); - assertThat(mOperation.isStarted()).isFalse(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isFalse(); - verify(mClientMonitor).waitForCookie(any()); + assertThat(mInterruptableOperation.isReadyToStart(mOnStartCallback)).isEqualTo(cookie); + assertThat(mInterruptableOperation.isStarted()).isFalse(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isFalse(); + verify(mInterruptableClientMonitor).waitForCookie(any()); - final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie); assertThat(started).isTrue(); - verify(mClientMonitor).start(mStartedCallbackCaptor.capture()); - mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor); - assertThat(mOperation.isStarted()).isTrue(); + verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture()); + mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor); + assertThat(mInterruptableOperation.isStarted()).isTrue(); } @Test public void testNoStartWithoutCookie() { final int goodCookie = 20; final int badCookie = 22; - when(mClientMonitor.getCookie()).thenReturn(goodCookie); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(goodCookie); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - assertThat(mOperation.isReadyToStart(mOnStartCallback)).isEqualTo(goodCookie); - final boolean started = mOperation.startWithCookie(mOnStartCallback, badCookie); + assertThat(mInterruptableOperation.isReadyToStart(mOnStartCallback)).isEqualTo(goodCookie); + final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, + badCookie); assertThat(started).isFalse(); - assertThat(mOperation.isStarted()).isFalse(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isFalse(); - verify(mClientMonitor).waitForCookie(any()); - verify(mClientMonitor, never()).start(any()); + assertThat(mInterruptableOperation.isStarted()).isFalse(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isFalse(); + verify(mInterruptableClientMonitor).waitForCookie(any()); + verify(mInterruptableClientMonitor, never()).start(any()); } @Test public void testSecondStartWithCookieCrashesWhenDebuggable() { final int cookie = 5; mIsDebuggable = true; - when(mClientMonitor.getCookie()).thenReturn(cookie); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie); assertThat(started).isTrue(); assertThrows(IllegalStateException.class, - () -> mOperation.startWithCookie(mOnStartCallback, cookie)); + () -> mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)); } @Test public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() { final int cookie = 5; mIsDebuggable = false; - when(mClientMonitor.getCookie()).thenReturn(cookie); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(cookie); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie); + final boolean started = mInterruptableOperation.startWithCookie(mOnStartCallback, cookie); assertThat(started).isTrue(); - final boolean startedAgain = mOperation.startWithCookie(mOnStartCallback, cookie); + final boolean startedAgain = mInterruptableOperation.startWithCookie(mOnStartCallback, + cookie); assertThat(startedAgain).isFalse(); } @Test public void startsWhenReadyAndHalAvailable() { - when(mClientMonitor.getCookie()).thenReturn(0); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(0); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); - verify(mClientMonitor).start(mStartedCallbackCaptor.capture()); - mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor); + mInterruptableOperation.start(mOnStartCallback); + verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture()); + mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor); - assertThat(mOperation.isStarted()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isFalse(); + assertThat(mInterruptableOperation.isStarted()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isFalse(); - verify(mClientCallback).onClientStarted(eq(mClientMonitor)); - verify(mOnStartCallback).onClientStarted(eq(mClientMonitor)); + verify(mClientCallback).onClientStarted(eq(mInterruptableClientMonitor)); + verify(mOnStartCallback).onClientStarted(eq(mInterruptableClientMonitor)); verify(mClientCallback, never()).onClientFinished(any(), anyBoolean()); verify(mOnStartCallback, never()).onClientFinished(any(), anyBoolean()); - mStartedCallbackCaptor.getValue().onClientFinished(mClientMonitor, true); + mStartedCallbackCaptor.getValue().onClientFinished(mInterruptableClientMonitor, + true); - assertThat(mOperation.isFinished()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - verify(mClientMonitor).destroy(); - verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(true)); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + verify(mInterruptableClientMonitor).destroy(); + verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(true)); } @Test public void startFailsWhenReadyButHalNotAvailable() { - when(mClientMonitor.getCookie()).thenReturn(0); - when(mClientMonitor.getFreshDaemon()).thenReturn(null); + when(mInterruptableClientMonitor.getCookie()).thenReturn(0); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(null); - mOperation.start(mOnStartCallback); - verify(mClientMonitor, never()).start(any()); + mInterruptableOperation.start(mOnStartCallback); + verify(mInterruptableClientMonitor, never()).start(any()); - assertThat(mOperation.isStarted()).isFalse(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isTrue(); + assertThat(mInterruptableOperation.isStarted()).isFalse(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); - verify(mClientCallback, never()).onClientStarted(eq(mClientMonitor)); - verify(mOnStartCallback, never()).onClientStarted(eq(mClientMonitor)); - verify(mClientCallback).onClientFinished(eq(mClientMonitor), eq(false)); - verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(false)); + verify(mClientCallback, never()).onClientStarted(eq(mInterruptableClientMonitor)); + verify(mOnStartCallback, never()).onClientStarted(eq(mInterruptableClientMonitor)); + verify(mClientCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false)); + verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false)); } @Test public void secondStartCrashesWhenDebuggable() { mIsDebuggable = true; - when(mClientMonitor.getCookie()).thenReturn(0); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(0); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mOperation.start(mOnStartCallback); + final boolean started = mInterruptableOperation.start(mOnStartCallback); assertThat(started).isTrue(); - assertThrows(IllegalStateException.class, () -> mOperation.start(mOnStartCallback)); + assertThrows(IllegalStateException.class, () -> mInterruptableOperation.start( + mOnStartCallback)); } @Test public void secondStartFailsNicelyWhenNotDebuggable() { mIsDebuggable = false; - when(mClientMonitor.getCookie()).thenReturn(0); - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getCookie()).thenReturn(0); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - final boolean started = mOperation.start(mOnStartCallback); + final boolean started = mInterruptableOperation.start(mOnStartCallback); assertThat(started).isTrue(); - final boolean startedAgain = mOperation.start(mOnStartCallback); + final boolean startedAgain = mInterruptableOperation.start(mOnStartCallback); assertThat(startedAgain).isFalse(); } @@ -234,77 +241,78 @@ public class BiometricSchedulerOperationTest { public void doesNotStartWithCookie() { // This class only throws exceptions when debuggable. mIsDebuggable = true; - when(mClientMonitor.getCookie()).thenReturn(9); + when(mInterruptableClientMonitor.getCookie()).thenReturn(9); assertThrows(IllegalStateException.class, - () -> mOperation.start(mock(ClientMonitorCallback.class))); + () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class))); } @Test public void cannotRestart() { // This class only throws exceptions when debuggable. mIsDebuggable = true; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); + mInterruptableOperation.start(mOnStartCallback); assertThrows(IllegalStateException.class, - () -> mOperation.start(mock(ClientMonitorCallback.class))); + () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class))); } @Test public void abortsNotRunning() { // This class only throws exceptions when debuggable. mIsDebuggable = true; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.abort(); + mInterruptableOperation.abort(); - assertThat(mOperation.isFinished()).isTrue(); - verify(mClientMonitor).unableToStart(); - verify(mClientMonitor).destroy(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + verify(mInterruptableClientMonitor).unableToStart(); + verify(mInterruptableClientMonitor).destroy(); assertThrows(IllegalStateException.class, - () -> mOperation.start(mock(ClientMonitorCallback.class))); + () -> mInterruptableOperation.start(mock(ClientMonitorCallback.class))); } @Test public void abortCrashesWhenDebuggableIfOperationIsRunning() { mIsDebuggable = true; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); + mInterruptableOperation.start(mOnStartCallback); - assertThrows(IllegalStateException.class, () -> mOperation.abort()); + assertThrows(IllegalStateException.class, () -> mInterruptableOperation.abort()); } @Test public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() { mIsDebuggable = false; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); + mInterruptableOperation.start(mOnStartCallback); - mOperation.abort(); + mInterruptableOperation.abort(); } @Test public void cancel() { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); - mOperation.start(mOnStartCallback); - verify(mClientMonitor).start(mStartedCallbackCaptor.capture()); - mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor); - mOperation.cancel(mHandler, cancelCb); + mInterruptableOperation.start(mOnStartCallback); + verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture()); + mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor); + mInterruptableOperation.cancel(mHandler, cancelCb); - assertThat(mOperation.isCanceling()).isTrue(); - verify(mClientMonitor).cancel(); - verify(mClientMonitor, never()).destroy(); + assertThat(mInterruptableOperation.isCanceling()).isTrue(); + verify(mInterruptableClientMonitor).cancel(); + verify(mInterruptableClientMonitor, never()).destroy(); - mStartedCallbackCaptor.getValue().onClientFinished(mClientMonitor, true); + mStartedCallbackCaptor.getValue().onClientFinished(mInterruptableClientMonitor, + true); - assertThat(mOperation.isFinished()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - verify(mClientMonitor).destroy(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + verify(mInterruptableClientMonitor).destroy(); // should be unused since the operation was started verify(cancelCb, never()).onClientStarted(any()); @@ -313,61 +321,84 @@ public class BiometricSchedulerOperationTest { @Test public void cancelWithoutStarting() { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); - mOperation.cancel(mHandler, cancelCb); + mInterruptableOperation.cancel(mHandler, cancelCb); - assertThat(mOperation.isCanceling()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isTrue(); ArgumentCaptor<ClientMonitorCallback> cbCaptor = ArgumentCaptor.forClass(ClientMonitorCallback.class); - verify(mClientMonitor).cancelWithoutStarting(cbCaptor.capture()); + verify(mInterruptableClientMonitor).cancelWithoutStarting(cbCaptor.capture()); - cbCaptor.getValue().onClientFinished(mClientMonitor, true); - verify(cancelCb).onClientFinished(eq(mClientMonitor), eq(true)); - verify(mClientMonitor, never()).start(any()); - verify(mClientMonitor, never()).cancel(); - verify(mClientMonitor).destroy(); + cbCaptor.getValue().onClientFinished(mInterruptableClientMonitor, true); + verify(cancelCb).onClientFinished(eq(mInterruptableClientMonitor), eq(true)); + verify(mInterruptableClientMonitor, never()).start(any()); + verify(mInterruptableClientMonitor, never()).cancel(); + verify(mInterruptableClientMonitor).destroy(); } @Test public void cancelCrashesWhenDebuggableIfOperationIsFinished() { mIsDebuggable = true; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.abort(); - assertThat(mOperation.isFinished()).isTrue(); + mInterruptableOperation.abort(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); - assertThrows(IllegalStateException.class, () -> mOperation.cancel(mHandler, cancelCb)); + assertThrows(IllegalStateException.class, () -> mInterruptableOperation.cancel(mHandler, + cancelCb)); } @Test public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() { mIsDebuggable = false; - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.abort(); - assertThat(mOperation.isFinished()).isTrue(); + mInterruptableOperation.abort(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class); - mOperation.cancel(mHandler, cancelCb); + mInterruptableOperation.cancel(mHandler, cancelCb); } @Test - public void markCanceling() { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); - - mOperation.markCanceling(); - - assertThat(mOperation.isMarkedCanceling()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - assertThat(mOperation.isFinished()).isFalse(); - verify(mClientMonitor, never()).start(any()); - verify(mClientMonitor, never()).cancel(); - verify(mClientMonitor, never()).cancelWithoutStarting(any()); - verify(mClientMonitor, never()).unableToStart(); - verify(mClientMonitor, never()).destroy(); + public void markCanceling_interruptableClient() { + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); + + mInterruptableOperation.markCanceling(); + + assertThat(mInterruptableOperation.isMarkedCanceling()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + assertThat(mInterruptableOperation.isFinished()).isFalse(); + verify(mInterruptableClientMonitor, never()).start(any()); + verify(mInterruptableClientMonitor, never()).cancel(); + verify(mInterruptableClientMonitor, never()).cancelWithoutStarting(any()); + verify(mInterruptableClientMonitor, never()).unableToStart(); + verify(mInterruptableClientMonitor, never()).destroy(); + } + + @Test + public void markCanceling_nonInterruptableClient() { + mNonInterruptableOperation.markCanceling(); + + assertThat(mNonInterruptableOperation.isMarkedCanceling()).isFalse(); + assertThat(mNonInterruptableOperation.isCanceling()).isFalse(); + assertThat(mNonInterruptableOperation.isFinished()).isFalse(); + verify(mNonInterruptableClientMonitor, never()).start(any()); + verify(mNonInterruptableClientMonitor, never()).cancel(); + verify(mNonInterruptableClientMonitor, never()).cancelWithoutStarting(any()); + verify(mNonInterruptableClientMonitor, never()).destroy(); + } + + @Test + public void markCancelingForWatchdog() { + mNonInterruptableOperation.markCancelingForWatchdog(); + mInterruptableOperation.markCancelingForWatchdog(); + + assertThat(mInterruptableOperation.isMarkedCanceling()).isTrue(); + assertThat(mNonInterruptableOperation.isMarkedCanceling()).isTrue(); } @Test @@ -381,26 +412,26 @@ public class BiometricSchedulerOperationTest { } private void markCancellingAndStart(Integer withCookie) { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); if (withCookie != null) { - when(mClientMonitor.getCookie()).thenReturn(withCookie); + when(mInterruptableClientMonitor.getCookie()).thenReturn(withCookie); } - mOperation.markCanceling(); + mInterruptableOperation.markCanceling(); final ClientMonitorCallback cb = mock(ClientMonitorCallback.class); if (withCookie != null) { - mOperation.startWithCookie(cb, withCookie); + mInterruptableOperation.startWithCookie(cb, withCookie); } else { - mOperation.start(cb); + mInterruptableOperation.start(cb); } - assertThat(mOperation.isFinished()).isTrue(); - verify(cb).onClientFinished(eq(mClientMonitor), eq(true)); - verify(mClientMonitor, never()).start(any()); - verify(mClientMonitor, never()).cancel(); - verify(mClientMonitor, never()).cancelWithoutStarting(any()); - verify(mClientMonitor, never()).unableToStart(); - verify(mClientMonitor).destroy(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + verify(cb).onClientFinished(eq(mInterruptableClientMonitor), eq(true)); + verify(mInterruptableClientMonitor, never()).start(any()); + verify(mInterruptableClientMonitor, never()).cancel(); + verify(mInterruptableClientMonitor, never()).cancelWithoutStarting(any()); + verify(mInterruptableClientMonitor, never()).unableToStart(); + verify(mInterruptableClientMonitor).destroy(); } @Test @@ -414,23 +445,23 @@ public class BiometricSchedulerOperationTest { } private void cancelWatchdog(boolean start) { - when(mClientMonitor.getFreshDaemon()).thenReturn(mHal); + when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); - mOperation.start(mOnStartCallback); + mInterruptableOperation.start(mOnStartCallback); if (start) { - verify(mClientMonitor).start(mStartedCallbackCaptor.capture()); - mStartedCallbackCaptor.getValue().onClientStarted(mClientMonitor); + verify(mInterruptableClientMonitor).start(mStartedCallbackCaptor.capture()); + mStartedCallbackCaptor.getValue().onClientStarted(mInterruptableClientMonitor); } - mOperation.cancel(mHandler, mock(ClientMonitorCallback.class)); + mInterruptableOperation.cancel(mHandler, mock(ClientMonitorCallback.class)); - assertThat(mOperation.isCanceling()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isTrue(); // omit call to onClientFinished and trigger watchdog - mOperation.mCancelWatchdog.run(); + mInterruptableOperation.mCancelWatchdog.run(); - assertThat(mOperation.isFinished()).isTrue(); - assertThat(mOperation.isCanceling()).isFalse(); - verify(mOnStartCallback).onClientFinished(eq(mClientMonitor), eq(false)); - verify(mClientMonitor).destroy(); + assertThat(mInterruptableOperation.isFinished()).isTrue(); + assertThat(mInterruptableOperation.isCanceling()).isFalse(); + verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false)); + verify(mInterruptableClientMonitor).destroy(); } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java index d3f04dfcfa17..903ed9082481 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java @@ -17,6 +17,8 @@ package com.android.server.biometrics.sensors.face; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; @@ -68,7 +70,9 @@ public class FaceServiceRegistryTest { @Mock private ServiceProvider mProvider2; @Captor - private ArgumentCaptor<FaceSensorPropertiesInternal> mPropsCaptor; + private ArgumentCaptor<Integer> mIdCaptor; + @Captor + private ArgumentCaptor<Integer> mStrengthCaptor; private FaceSensorPropertiesInternal mProvider1Props; private FaceSensorPropertiesInternal mProvider2Props; @@ -78,13 +82,13 @@ public class FaceServiceRegistryTest { public void setup() { mProvider1Props = new FaceSensorPropertiesInternal(SENSOR_ID_1, STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FaceSensorProperties.TYPE_RGB, + List.of(), FaceSensorProperties.TYPE_RGB, true /* supportsFace Detection */, true /* supportsSelfIllumination */, false /* resetLockoutRequiresHardwareAuthToken */); mProvider2Props = new FaceSensorPropertiesInternal(SENSOR_ID_2, STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FaceSensorProperties.TYPE_IR, + List.of(), FaceSensorProperties.TYPE_IR, true /* supportsFace Detection */, true /* supportsSelfIllumination */, false /* resetLockoutRequiresHardwareAuthToken */); @@ -103,9 +107,10 @@ public class FaceServiceRegistryTest { assertThat(mRegistry.getProviders()).containsExactly(mProvider1, mProvider2); assertThat(mRegistry.getAllProperties()).containsExactly(mProvider1Props, mProvider2Props); verify(mBiometricService, times(2)).registerAuthenticator( - eq(TYPE_FACE), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()) - .containsExactly(mProvider1Props, mProvider2Props); + mIdCaptor.capture(), eq(TYPE_FACE), mStrengthCaptor.capture(), any()); + assertThat(mIdCaptor.getAllValues()).containsExactly(SENSOR_ID_1, SENSOR_ID_2); + assertThat(mStrengthCaptor.getAllValues()) + .containsExactly(BIOMETRIC_WEAK, BIOMETRIC_STRONG); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java index 6e09069e654b..13c3f64fec93 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java @@ -17,6 +17,8 @@ package com.android.server.biometrics.sensors.fingerprint; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; @@ -68,7 +70,9 @@ public class FingerprintServiceRegistryTest { @Mock private ServiceProvider mProvider2; @Captor - private ArgumentCaptor<FingerprintSensorPropertiesInternal> mPropsCaptor; + private ArgumentCaptor<Integer> mIdCaptor; + @Captor + private ArgumentCaptor<Integer> mStrengthCaptor; private FingerprintSensorPropertiesInternal mProvider1Props; private FingerprintSensorPropertiesInternal mProvider2Props; @@ -78,11 +82,11 @@ public class FingerprintServiceRegistryTest { public void setup() { mProvider1Props = new FingerprintSensorPropertiesInternal(SENSOR_ID_1, STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, + List.of(), FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, false /* resetLockoutRequiresHardwareAuthToken */); mProvider2Props = new FingerprintSensorPropertiesInternal(SENSOR_ID_2, STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, FingerprintSensorProperties.TYPE_UNKNOWN, + List.of(), FingerprintSensorProperties.TYPE_UNKNOWN, false /* resetLockoutRequiresHardwareAuthToken */); when(mProvider1.getSensorProperties()).thenReturn(List.of(mProvider1Props)); @@ -99,9 +103,10 @@ public class FingerprintServiceRegistryTest { assertThat(mRegistry.getProviders()).containsExactly(mProvider1, mProvider2); assertThat(mRegistry.getAllProperties()).containsExactly(mProvider1Props, mProvider2Props); verify(mBiometricService, times(2)).registerAuthenticator( - eq(TYPE_FINGERPRINT), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()) - .containsExactly(mProvider1Props, mProvider2Props); + mIdCaptor.capture(), eq(TYPE_FINGERPRINT), mStrengthCaptor.capture(), any()); + assertThat(mIdCaptor.getAllValues()).containsExactly(SENSOR_ID_1, SENSOR_ID_2); + assertThat(mStrengthCaptor.getAllValues()) + .containsExactly(BIOMETRIC_WEAK, BIOMETRIC_STRONG); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java index 1089c07e6787..2aa62d96168d 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java @@ -27,6 +27,8 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFP import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertEquals; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -44,6 +46,7 @@ import android.hardware.fingerprint.FingerprintAuthenticateOptions; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; import android.hardware.fingerprint.IFingerprintServiceReceiver; +import android.os.Binder; import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.provider.Settings; @@ -54,8 +57,10 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; +import com.android.server.LocalServices; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import org.junit.Before; import org.junit.Rule; @@ -103,6 +108,8 @@ public class FingerprintServiceTest { private IFingerprintServiceReceiver mServiceReceiver; @Mock private IBinder mToken; + @Mock + private VirtualDeviceManagerInternal mVdmInternal; @Captor private ArgumentCaptor<FingerprintAuthenticateOptions> mAuthenticateOptionsCaptor; @@ -110,21 +117,22 @@ public class FingerprintServiceTest { private final FingerprintSensorPropertiesInternal mSensorPropsDefault = new FingerprintSensorPropertiesInternal(ID_DEFAULT, STRENGTH_STRONG, 2 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, + List.of(), TYPE_REAR, false /* resetLockoutRequiresHardwareAuthToken */); private final FingerprintSensorPropertiesInternal mSensorPropsVirtual = new FingerprintSensorPropertiesInternal(ID_VIRTUAL, STRENGTH_STRONG, 2 /* maxEnrollmentsPerUser */, - List.of() /* componentInfo */, + List.of(), TYPE_UDFPS_OPTICAL, false /* resetLockoutRequiresHardwareAuthToken */); - @Captor - private ArgumentCaptor<FingerprintSensorPropertiesInternal> mPropsCaptor; private FingerprintService mService; @Before public void setup() throws Exception { + LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class); + LocalServices.addService(VirtualDeviceManagerInternal.class, mVdmInternal); + when(mFingerprintDefault.getSensorProperties()).thenReturn(List.of(mSensorPropsDefault)); when(mFingerprintVirtual.getSensorProperties()).thenReturn(List.of(mSensorPropsVirtual)); when(mFingerprintDefault.containsSensor(anyInt())) @@ -168,8 +176,7 @@ public class FingerprintServiceTest { mService.mServiceWrapper.registerAuthenticators(HIDL_AUTHENTICATORS); waitForRegistration(); - verify(mIBiometricService).registerAuthenticator(anyInt(), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()).containsExactly(mSensorPropsDefault); + verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT), anyInt(), anyInt(), any()); } @Test @@ -181,8 +188,7 @@ public class FingerprintServiceTest { mService.mServiceWrapper.registerAuthenticators(HIDL_AUTHENTICATORS); waitForRegistration(); - verify(mIBiometricService).registerAuthenticator(anyInt(), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()).containsExactly(mSensorPropsVirtual); + verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any()); } @Test @@ -192,8 +198,7 @@ public class FingerprintServiceTest { mService.mServiceWrapper.registerAuthenticators(HIDL_AUTHENTICATORS); waitForRegistration(); - verify(mIBiometricService).registerAuthenticator(anyInt(), mPropsCaptor.capture(), any()); - assertThat(mPropsCaptor.getAllValues()).containsExactly(mSensorPropsVirtual); + verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any()); } private void waitForRegistration() throws Exception { @@ -225,6 +230,36 @@ public class FingerprintServiceTest { verifyNoAuthenticate(mFingerprintVirtual); } + @Test + public void testAuthenticate_noVdmInternalService_noCrash() throws Exception { + initServiceWithAndWait(NAME_DEFAULT, NAME_VIRTUAL); + LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class); + + final long operationId = 2; + + // This should not crash + mService.mServiceWrapper.authenticate(mToken, operationId, mServiceReceiver, + new FingerprintAuthenticateOptions.Builder() + .setSensorId(SENSOR_ID_ANY) + .build()); + } + + @Test + public void testAuthenticate_callsVirtualDeviceManagerOnAuthenticationPrompt() + throws Exception { + initServiceWithAndWait(NAME_DEFAULT, NAME_VIRTUAL); + + final long operationId = 2; + mService.mServiceWrapper.authenticate(mToken, operationId, mServiceReceiver, + new FingerprintAuthenticateOptions.Builder() + .setSensorId(SENSOR_ID_ANY) + .build()); + + ArgumentCaptor<Integer> uidCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mVdmInternal).onAuthenticationPrompt(uidCaptor.capture()); + assertEquals((int) (uidCaptor.getValue()), Binder.getCallingUid()); + } + private FingerprintAuthenticateOptions verifyAuthenticateWithNewRequestId( FingerprintProvider provider, long operationId) { diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java index ccddb2fd95bc..1475537dee14 100644 --- a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java @@ -75,7 +75,7 @@ public class CallMetadataSyncConnectionServiceTest { callMetadataSyncData.addCall(call); mSyncConnectionService.mCrossDeviceSyncControllerCallback.processContextSyncMessage( /* associationId= */ 0, callMetadataSyncData); - verify(mMockTelecomManager, times(1)).addNewIncomingCall(any(), any()); + verify(mMockTelecomManager, times(0)).addNewIncomingCall(any(), any()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceCallTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceCallTest.java index 5a0646c0e0e5..6a939ab34768 100644 --- a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceCallTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceCallTest.java @@ -23,6 +23,7 @@ import android.platform.test.annotations.Presubmit; import android.telecom.Call; import android.telecom.ParcelableCall; import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; import android.testing.AndroidTestingRunner; import androidx.test.InstrumentationRegistry; @@ -45,7 +46,7 @@ public class CrossDeviceCallTest { @Test public void updateCallDetails_uninitialized() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); assertWithMessage("Wrong status").that(crossDeviceCall.getStatus()) .isEqualTo(android.companion.Telecom.Call.UNKNOWN_STATUS); @@ -55,7 +56,7 @@ public class CrossDeviceCallTest { @Test public void updateCallDetails_ringing() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.updateCallDetails(createCallDetails(Call.STATE_RINGING, Call.Details.CAPABILITY_HOLD | Call.Details.CAPABILITY_MUTE)); @@ -70,7 +71,7 @@ public class CrossDeviceCallTest { @Test public void updateCallDetails_ongoing() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.updateCallDetails(createCallDetails(Call.STATE_ACTIVE, Call.Details.CAPABILITY_HOLD | Call.Details.CAPABILITY_MUTE)); @@ -85,7 +86,7 @@ public class CrossDeviceCallTest { @Test public void updateCallDetails_holding() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.updateCallDetails(createCallDetails(Call.STATE_HOLDING, Call.Details.CAPABILITY_HOLD | Call.Details.CAPABILITY_MUTE)); @@ -99,7 +100,7 @@ public class CrossDeviceCallTest { @Test public void updateCallDetails_cannotHold() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.updateCallDetails( createCallDetails(Call.STATE_ACTIVE, Call.Details.CAPABILITY_MUTE)); @@ -113,7 +114,7 @@ public class CrossDeviceCallTest { @Test public void updateCallDetails_cannotMute() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.updateCallDetails( createCallDetails(Call.STATE_ACTIVE, Call.Details.CAPABILITY_HOLD)); @@ -127,7 +128,7 @@ public class CrossDeviceCallTest { @Test public void updateCallDetails_transitionRingingToOngoing() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.updateCallDetails(createCallDetails(Call.STATE_RINGING, Call.Details.CAPABILITY_HOLD | Call.Details.CAPABILITY_MUTE)); @@ -150,7 +151,7 @@ public class CrossDeviceCallTest { @Test public void updateSilencedIfRinging_ringing_silenced() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.updateCallDetails(createCallDetails(Call.STATE_RINGING, Call.Details.CAPABILITY_HOLD | Call.Details.CAPABILITY_MUTE)); @@ -165,7 +166,7 @@ public class CrossDeviceCallTest { @Test public void updateSilencedIfRinging_notRinging_notSilenced() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.updateCallDetails(createCallDetails(Call.STATE_ACTIVE, Call.Details.CAPABILITY_HOLD | Call.Details.CAPABILITY_MUTE)); @@ -179,12 +180,11 @@ public class CrossDeviceCallTest { } @Test - public void getReadableCallerId_enterpriseCall_adminBlocked_ott() { + public void getReadableCallerId_enterpriseCall_adminBlocked_hasContact() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.mIsEnterprise = true; - crossDeviceCall.mIsOtt = true; crossDeviceCall.updateCallDetails( createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0)); @@ -195,30 +195,29 @@ public class CrossDeviceCallTest { } @Test - public void getReadableCallerId_enterpriseCall_adminUnblocked_ott() { + public void getReadableCallerId_enterpriseCall_adminUnblocked_hasContact() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.mIsEnterprise = true; - crossDeviceCall.mIsOtt = true; crossDeviceCall.updateCallDetails( createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0)); final String result = crossDeviceCall.getReadableCallerId(false); assertWithMessage("Wrong caller id").that(result) - .isEqualTo(CALLER_DISPLAY_NAME); + .isEqualTo(CONTACT_DISPLAY_NAME); } @Test - public void getReadableCallerId_enterpriseCall_adminBlocked_pstn() { + public void getReadableCallerId_enterpriseCall_adminBlocked_noContact() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.mIsEnterprise = true; - crossDeviceCall.mIsOtt = false; crossDeviceCall.updateCallDetails( - createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0)); + createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0, /* hasContactName= */ + false)); final String result = crossDeviceCall.getReadableCallerId(true); @@ -227,14 +226,14 @@ public class CrossDeviceCallTest { } @Test - public void getReadableCallerId_nonEnterpriseCall_adminBlocked_ott() { + public void getReadableCallerId_nonEnterpriseCall_adminBlocked_noContact() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.mIsEnterprise = false; - crossDeviceCall.mIsOtt = true; crossDeviceCall.updateCallDetails( - createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0)); + createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0, /* hasContactName= */ + false)); final String result = crossDeviceCall.getReadableCallerId(true); @@ -243,14 +242,14 @@ public class CrossDeviceCallTest { } @Test - public void getReadableCallerId_nonEnterpriseCall_adminUnblocked_ott() { + public void getReadableCallerId_nonEnterpriseCall_adminUnblocked_noContact() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.mIsEnterprise = false; - crossDeviceCall.mIsOtt = true; crossDeviceCall.updateCallDetails( - createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0)); + createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0, /* hasContactName= */ + false)); final String result = crossDeviceCall.getReadableCallerId(false); @@ -259,12 +258,11 @@ public class CrossDeviceCallTest { } @Test - public void getReadableCallerId_nonEnterpriseCall_adminBlocked_pstn() { + public void getReadableCallerId_nonEnterpriseCall_adminBlocked_hasContact() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.mIsEnterprise = false; - crossDeviceCall.mIsOtt = false; crossDeviceCall.updateCallDetails( createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0)); @@ -275,12 +273,11 @@ public class CrossDeviceCallTest { } @Test - public void getReadableCallerId_nonEnterpriseCall_adminUnblocked_pstn() { + public void getReadableCallerId_nonEnterpriseCall_adminUnblocked_hasContact() { final CrossDeviceCall crossDeviceCall = new CrossDeviceCall( - InstrumentationRegistry.getTargetContext().getPackageManager(), + InstrumentationRegistry.getTargetContext(), mUninitializedCallDetails, /* callAudioState= */ null); crossDeviceCall.mIsEnterprise = false; - crossDeviceCall.mIsOtt = false; crossDeviceCall.updateCallDetails( createCallDetails(Call.STATE_ACTIVE, /* capabilities= */ 0)); @@ -291,10 +288,17 @@ public class CrossDeviceCallTest { } private Call.Details createCallDetails(int state, int capabilities) { + return createCallDetails(state, capabilities, /* hasContactName= */ true); + } + + private Call.Details createCallDetails(int state, int capabilities, boolean hasContactName) { final ParcelableCall.ParcelableCallBuilder parcelableCallBuilder = new ParcelableCall.ParcelableCallBuilder(); parcelableCallBuilder.setCallerDisplayName(CALLER_DISPLAY_NAME); - parcelableCallBuilder.setContactDisplayName(CONTACT_DISPLAY_NAME); + if (hasContactName) { + parcelableCallBuilder.setContactDisplayName(CONTACT_DISPLAY_NAME); + } + parcelableCallBuilder.setCallerDisplayNamePresentation(TelecomManager.PRESENTATION_ALLOWED); parcelableCallBuilder.setCapabilities(capabilities); parcelableCallBuilder.setState(state); parcelableCallBuilder.setConferenceableCallIds(Collections.emptyList()); diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java index 33e7cd2891fc..7688fcb2faba 100644 --- a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -211,4 +212,62 @@ public class CrossDeviceSyncControllerTest { verify(mMockTelecomManager, times(1)).registerPhoneAccount(any()); verify(mMockTelecomManager, times(1)).unregisterPhoneAccount(any()); } + + @Test + public void updateCalls_newCall() { + final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call(); + call.setId("123abc"); + call.setFacilitator(new CallMetadataSyncData.CallFacilitator("name", "com.android.test")); + final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData(); + callMetadataSyncData.addCall(call); + final CrossDeviceSyncController.CallManager callManager = + new CrossDeviceSyncController.CallManager(mMockContext, + new CrossDeviceSyncController.PhoneAccountManager(mMockContext)); + callManager.updateCalls(/* associationId= */ 0, callMetadataSyncData); + verify(mMockTelecomManager, times(1)).addNewIncomingCall(any(), any()); + } + + @Test + public void updateCalls_newCall_noFacilitator() { + final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call(); + call.setId("123abc"); + final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData(); + callMetadataSyncData.addCall(call); + final CrossDeviceSyncController.CallManager callManager = + new CrossDeviceSyncController.CallManager(mMockContext, + new CrossDeviceSyncController.PhoneAccountManager(mMockContext)); + callManager.updateCalls(/* associationId= */ 0, callMetadataSyncData); + verify(mMockTelecomManager, times(0)).addNewIncomingCall(any(), any()); + } + + @Test + public void updateCalls_existingCall() { + final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call(); + call.setId("123abc"); + final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData(); + callMetadataSyncData.addCall(call); + final CrossDeviceSyncController.CallManager callManager = + new CrossDeviceSyncController.CallManager(mMockContext, + new CrossDeviceSyncController.PhoneAccountManager(mMockContext)); + callManager.mCallIds.put(/* associationId= */ 0, Set.of(call.getId())); + callManager.updateCalls(/* associationId= */ 0, callMetadataSyncData); + verify(mMockTelecomManager, never()).addNewIncomingCall(any(), any()); + } + + @Test + public void updateCalls_removedCall() { + final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call(); + call.setId("123abc"); + final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData(); + callMetadataSyncData.addCall(call); + final CrossDeviceSyncController.CallManager callManager = + new CrossDeviceSyncController.CallManager(mMockContext, + new CrossDeviceSyncController.PhoneAccountManager(mMockContext)); + callManager.mCallIds.put(/* associationId= */ 0, Set.of(call.getId(), "fakeCallId")); + callManager.updateCalls(/* associationId= */ 0, callMetadataSyncData); + verify(mMockTelecomManager, never()).addNewIncomingCall(any(), any()); + assertWithMessage("Hasn't removed the id of the removed call") + .that(callManager.mCallIds) + .containsExactly(/* associationId= */ 0, Set.of(call.getId())); + } } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java index 1d6846c4c807..eb2ee35161ff 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java @@ -76,7 +76,7 @@ import javax.xml.parsers.DocumentBuilderFactory; public class PolicyVersionUpgraderTest extends DpmTestBase { // NOTE: Only change this value if the corresponding CL also adds a test to test the upgrade // to the new version. - private static final int LATEST_TESTED_VERSION = 4; + private static final int LATEST_TESTED_VERSION = 5; public static final String PERMISSIONS_TAG = "admin-can-grant-sensors-permissions"; public static final String DEVICE_OWNER_XML = "device_owner_2.xml"; private ComponentName mFakeAdmin; @@ -313,6 +313,24 @@ public class PolicyVersionUpgraderTest extends DpmTestBase { } @Test + public void testEffectiveKeepProfilesRunningSet() throws Exception { + writeVersionToXml(4); + + final int userId = UserHandle.USER_SYSTEM; + mProvider.mUsers = new int[]{userId}; + preparePoliciesFile(userId, "device_policies.xml"); + + mUpgrader.upgradePolicy(5); + + assertThat(readVersionFromXml()).isAtLeast(5); + + Document policies = readPolicies(userId); + Element keepProfilesRunning = (Element) policies.getDocumentElement() + .getElementsByTagName("keep-profiles-running").item(0); + assertThat(keepProfilesRunning.getAttribute("value")).isEqualTo("false"); + } + + @Test public void isLatestVersionTested() { assertThat(DevicePolicyManagerService.DPMS_VERSION).isEqualTo(LATEST_TESTED_VERSION); } diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java index 4d068554129f..668415041129 100644 --- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java @@ -75,6 +75,10 @@ public final class DeviceStateManagerServiceTest { private static final DeviceState UNSUPPORTED_DEVICE_STATE = new DeviceState(255, "UNSUPPORTED", 0 /* flags */); + private static final int[] SUPPORTED_DEVICE_STATE_IDENTIFIERS = + new int[]{DEFAULT_DEVICE_STATE.getIdentifier(), OTHER_DEVICE_STATE.getIdentifier(), + DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP.getIdentifier()}; + private static final int FAKE_PROCESS_ID = 100; private TestDeviceStatePolicy mPolicy; @@ -88,6 +92,11 @@ public final class DeviceStateManagerServiceTest { mProvider = new TestDeviceStateProvider(); mPolicy = new TestDeviceStatePolicy(mProvider); mSysPropSetter = new TestSystemPropertySetter(); + setupDeviceStateManagerService(); + flushHandler(); // Flush the handler to ensure the initial values are committed. + } + + private void setupDeviceStateManagerService() { mService = new DeviceStateManagerService(InstrumentationRegistry.getContext(), mPolicy, mSysPropSetter); @@ -97,8 +106,6 @@ public final class DeviceStateManagerServiceTest { when(mService.mActivityTaskManagerInternal.getTopApp()) .thenReturn(mWindowProcessController); when(mWindowProcessController.getPid()).thenReturn(FAKE_PROCESS_ID); - - flushHandler(); // Flush the handler to ensure the initial values are committed. } private void flushHandler() { @@ -264,14 +271,26 @@ public final class DeviceStateManagerServiceTest { public void getDeviceStateInfo() throws RemoteException { DeviceStateInfo info = mService.getBinderService().getDeviceStateInfo(); assertNotNull(info); - assertArrayEquals(info.supportedStates, - new int[] { DEFAULT_DEVICE_STATE.getIdentifier(), - OTHER_DEVICE_STATE.getIdentifier(), - DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP.getIdentifier()}); + assertArrayEquals(info.supportedStates, SUPPORTED_DEVICE_STATE_IDENTIFIERS); assertEquals(info.baseState, DEFAULT_DEVICE_STATE.getIdentifier()); assertEquals(info.currentState, DEFAULT_DEVICE_STATE.getIdentifier()); } + @Test + public void getDeviceStateInfo_baseStateAndCommittedStateNotSet() throws RemoteException { + // Create a provider and a service without an initial base state. + mProvider = new TestDeviceStateProvider(null /* initialState */); + mPolicy = new TestDeviceStatePolicy(mProvider); + setupDeviceStateManagerService(); + flushHandler(); // Flush the handler to ensure the initial values are committed. + + DeviceStateInfo info = mService.getBinderService().getDeviceStateInfo(); + + assertArrayEquals(info.supportedStates, SUPPORTED_DEVICE_STATE_IDENTIFIERS); + assertEquals(info.baseState, INVALID_DEVICE_STATE); + assertEquals(info.currentState, INVALID_DEVICE_STATE); + } + @FlakyTest(bugId = 223153452) @Test public void registerCallback() throws RemoteException { @@ -325,6 +344,21 @@ public final class DeviceStateManagerServiceTest { } @Test + public void registerCallback_initialValueUnavailable() throws RemoteException { + // Create a provider and a service without an initial base state. + mProvider = new TestDeviceStateProvider(null /* initialState */); + mPolicy = new TestDeviceStatePolicy(mProvider); + setupDeviceStateManagerService(); + flushHandler(); // Flush the handler to ensure the initial values are committed. + + TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); + mService.getBinderService().registerCallback(callback); + flushHandler(); + // The callback should never be called when the base state is not set yet. + assertNull(callback.getLastNotifiedInfo()); + } + + @Test public void requestState() throws RemoteException { TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback(); mService.getBinderService().registerCallback(callback); @@ -939,8 +973,18 @@ public final class DeviceStateManagerServiceTest { DEFAULT_DEVICE_STATE, OTHER_DEVICE_STATE, DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP}; + + @Nullable private final DeviceState mInitialState; private Listener mListener; + private TestDeviceStateProvider() { + this(DEFAULT_DEVICE_STATE); + } + + private TestDeviceStateProvider(@Nullable DeviceState initialState) { + mInitialState = initialState; + } + @Override public void setListener(Listener listener) { if (mListener != null) { @@ -950,7 +994,9 @@ public final class DeviceStateManagerServiceTest { mListener = listener; mListener.onSupportedDeviceStatesChanged(mSupportedDeviceStates, SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED); - mListener.onStateChanged(mSupportedDeviceStates[0].getIdentifier()); + if (mInitialState != null) { + mListener.onStateChanged(mInitialState.getIdentifier()); + } } public void notifySupportedDeviceStates(DeviceState[] supportedDeviceStates) { diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java index 8981160d1f25..021f2d1df835 100644 --- a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java @@ -323,6 +323,61 @@ public class BrightnessTrackerTest { } @Test + public void testMultipleBrightnessEvents() { + final float brightnessOne = 0.2f; + final float brightnessTwo = 0.4f; + final float brightnessThree = 0.6f; + final float brightnessFour = 0.3f; + final String displayId = "1234"; + final float[] luxValues = new float[]{1.0f}; + + startTracker(mTracker); + final long sensorTime = TimeUnit.NANOSECONDS.toMillis(mInjector.elapsedRealtimeNanos()); + final long sensorTime2 = sensorTime + TimeUnit.SECONDS.toMillis(20); + final long sensorTime3 = sensorTime2 + TimeUnit.SECONDS.toMillis(30); + final long sensorTime4 = sensorTime3 + TimeUnit.SECONDS.toMillis(40); + final long originalTime = mInjector.currentTimeMillis(); + + mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2)); + notifyBrightnessChanged(mTracker, brightnessOne, displayId, luxValues, + new long[] {sensorTime}); + + mInjector.incrementTime(TimeUnit.SECONDS.toMillis(20)); + notifyBrightnessChanged(mTracker, brightnessTwo, displayId, luxValues, + new long[] {sensorTime2}); + + mInjector.incrementTime(TimeUnit.SECONDS.toMillis(30)); + notifyBrightnessChanged(mTracker, brightnessThree, displayId, luxValues, + new long[] {sensorTime3}); + + mInjector.incrementTime(TimeUnit.SECONDS.toMillis(40)); + notifyBrightnessChanged(mTracker, brightnessFour, displayId, luxValues, + new long[] {sensorTime4}); + mTracker.stop(); + List<BrightnessChangeEvent> events = mTracker.getEvents(0, true).getList(); + assertEquals(4, events.size()); + BrightnessChangeEvent eventOne = events.get(0); + assertEquals(brightnessOne, eventOne.brightness, FLOAT_DELTA); + assertEquals(originalTime, + eventOne.luxTimestamps[0]); + + BrightnessChangeEvent eventTwo = events.get(1); + assertEquals(brightnessTwo, eventTwo.brightness, FLOAT_DELTA); + assertEquals(originalTime + TimeUnit.SECONDS.toMillis(20), + eventTwo.luxTimestamps[0]); + + BrightnessChangeEvent eventThree = events.get(2); + assertEquals(brightnessThree, eventThree.brightness, FLOAT_DELTA); + assertEquals(originalTime + TimeUnit.SECONDS.toMillis(50), + eventThree.luxTimestamps[0]); + + BrightnessChangeEvent eventFour = events.get(3); + assertEquals(brightnessFour, eventFour.brightness, FLOAT_DELTA); + assertEquals(originalTime + TimeUnit.SECONDS.toMillis(90), + eventFour.luxTimestamps[0]); + } + + @Test public void testBrightnessFullPopulatedEvent() { final int initialBrightness = 230; final int brightness = 130; diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java index f79097f2a7ca..a6c57371803b 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -1084,7 +1084,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession( mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class)); ContentRecordingSession session = mContentRecordingSessionCaptor.getValue(); - assertThat(session.isWaitingToRecord()).isTrue(); + assertThat(session.isWaitingForConsent()).isTrue(); } @Test @@ -1119,7 +1119,7 @@ public class DisplayManagerServiceTest { assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_DISPLAY); assertThat(session.getVirtualDisplayId()).isEqualTo(displayId); assertThat(session.getDisplayToRecord()).isEqualTo(displayToRecord); - assertThat(session.isWaitingToRecord()).isFalse(); + assertThat(session.isWaitingForConsent()).isFalse(); } @Test diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java index f6cf57161f59..30ff8ba6e331 100644 --- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java +++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java @@ -17,6 +17,7 @@ package com.android.server.display; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; @@ -191,6 +192,19 @@ public class LogicalDisplayTest { } @Test + public void testUpdateLayoutLimitedRefreshRate_setsDirtyFlag() { + SurfaceControl.RefreshRateRange layoutLimitedRefreshRate = + new SurfaceControl.RefreshRateRange(0, 120); + assertFalse(mLogicalDisplay.isDirtyLocked()); + + mLogicalDisplay.updateLayoutLimitedRefreshRateLocked(layoutLimitedRefreshRate); + assertTrue(mLogicalDisplay.isDirtyLocked()); + + mLogicalDisplay.updateLocked(mDeviceRepo); + assertFalse(mLogicalDisplay.isDirtyLocked()); + } + + @Test public void testUpdateRefreshRateThermalThrottling() { SparseArray<SurfaceControl.RefreshRateRange> refreshRanges = new SparseArray<>(); refreshRanges.put(0, new SurfaceControl.RefreshRateRange(0, 120)); @@ -207,6 +221,45 @@ public class LogicalDisplayTest { } @Test + public void testUpdateRefreshRateThermalThrottling_setsDirtyFlag() { + SparseArray<SurfaceControl.RefreshRateRange> refreshRanges = new SparseArray<>(); + refreshRanges.put(0, new SurfaceControl.RefreshRateRange(0, 120)); + assertFalse(mLogicalDisplay.isDirtyLocked()); + + mLogicalDisplay.updateThermalRefreshRateThrottling(refreshRanges); + assertTrue(mLogicalDisplay.isDirtyLocked()); + + mLogicalDisplay.updateLocked(mDeviceRepo); + assertFalse(mLogicalDisplay.isDirtyLocked()); + } + + @Test + public void testUpdateDisplayGroupIdLocked() { + int newId = 999; + DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked(); + mLogicalDisplay.updateDisplayGroupIdLocked(newId); + DisplayInfo info2 = mLogicalDisplay.getDisplayInfoLocked(); + // Display info should only be updated when updateLocked is called + assertEquals(info2, info1); + + mLogicalDisplay.updateLocked(mDeviceRepo); + DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked(); + assertNotEquals(info3, info2); + assertEquals(newId, info3.displayGroupId); + } + + @Test + public void testUpdateDisplayGroupIdLocked_setsDirtyFlag() { + assertFalse(mLogicalDisplay.isDirtyLocked()); + + mLogicalDisplay.updateDisplayGroupIdLocked(99); + assertTrue(mLogicalDisplay.isDirtyLocked()); + + mLogicalDisplay.updateLocked(mDeviceRepo); + assertFalse(mLogicalDisplay.isDirtyLocked()); + } + + @Test public void testSetThermalBrightnessThrottlingDataId() { String brightnessThrottlingDataId = "throttling_data_id"; DisplayInfo info1 = mLogicalDisplay.getDisplayInfoLocked(); @@ -220,4 +273,15 @@ public class LogicalDisplayTest { assertNotEquals(info3, info2); assertEquals(brightnessThrottlingDataId, info3.thermalBrightnessThrottlingDataId); } + + @Test + public void testSetThermalBrightnessThrottlingDataId_setsDirtyFlag() { + assertFalse(mLogicalDisplay.isDirtyLocked()); + + mLogicalDisplay.setThermalBrightnessThrottlingDataIdLocked("99"); + assertTrue(mLogicalDisplay.isDirtyLocked()); + + mLogicalDisplay.updateLocked(mDeviceRepo); + assertFalse(mLogicalDisplay.isDirtyLocked()); + } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java index ad1ecf1ad1c8..be5e2623ac20 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java @@ -26,7 +26,6 @@ import static org.mockito.Mockito.spy; import android.content.Context; import android.content.ContextWrapper; -import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -61,19 +60,9 @@ public class ActiveSourceActionTest { public void setUp() throws Exception { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + FakeAudioFramework audioFramework = new FakeAudioFramework(); mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(), - new FakeAudioDeviceVolumeManagerWrapper()) { - @Override - AudioManager getAudioManager() { - return new AudioManager() { - @Override - public void setWiredDeviceConnectionState( - int type, int state, String address, String name) { - // Do nothing. - } - }; - } - + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isPowerStandby() { return false; @@ -149,31 +138,4 @@ public class ActiveSourceActionTest { assertThat(playbackDevice.getActiveSource().physicalAddress).isEqualTo(mPhysicalAddress); assertThat(playbackDevice.isActiveSource()).isTrue(); } - - @Test - public void audioDevice_sendsActiveSource_noMenuStatus() { - HdmiCecLocalDeviceAudioSystem audioDevice = new HdmiCecLocalDeviceAudioSystem( - mHdmiControlService); - audioDevice.init(); - mLocalDevices.add(audioDevice); - mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); - mTestLooper.dispatchAll(); - - HdmiCecFeatureAction action = new com.android.server.hdmi.ActiveSourceAction( - audioDevice, ADDR_TV); - audioDevice.addAndStartAction(action); - mTestLooper.dispatchAll(); - - HdmiCecMessage activeSource = - HdmiCecMessageBuilder.buildActiveSource( - audioDevice.getDeviceInfo().getLogicalAddress(), mPhysicalAddress); - HdmiCecMessage menuStatus = - HdmiCecMessageBuilder.buildReportMenuStatus( - audioDevice.getDeviceInfo().getLogicalAddress(), - ADDR_TV, - Constants.MENU_STATE_ACTIVATED); - - assertThat(mNativeWrapper.getResultMessages()).contains(activeSource); - assertThat(mNativeWrapper.getResultMessages()).doesNotContain(menuStatus); - } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java index 3df0449c14b7..5be3c8e4671c 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java @@ -25,7 +25,6 @@ import static org.mockito.Mockito.spy; import android.content.Context; import android.content.ContextWrapper; import android.hardware.tv.cec.V1_0.SendMessageResult; -import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -37,8 +36,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Collections; @@ -58,27 +55,21 @@ public class ArcInitiationActionFromAvrTest { private TestLooper mTestLooper = new TestLooper(); private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); - @Mock - private AudioManager mAudioManager; - @Before public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + HdmiControlService hdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(), - new FakeAudioDeviceVolumeManagerWrapper()) { + audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isPowerStandby() { return false; } - @Override - AudioManager getAudioManager() { - return mAudioManager; - } @Override boolean isAddressAllocated() { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java index 61ab99b1d017..7845c307c15f 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java @@ -27,7 +27,6 @@ import android.content.ContextWrapper; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.tv.cec.V1_0.SendMessageResult; -import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -39,7 +38,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -61,8 +59,6 @@ public class ArcTerminationActionFromAvrTest { private TestLooper mTestLooper = new TestLooper(); private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); - @Mock - private AudioManager mAudioManager; @Before public void setUp() throws Exception { @@ -70,14 +66,12 @@ public class ArcTerminationActionFromAvrTest { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + HdmiControlService hdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(), - new FakeAudioDeviceVolumeManagerWrapper()) { - @Override - AudioManager getAudioManager() { - return mAudioManager; - } - + audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isPowerStandby() { return false; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java index 9f295b8f42c2..bc09d4b10723 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java @@ -24,16 +24,11 @@ import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_BOOT_UP; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; -import static org.mockito.AdditionalMatchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -59,8 +54,6 @@ import com.android.server.SystemService; import org.junit.Before; import org.junit.Test; -import org.mockito.InOrder; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -68,20 +61,20 @@ import java.util.Arrays; import java.util.Collections; /** - * Tests that Absolute Volume Control (AVC) is enabled and disabled correctly, and that + * Tests that absolute volume behavior (AVB) is enabled and disabled correctly, and that * the device responds correctly to incoming <Report Audio Status> messages and API calls - * from AudioService when AVC is active. + * from AudioService when AVB is active. * * This is an abstract base class. Concrete subclasses specify the type of the local device, and the * type of the System Audio device. This allows each test to be run for multiple setups. * * We test the following pairs of (local device, System Audio device): - * (Playback, TV): {@link PlaybackDeviceToTvAvcTest} - * (Playback, Audio System): {@link PlaybackDeviceToAudioSystemAvcTest} - * (TV, Audio System): {@link TvToAudioSystemAvcTest} + * (Playback, TV): {@link PlaybackDeviceToTvAvbTest} + * (Playback, Audio System): {@link PlaybackDeviceToAudioSystemAvbTest} + * (TV, Audio System): {@link TvToAudioSystemAvbTest} */ -public abstract class BaseAbsoluteVolumeControlTest { - private HdmiControlService mHdmiControlService; +public abstract class BaseAbsoluteVolumeBehaviorTest { + protected HdmiControlService mHdmiControlService; private HdmiCecController mHdmiCecController; private HdmiCecLocalDevice mHdmiCecLocalDevice; private FakeHdmiCecConfig mHdmiCecConfig; @@ -90,19 +83,20 @@ public abstract class BaseAbsoluteVolumeControlTest { private Context mContextSpy; private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); - @Mock protected AudioManager mAudioManager; - protected FakeAudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager; + protected FakeAudioFramework mAudioFramework; + protected AudioManagerWrapper mAudioManager; + protected AudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager; protected TestLooper mTestLooper = new TestLooper(); protected FakeNativeWrapper mNativeWrapper; - // Audio Status given by the System Audio device in its initial <Report Audio Status> that - // triggers AVC being enabled + // Default Audio Status given by the System Audio device in its initial <Report Audio Status> + // that triggers AVB being enabled private static final AudioStatus INITIAL_SYSTEM_AUDIO_DEVICE_STATUS = new AudioStatus(50, false); - // VolumeInfo passed to AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior to enable AVC - private static final VolumeInfo ENABLE_AVC_VOLUME_INFO = + // VolumeInfo passed to AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior to enable AVB + private static final VolumeInfo ENABLE_AVB_VOLUME_INFO = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) .setMuted(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute()) .setVolumeIndex(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume()) @@ -110,6 +104,8 @@ public abstract class BaseAbsoluteVolumeControlTest { .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build(); + private static final int EMPTY_FLAGS = 0; + protected abstract HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService); protected abstract int getPhysicalAddress(); @@ -126,17 +122,17 @@ public abstract class BaseAbsoluteVolumeControlTest { mContextSpy = spy(new ContextWrapper( InstrumentationRegistry.getInstrumentation().getTargetContext())); - mAudioDeviceVolumeManager = spy(new FakeAudioDeviceVolumeManagerWrapper()); + mAudioFramework = new FakeAudioFramework(); + mAudioManager = spy(mAudioFramework.getAudioManager()); + mAudioDeviceVolumeManager = spy(mAudioFramework.getAudioDeviceVolumeManager()); + + mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 1, EMPTY_FLAGS); + mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, true); mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), Collections.singletonList(getDeviceType()), - mAudioDeviceVolumeManager) { - @Override - AudioManager getAudioManager() { - return mAudioManager; - } - + mAudioManager, mAudioDeviceVolumeManager) { @Override protected void writeStringSystemProperty(String key, String value) { // do nothing @@ -186,22 +182,12 @@ public abstract class BaseAbsoluteVolumeControlTest { mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_BOOT_UP); mTestLooper.dispatchAll(); - // Simulate AudioManager's behavior and response when setDeviceVolumeBehavior is called - doAnswer(invocation -> { - setDeviceVolumeBehavior(invocation.getArgument(0), invocation.getArgument(1)); - return null; - }).when(mAudioManager).setDeviceVolumeBehavior(any(), anyInt()); - - // Set starting volume behavior - doReturn(AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE) - .when(mAudioManager).getDeviceVolumeBehavior(eq(getAudioOutputDevice())); - // Audio service always plays STREAM_MUSIC on the device we need - doReturn(Collections.singletonList(getAudioOutputDevice())).when(mAudioManager) - .getDevicesForAttributes(HdmiControlService.STREAM_MUSIC_ATTRIBUTES); + mAudioFramework.setDevicesForAttributes(HdmiControlService.STREAM_MUSIC_ATTRIBUTES, + Collections.singletonList(getAudioOutputDevice())); // Max volume of STREAM_MUSIC - doReturn(25).when(mAudioManager).getStreamMaxVolume(AudioManager.STREAM_MUSIC); + mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, 25); // Receive messages from devices to make sure they're registered in HdmiCecNetwork mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveDevicePowerStatus( @@ -221,16 +207,6 @@ public abstract class BaseAbsoluteVolumeControlTest { } /** - * Simulates the volume behavior of {@code device} being set to {@code behavior}. - */ - protected void setDeviceVolumeBehavior(AudioDeviceAttributes device, - @AudioManager.DeviceVolumeBehavior int behavior) { - doReturn(behavior).when(mAudioManager).getDeviceVolumeBehavior(eq(device)); - mHdmiControlService.onDeviceVolumeBehaviorChanged(device, behavior); - mTestLooper.dispatchAll(); - } - - /** * Changes the setting for CEC volume. */ protected void setCecVolumeControlSetting(@HdmiControlManager.VolumeControl int setting) { @@ -278,16 +254,6 @@ public abstract class BaseAbsoluteVolumeControlTest { } /** - * Has the device receive a <Report Audio Status> reporting the status in - * {@link #INITIAL_SYSTEM_AUDIO_DEVICE_STATUS} - */ - protected void receiveInitialReportAudioStatus() { - receiveReportAudioStatus( - INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(), - INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute()); - } - - /** * Has the device receive a <Report Audio Status> message from the System Audio Device. */ protected void receiveReportAudioStatus(int volume, boolean mute) { @@ -300,40 +266,34 @@ public abstract class BaseAbsoluteVolumeControlTest { } /** - * Triggers all the conditions required to enable Absolute Volume Control. + * Triggers all the conditions required to enable absolute volume behavior. */ - protected void enableAbsoluteVolumeControl() { - setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + protected void enableAbsoluteVolumeBehavior() { + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED); enableSystemAudioModeIfNeeded(); - receiveInitialReportAudioStatus(); + receiveReportAudioStatus( + INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(), + INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute()); - verifyAbsoluteVolumeEnabled(); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); } - /** - * Verifies that AVC was enabled - that is the audio output device's volume behavior was last - * set to absolute volume behavior. - */ - protected void verifyAbsoluteVolumeEnabled() { - InOrder inOrder = inOrder(mAudioManager, mAudioDeviceVolumeManager); - inOrder.verify(mAudioDeviceVolumeManager, atLeastOnce()).setDeviceAbsoluteVolumeBehavior( - eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean()); - inOrder.verify(mAudioManager, never()).setDeviceVolumeBehavior( - eq(getAudioOutputDevice()), not(eq(AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE))); - } + protected void enableAdjustOnlyAbsoluteVolumeBehavior() { + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); + enableSystemAudioModeIfNeeded(); + receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED); + receiveReportAudioStatus( + INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(), + INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute()); - /** - * Verifies that AVC was disabled - that is, the audio output device's volume behavior was - * last set to something other than absolute volume behavior. - */ - protected void verifyAbsoluteVolumeDisabled() { - InOrder inOrder = inOrder(mAudioManager, mAudioDeviceVolumeManager); - inOrder.verify(mAudioManager, atLeastOnce()).setDeviceVolumeBehavior( - eq(getAudioOutputDevice()), not(eq(AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE))); - inOrder.verify(mAudioDeviceVolumeManager, never()).setDeviceAbsoluteVolumeBehavior( - eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean()); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); } protected void verifyGiveAudioStatusNeverSent() { @@ -350,7 +310,8 @@ public abstract class BaseAbsoluteVolumeControlTest { @Test public void allConditionsExceptSavlSupportMet_sendsSetAudioVolumeLevelAndGiveFeatures() { - setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); enableSystemAudioModeIfNeeded(); @@ -365,7 +326,8 @@ public abstract class BaseAbsoluteVolumeControlTest { @Test public void allConditionsMet_savlSupportLast_reportFeatures_giveAudioStatusSent() { - setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); enableSystemAudioModeIfNeeded(); verifyGiveAudioStatusNeverSent(); @@ -376,7 +338,8 @@ public abstract class BaseAbsoluteVolumeControlTest { @Test public void allConditionsMet_savlSupportLast_noFeatureAbort_giveAudioStatusSent() { - setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); enableSystemAudioModeIfNeeded(); verifyGiveAudioStatusNeverSent(); @@ -388,7 +351,8 @@ public abstract class BaseAbsoluteVolumeControlTest { @Test public void allConditionsMet_cecVolumeEnabledLast_giveAudioStatusSent() { - setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); enableSystemAudioModeIfNeeded(); receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED); verifyGiveAudioStatusNeverSent(); @@ -404,7 +368,9 @@ public abstract class BaseAbsoluteVolumeControlTest { receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED); verifyGiveAudioStatusNeverSent(); - setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + mTestLooper.dispatchAll(); verifyGiveAudioStatusSent(); } @@ -413,7 +379,8 @@ public abstract class BaseAbsoluteVolumeControlTest { // Only run when the System Audio device is an Audio System. assume().that(getSystemAudioDeviceType()).isEqualTo(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); - setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED); verifyGiveAudioStatusNeverSent(); @@ -423,62 +390,97 @@ public abstract class BaseAbsoluteVolumeControlTest { } @Test - public void giveAudioStatusSent_systemAudioDeviceSendsReportAudioStatus_avcEnabled() { + public void giveAudioStatusSent_systemAudioDeviceSendsReportAudioStatus_avbEnabled() { + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); enableSystemAudioModeIfNeeded(); receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED); - setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); - // Verify that AVC was never enabled - verify(mAudioDeviceVolumeManager, never()).setDeviceAbsoluteVolumeBehavior( - eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean()); - receiveInitialReportAudioStatus(); + // AVB should not be enabled before receiving <Report Audio Status> + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + + receiveReportAudioStatus(60, false); + + // Check that absolute volume behavior was the last one adopted + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); + + // Check that the volume and mute status received were included when setting AVB + verify(mAudioDeviceVolumeManager).setDeviceAbsoluteVolumeBehavior( + eq(getAudioOutputDevice()), + eq(new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setVolumeIndex(60) + .setMuted(false) + .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) + .setMinVolumeIndex(AudioStatus.MIN_VOLUME) + .build()), + any(), any(), anyBoolean()); + } + + @Test + public void giveAudioStatusSent_reportAudioStatusVolumeOutOfBounds_avbNotEnabled() { + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); + enableSystemAudioModeIfNeeded(); + receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED); - verifyAbsoluteVolumeEnabled(); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + receiveReportAudioStatus(127, false); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); } @Test - public void avcEnabled_cecVolumeDisabled_absoluteVolumeDisabled() { - enableAbsoluteVolumeControl(); + public void avbEnabled_cecVolumeDisabled_avbDisabled() { + enableAbsoluteVolumeBehavior(); setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_DISABLED); - verifyAbsoluteVolumeDisabled(); + + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); } @Test - public void avcEnabled_setAudioVolumeLevelNotSupported_absoluteVolumeDisabled() { - enableAbsoluteVolumeControl(); + public void avbEnabled_setAudioVolumeLevelNotSupported_avbDisabled() { + enableAbsoluteVolumeBehavior(); receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED); - verifyAbsoluteVolumeDisabled(); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); } @Test - public void avcEnabled_setAudioVolumeLevelFeatureAborted_absoluteVolumeDisabled() { - enableAbsoluteVolumeControl(); + public void avbEnabled_setAudioVolumeLevelFeatureAborted_avbDisabled() { + enableAbsoluteVolumeBehavior(); mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildFeatureAbortCommand( getSystemAudioDeviceLogicalAddress(), getLogicalAddress(), Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL, Constants.ABORT_UNRECOGNIZED_OPCODE)); mTestLooper.dispatchAll(); - verifyAbsoluteVolumeDisabled(); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); } @Test - public void avcEnabled_systemAudioModeDisabled_absoluteVolumeDisabled() { + public void avbEnabled_systemAudioModeDisabled_avbDisabled() { // Only run when the System Audio device is an Audio System. assume().that(getSystemAudioDeviceType()).isEqualTo(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); - enableAbsoluteVolumeControl(); + enableAbsoluteVolumeBehavior(); receiveSetSystemAudioMode(false); - verifyAbsoluteVolumeDisabled(); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); } @Test - public void avcEnabled_receiveReportAudioStatus_notifiesVolumeOrMuteChanges() { + public void avbEnabled_receiveReportAudioStatus_notifiesVolumeOrMuteChanges() { // Initial <Report Audio Status> has volume=50 and mute=false - enableAbsoluteVolumeControl(); + enableAbsoluteVolumeBehavior(); // New volume and mute status: sets both receiveReportAudioStatus(20, true); @@ -512,12 +514,20 @@ public abstract class BaseAbsoluteVolumeControlTest { eq(AudioManager.ADJUST_UNMUTE), anyInt()); clearInvocations(mAudioManager); + // Volume not within range [0, 100]: sets neither volume nor mute + receiveReportAudioStatus(127, true); + verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(), + anyInt()); + verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(), + anyInt()); + clearInvocations(mAudioManager); + // If AudioService causes us to send <Set Audio Volume Level>, the System Audio device's // volume changes. Afterward, a duplicate of an earlier <Report Audio Status> should // still cause us to call setStreamVolume() mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged( getAudioOutputDevice(), - new VolumeInfo.Builder(ENABLE_AVC_VOLUME_INFO) + new VolumeInfo.Builder(ENABLE_AVB_VOLUME_INFO) .setVolumeIndex(20) .build() ); @@ -530,13 +540,13 @@ public abstract class BaseAbsoluteVolumeControlTest { } @Test - public void avcEnabled_audioDeviceVolumeAdjusted_sendsUserControlPressedAndGiveAudioStatus() { - enableAbsoluteVolumeControl(); + public void avbEnabled_audioDeviceVolumeAdjusted_sendsUserControlPressedAndGiveAudioStatus() { + enableAbsoluteVolumeBehavior(); mNativeWrapper.clearResultMessages(); mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted( getAudioOutputDevice(), - ENABLE_AVC_VOLUME_INFO, + ENABLE_AVB_VOLUME_INFO, AudioManager.ADJUST_RAISE, AudioDeviceVolumeManager.ADJUST_MODE_NORMAL ); @@ -554,14 +564,16 @@ public abstract class BaseAbsoluteVolumeControlTest { } @Test - public void avcEnabled_audioDeviceVolumeChanged_sendsSetAudioVolumeLevel() { - enableAbsoluteVolumeControl(); + public void avbEnabled_audioDeviceVolumeChanged_sendsSetAudioVolumeLevel() { + enableAbsoluteVolumeBehavior(); mNativeWrapper.clearResultMessages(); mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged( getAudioOutputDevice(), - new VolumeInfo.Builder(ENABLE_AVC_VOLUME_INFO) + new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) .setVolumeIndex(20) + .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) + .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build() ); mTestLooper.dispatchAll(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BasePlaybackDeviceAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BasePlaybackDeviceAvbTest.java new file mode 100644 index 000000000000..4c12e436542b --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/BasePlaybackDeviceAvbTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.hdmi; + +import android.hardware.hdmi.DeviceFeatures; +import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.AudioDeviceAttributes; +import android.media.AudioManager; + +import org.junit.Test; + +/** + * Base class for tests for absolute volume behavior on Playback devices. Contains tests that are + * relevant to Playback devices but not to TVs. + * + * Subclasses contain tests for the following pairs of (local device, System Audio device): + * (Playback, TV): {@link PlaybackDeviceToTvAvbTest} + * (Playback, Audio System): {@link PlaybackDeviceToAudioSystemAvbTest} + */ +public abstract class BasePlaybackDeviceAvbTest extends BaseAbsoluteVolumeBehaviorTest { + + @Override + protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) { + return new HdmiCecLocalDevicePlayback(hdmiControlService); + } + + @Override + protected int getPhysicalAddress() { + return 0x1100; + } + + @Override + protected int getDeviceType() { + return HdmiDeviceInfo.DEVICE_PLAYBACK; + } + + @Override + protected AudioDeviceAttributes getAudioOutputDevice() { + return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI; + } + + /** + * Unlike TVs, Playback devices don't start the process for adopting adjust-only AVB + * if the System Audio device doesn't support <Set Audio Volume Level> + */ + @Test + public void savlNotSupported_allOtherConditionsMet_giveAudioStatusNotSent() { + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); + enableSystemAudioModeIfNeeded(); + + receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED); + verifyGiveAudioStatusNeverSent(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java index 28ba4bb503f9..9b65762e48ec 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java @@ -54,9 +54,13 @@ public class DetectTvSystemAudioModeSupportActionTest { @Before public void SetUp() { mDeviceInfoForTests = HdmiDeviceInfo.hardwarePort(1001, 1234); + + FakeAudioFramework audioFramework = new FakeAudioFramework(); + HdmiControlService hdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { + Collections.emptyList(), audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { @Override void sendCecCommand( diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java index c4c5c2a7008e..af4eab39492d 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java @@ -31,7 +31,6 @@ import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.tv.cec.V1_0.SendMessageResult; -import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -76,19 +75,11 @@ public class DevicePowerStatusActionTest { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(mContextSpy, Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), - new FakeAudioDeviceVolumeManagerWrapper()) { - @Override - AudioManager getAudioManager() { - return new AudioManager() { - @Override - public void setWiredDeviceConnectionState( - int type, int state, String address, String name) { - // Do nothing. - } - }; - } + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isPowerStandby() { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java index b571f4354452..5070b08a10cc 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java @@ -97,10 +97,13 @@ public class DeviceSelectActionFromPlaybackTest { Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), - new FakeAudioDeviceVolumeManagerWrapper()) { + audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isCecControlEnabled() { return true; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java index 9c1b67010d03..49023c6a22c4 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java @@ -106,10 +106,13 @@ public class DeviceSelectActionFromTvTest { Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), - new FakeAudioDeviceVolumeManagerWrapper()) { + audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isCecControlEnabled() { return true; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java deleted file mode 100644 index d33ef9bc8879..000000000000 --- a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.hdmi; - -import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener; -import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener; - -import android.annotation.CallbackExecutor; -import android.annotation.NonNull; -import android.media.AudioDeviceAttributes; -import android.media.AudioDeviceVolumeManager; -import android.media.AudioManager; -import android.media.VolumeInfo; - -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.Executor; - -/** - * Wrapper for {@link AudioDeviceVolumeManager} that stubs its methods. Useful for testing. - */ -public class FakeAudioDeviceVolumeManagerWrapper implements - AudioDeviceVolumeManagerWrapperInterface { - - private final Set<OnDeviceVolumeBehaviorChangedListener> mVolumeBehaviorListeners; - - public FakeAudioDeviceVolumeManagerWrapper() { - mVolumeBehaviorListeners = new HashSet<>(); - } - - @Override - public void addOnDeviceVolumeBehaviorChangedListener( - @NonNull @CallbackExecutor Executor executor, - @NonNull OnDeviceVolumeBehaviorChangedListener listener) - throws SecurityException { - mVolumeBehaviorListeners.add(listener); - } - - @Override - public void removeOnDeviceVolumeBehaviorChangedListener( - @NonNull OnDeviceVolumeBehaviorChangedListener listener) { - mVolumeBehaviorListeners.remove(listener); - } - - @Override - public void setDeviceAbsoluteVolumeBehavior( - @NonNull AudioDeviceAttributes device, - @NonNull VolumeInfo volume, - @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { - // Notify all volume behavior listeners that the device adopted absolute volume behavior - for (OnDeviceVolumeBehaviorChangedListener listener : mVolumeBehaviorListeners) { - listener.onDeviceVolumeBehaviorChanged(device, - AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); - } - } -} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java new file mode 100644 index 000000000000..7294ba62cdae --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java @@ -0,0 +1,239 @@ +/* + * 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.server.hdmi; + +import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener; +import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.media.VolumeInfo; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; + +/** + * Contains a fake AudioManager and fake AudioDeviceVolumeManager. + * Stores the shared state for these managers, simulating a fake AudioService. + */ +public class FakeAudioFramework { + + private final FakeAudioManagerWrapper mAudioManager = new FakeAudioManagerWrapper(); + private final FakeAudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager = + new FakeAudioDeviceVolumeManagerWrapper(); + + private static final int DEFAULT_DEVICE_VOLUME_BEHAVIOR = + AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE; + private final Map<AudioDeviceAttributes, Integer> mDeviceVolumeBehaviors = new HashMap<>(); + + private final Set<OnDeviceVolumeBehaviorChangedListener> mVolumeBehaviorListeners = + new HashSet<>(); + + private final Map<AudioAttributes, List<AudioDeviceAttributes>> mDevicesForAttributes = + new HashMap<>(); + + private static final int DEFAULT_VOLUME = 0; + private final Map<Integer, Integer> mStreamVolumes = new HashMap<>(); + + private static final int DEFAULT_MAX_VOLUME = 100; + private final Map<Integer, Integer> mStreamMaxVolumes = new HashMap<>(); + + private static final boolean DEFAULT_MUTE_STATUS = false; + private final Map<Integer, Boolean> mStreamMuteStatuses = new HashMap<>(); + + public FakeAudioFramework() { + } + + /** + * Returns a fake AudioManager whose methods affect this object's internal state. + */ + public FakeAudioManagerWrapper getAudioManager() { + return mAudioManager; + } + + public class FakeAudioManagerWrapper implements AudioManagerWrapper { + @Override + public void adjustStreamVolume(int streamType, int direction, + @AudioManager.PublicVolumeFlags int flags) { + switch (direction) { + case AudioManager.ADJUST_MUTE: + mStreamMuteStatuses.put(streamType, true); + break; + case AudioManager.ADJUST_UNMUTE: + mStreamMuteStatuses.put(streamType, false); + break; + default: + // Other adjustments not implemented + } + } + + @Override + public void setStreamVolume(int streamType, int index, + @AudioManager.PublicVolumeFlags int flags) { + mStreamVolumes.put(streamType, index); + } + + @Override + public int getStreamVolume(int streamType) { + return mStreamVolumes.getOrDefault(streamType, DEFAULT_VOLUME); + } + + @Override + public int getStreamMinVolume(int streamType) { + return 0; + } + + @Override + public int getStreamMaxVolume(int streamType) { + return mStreamMaxVolumes.getOrDefault(streamType, DEFAULT_MAX_VOLUME); + } + + @Override + public boolean isStreamMute(int streamType) { + return mStreamMuteStatuses.getOrDefault(streamType, DEFAULT_MUTE_STATUS); + } + + @Override + public void setStreamMute(int streamType, boolean state) { + mStreamMuteStatuses.put(streamType, state); + } + + @Override + public int setHdmiSystemAudioSupported(boolean on) { + return AudioSystem.DEVICE_NONE; + } + + @Override + public void setWiredDeviceConnectionState(AudioDeviceAttributes attributes, int state) { + // Do nothing + } + + @Override + public void setWiredDeviceConnectionState(int device, int state, String address, + String name) { + // Do nothing + } + + + @Override + @AudioManager.DeviceVolumeBehavior + public int getDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device) { + return mDeviceVolumeBehaviors.getOrDefault(device, DEFAULT_DEVICE_VOLUME_BEHAVIOR); + } + + public void setDeviceVolumeBehavior(@NonNull AudioDeviceAttributes device, + @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior) { + setVolumeBehaviorHelper(device, deviceVolumeBehavior); + } + + @Override + @NonNull + public List<AudioDeviceAttributes> getDevicesForAttributes( + @NonNull AudioAttributes attributes) { + return mDevicesForAttributes.getOrDefault(attributes, Collections.emptyList()); + } + } + + /** + * Returns a fake AudioDeviceVolumeManager whose methods affect this object's internal state. + */ + public FakeAudioDeviceVolumeManagerWrapper getAudioDeviceVolumeManager() { + return mAudioDeviceVolumeManager; + } + + public class FakeAudioDeviceVolumeManagerWrapper implements AudioDeviceVolumeManagerWrapper { + @Override + public void addOnDeviceVolumeBehaviorChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnDeviceVolumeBehaviorChangedListener listener) + throws SecurityException { + mVolumeBehaviorListeners.add(listener); + } + + @Override + public void removeOnDeviceVolumeBehaviorChangedListener( + @NonNull OnDeviceVolumeBehaviorChangedListener listener) { + mVolumeBehaviorListeners.remove(listener); + } + + @Override + public void setDeviceAbsoluteVolumeBehavior( + @NonNull AudioDeviceAttributes device, + @NonNull VolumeInfo volume, + @NonNull @CallbackExecutor Executor executor, + @NonNull OnAudioDeviceVolumeChangedListener vclistener, + boolean handlesVolumeAdjustment) { + setVolumeBehaviorHelper(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); + } + + @Override + public void setDeviceAbsoluteVolumeAdjustOnlyBehavior( + @NonNull AudioDeviceAttributes device, + @NonNull VolumeInfo volume, + @NonNull @CallbackExecutor Executor executor, + @NonNull OnAudioDeviceVolumeChangedListener vclistener, + boolean handlesVolumeAdjustment) { + setVolumeBehaviorHelper(device, + AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); + } + } + + /** + * Allows tests to manipulate the return value of + * {@link FakeAudioManagerWrapper#getDevicesForAttributes} + */ + public void setDevicesForAttributes(AudioAttributes attributes, + List<AudioDeviceAttributes> devices) { + mDevicesForAttributes.put(attributes, devices); + } + + /** + * Allows tests to manipulate the return value of + * {@link FakeAudioManagerWrapper#getStreamMaxVolume} + */ + public void setStreamMaxVolume(int streamType, int maxVolume) { + mStreamMaxVolumes.put(streamType, maxVolume); + } + + /** + * Helper method for changing an audio device's volume behavior. Notifies listeners. + */ + private void setVolumeBehaviorHelper(AudioDeviceAttributes device, + @AudioManager.DeviceVolumeBehavior int newVolumeBehavior) { + + int currentVolumeBehavior = mDeviceVolumeBehaviors.getOrDefault( + device, DEFAULT_DEVICE_VOLUME_BEHAVIOR); + + mDeviceVolumeBehaviors.put(device, newVolumeBehavior); + + if (newVolumeBehavior != currentVolumeBehavior) { + // Notify volume behavior listeners + for (OnDeviceVolumeBehaviorChangedListener listener : mVolumeBehaviorListeners) { + listener.onDeviceVolumeBehaviorChanged(device, newVolumeBehavior); + } + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java index e3d95586f943..5e54d3b8c237 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java @@ -86,9 +86,11 @@ public class HdmiCecAtomLoggingTest { mContextSpy = spy(new ContextWrapper( InstrumentationRegistry.getInstrumentation().getTargetContext())); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), - new FakeAudioDeviceVolumeManagerWrapper())); + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager())); doNothing().when(mHdmiControlServiceSpy) .writeStringSystemProperty(anyString(), anyString()); doReturn(mHdmiCecAtomWriterSpy).when(mHdmiControlServiceSpy).getAtomWriter(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java index a7232fefed10..0870bac6ef38 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java @@ -100,9 +100,11 @@ public class HdmiCecControllerTest { public void SetUp() { mMyLooper = mTestLooper.getLooper(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlServiceSpy = spy(new HdmiControlService( InstrumentationRegistry.getTargetContext(), Collections.emptyList(), - new FakeAudioDeviceVolumeManagerWrapper())); + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager())); doReturn(mMyLooper).when(mHdmiControlServiceSpy).getIoLooper(); doReturn(mMyLooper).when(mHdmiControlServiceSpy).getServiceLooper(); doAnswer(__ -> mCecVersion).when(mHdmiControlServiceSpy).getCecVersion(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java index f5c0f2a0a4b6..a6e05ddc792c 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java @@ -54,23 +54,23 @@ import java.util.ArrayList; @RunWith(JUnit4.class) /** Tests for {@link HdmiCecLocalDeviceAudioSystem} class. */ public class HdmiCecLocalDeviceAudioSystemTest { - private static final HdmiCecMessage MESSAGE_REQUEST_SAD_LCPM = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( ADDR_TV, ADDR_AUDIO_SYSTEM, new int[] {Constants.AUDIO_CODEC_LPCM}); + private static final int EMPTY_FLAGS = 0; + private HdmiControlService mHdmiControlService; private HdmiCecController mHdmiCecController; private HdmiCecLocalDeviceAudioSystem mHdmiCecLocalDeviceAudioSystem; private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback; private FakeNativeWrapper mNativeWrapper; private FakePowerManagerWrapper mPowerManager; + private FakeAudioFramework mAudioFramework; + private AudioManagerWrapper mAudioManager; private Looper mMyLooper; private TestLooper mTestLooper = new TestLooper(); private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); - private int mMusicVolume; - private int mMusicMaxVolume; - private boolean mMusicMute; private static final int SELF_PHYSICAL_ADDRESS = 0x2000; private static final int HDMI_1_PHYSICAL_ADDRESS = 0x2100; private static final int HDMI_2_PHYSICAL_ADDRESS = 0x2200; @@ -88,66 +88,12 @@ public class HdmiCecLocalDeviceAudioSystemTest { mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK); mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); + mAudioFramework = new FakeAudioFramework(); + mAudioManager = mAudioFramework.getAudioManager(); mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), - mLocalDeviceTypes, - new FakeAudioDeviceVolumeManagerWrapper()) { - @Override - AudioManager getAudioManager() { - return new AudioManager() { - @Override - public int getStreamVolume(int streamType) { - switch (streamType) { - case STREAM_MUSIC: - return mMusicVolume; - default: - return 0; - } - } - - @Override - public boolean isStreamMute(int streamType) { - switch (streamType) { - case STREAM_MUSIC: - return mMusicMute; - default: - return false; - } - } - - @Override - public int getStreamMaxVolume(int streamType) { - switch (streamType) { - case STREAM_MUSIC: - return mMusicMaxVolume; - default: - return 100; - } - } - - @Override - public void adjustStreamVolume( - int streamType, int direction, int flags) { - switch (streamType) { - case STREAM_MUSIC: - if (direction == AudioManager.ADJUST_UNMUTE) { - mMusicMute = false; - } else if (direction == AudioManager.ADJUST_MUTE) { - mMusicMute = true; - } - break; - default: - } - } - - @Override - public void setWiredDeviceConnectionState( - int type, int state, String address, String name) { - // Do nothing. - } - }; - } - + mLocalDeviceTypes, mAudioManager, + mAudioFramework.getAudioDeviceVolumeManager()) { @Override void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) { mDeviceInfo = device; @@ -236,10 +182,12 @@ public class HdmiCecLocalDeviceAudioSystemTest { @Test public void handleGiveAudioStatus_volume_10_mute_true() throws Exception { - mMusicVolume = 10; - mMusicMute = true; - mMusicMaxVolume = 20; - int scaledVolume = VolumeControlAction.scaleToCecVolume(10, mMusicMaxVolume); + mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 10, EMPTY_FLAGS); + mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_MUTE, + EMPTY_FLAGS); + mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, 20); + int scaledVolume = VolumeControlAction.scaleToCecVolume(10, + mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)); HdmiCecMessage expectedMessage = HdmiCecMessageBuilder.buildReportAudioStatus( ADDR_AUDIO_SYSTEM, ADDR_TV, scaledVolume, true); @@ -303,7 +251,7 @@ public class HdmiCecLocalDeviceAudioSystemTest { @Test @Ignore("b/120845532") public void handleSetSystemAudioMode_setOn_orignalOff() throws Exception { - mMusicMute = true; + mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, true); HdmiCecMessage messageSet = HdmiCecMessageBuilder.buildSetSystemAudioMode(ADDR_TV, ADDR_AUDIO_SYSTEM, true); HdmiCecMessage messageGive = @@ -326,13 +274,13 @@ public class HdmiCecLocalDeviceAudioSystemTest { .isEqualTo(Constants.HANDLED); mTestLooper.dispatchAll(); assertThat(mNativeWrapper.getOnlyResultMessage()).isEqualTo(expectedMessage); - assertThat(mMusicMute).isFalse(); + assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isFalse(); } @Test @Ignore("b/120845532") public void handleSystemAudioModeRequest_turnOffByTv() throws Exception { - assertThat(mMusicMute).isFalse(); + assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isFalse(); // Check if feature correctly turned off HdmiCecMessage messageGive = HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus(ADDR_TV, ADDR_AUDIO_SYSTEM); @@ -354,7 +302,7 @@ public class HdmiCecLocalDeviceAudioSystemTest { .isEqualTo(Constants.HANDLED); mTestLooper.dispatchAll(); assertThat(mNativeWrapper.getOnlyResultMessage()).isEqualTo(expectedMessage); - assertThat(mMusicMute).isTrue(); + assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isTrue(); } @Test @@ -368,7 +316,7 @@ public class HdmiCecLocalDeviceAudioSystemTest { HdmiCecMessageBuilder.buildSetSystemAudioMode( ADDR_AUDIO_SYSTEM, ADDR_BROADCAST, false); assertThat(mNativeWrapper.getResultMessages()).contains(expectedMessage); - assertThat(mMusicMute).isTrue(); + assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isTrue(); } @Test @@ -463,13 +411,13 @@ public class HdmiCecLocalDeviceAudioSystemTest { public void terminateSystemAudioMode_systemAudioModeOff() throws Exception { mHdmiCecLocalDeviceAudioSystem.checkSupportAndSetSystemAudioMode(false); assertThat(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated()).isFalse(); - mMusicMute = false; + mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, false); HdmiCecMessage message = HdmiCecMessageBuilder.buildSetSystemAudioMode( ADDR_AUDIO_SYSTEM, ADDR_BROADCAST, false); mHdmiCecLocalDeviceAudioSystem.terminateSystemAudioMode(); assertThat(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated()).isFalse(); - assertThat(mMusicMute).isFalse(); + assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isFalse(); assertThat(mNativeWrapper.getResultMessages()).isEmpty(); } @@ -477,13 +425,13 @@ public class HdmiCecLocalDeviceAudioSystemTest { public void terminateSystemAudioMode_systemAudioModeOn() throws Exception { mHdmiCecLocalDeviceAudioSystem.checkSupportAndSetSystemAudioMode(true); assertThat(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated()).isTrue(); - mMusicMute = false; + mAudioManager.setStreamMute(AudioManager.STREAM_MUSIC, false); HdmiCecMessage expectedMessage = HdmiCecMessageBuilder.buildSetSystemAudioMode( ADDR_AUDIO_SYSTEM, ADDR_BROADCAST, false); mHdmiCecLocalDeviceAudioSystem.terminateSystemAudioMode(); assertThat(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated()).isFalse(); - assertThat(mMusicMute).isTrue(); + assertThat(mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)).isTrue(); mTestLooper.dispatchAll(); assertThat(mNativeWrapper.getResultMessages()).contains(expectedMessage); } @@ -705,8 +653,6 @@ public class HdmiCecLocalDeviceAudioSystemTest { @Test public void giveAudioStatus_volumeEnabled() { - mMusicVolume = 50; - mMusicMaxVolume = 100; mHdmiControlService.setHdmiCecVolumeControlEnabledInternal( HdmiControlManager.VOLUME_CONTROL_ENABLED); mHdmiCecLocalDeviceAudioSystem.setSystemAudioControlFeatureEnabled(true); @@ -733,8 +679,6 @@ public class HdmiCecLocalDeviceAudioSystemTest { @Test public void giveAudioStatus_volumeDisabled() { - mMusicVolume = 50; - mMusicMaxVolume = 100; mHdmiControlService.setHdmiCecVolumeControlEnabledInternal( HdmiControlManager.VOLUME_CONTROL_DISABLED); mHdmiCecLocalDeviceAudioSystem.setSystemAudioControlFeatureEnabled(true); @@ -761,8 +705,6 @@ public class HdmiCecLocalDeviceAudioSystemTest { @Test public void reportAudioStatus_volumeEnabled() { - mMusicVolume = 50; - mMusicMaxVolume = 100; mHdmiControlService.setHdmiCecVolumeControlEnabledInternal( HdmiControlManager.VOLUME_CONTROL_ENABLED); mHdmiCecLocalDeviceAudioSystem.setSystemAudioControlFeatureEnabled(true); @@ -786,8 +728,6 @@ public class HdmiCecLocalDeviceAudioSystemTest { @Test public void reportAudioStatus_volumeDisabled() { - mMusicVolume = 50; - mMusicMaxVolume = 100; mHdmiControlService.setHdmiCecVolumeControlEnabledInternal( HdmiControlManager.VOLUME_CONTROL_DISABLED); mHdmiCecLocalDeviceAudioSystem.setSystemAudioControlFeatureEnabled(true); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java index beba9c64a88f..40c762c28194 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java @@ -31,7 +31,6 @@ import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.tv.cec.V1_0.SendMessageResult; -import android.media.AudioManager; import android.os.Looper; import android.os.RemoteException; import android.os.test.TestLooper; @@ -46,7 +45,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -88,8 +86,6 @@ public class HdmiCecLocalDevicePlaybackTest { private boolean mActiveMediaSessionsPaused; private FakePowerManagerInternalWrapper mPowerManagerInternal = new FakePowerManagerInternalWrapper(); - @Mock - protected AudioManager mAudioManager; @Before public void setUp() { @@ -98,10 +94,12 @@ public class HdmiCecLocalDevicePlaybackTest { Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), - new FakeAudioDeviceVolumeManagerWrapper()) { + audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { @Override void wakeUp() { @@ -110,11 +108,6 @@ public class HdmiCecLocalDevicePlaybackTest { } @Override - AudioManager getAudioManager() { - return mAudioManager; - } - - @Override void pauseActiveMediaSessions() { mActiveMediaSessionsPaused = true; } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java index 9c5c0d4dd66f..9882670432ae 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java @@ -33,6 +33,7 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -53,8 +54,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Arrays; @@ -128,18 +127,17 @@ public class HdmiCecLocalDeviceTest { private boolean isControlEnabled; private int mPowerStatus; - @Mock - private AudioManager mAudioManager; + private AudioManagerWrapper mAudioManager; @Before public void SetUp() { - MockitoAnnotations.initMocks(this); - Context context = InstrumentationRegistry.getTargetContext(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mAudioManager = spy(audioFramework.getAudioManager()); mHdmiControlService = new HdmiControlService(context, Collections.emptyList(), - new FakeAudioDeviceVolumeManagerWrapper()) { + mAudioManager, audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isCecControlEnabled() { return isControlEnabled; @@ -171,11 +169,6 @@ public class HdmiCecLocalDeviceTest { void wakeUp() { mWakeupMessageReceived = true; } - - @Override - AudioManager getAudioManager() { - return mAudioManager; - } }; mHdmiControlService.setIoLooper(mTestLooper.getLooper()); mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context)); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index d2b1bdd127d2..d52b7ea38725 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -33,9 +33,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.content.Context; import android.hardware.hdmi.HdmiControlManager; @@ -55,8 +55,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Collections; @@ -122,20 +120,21 @@ public class HdmiCecLocalDeviceTvTest { } } - @Mock - private AudioManager mAudioManager; + private FakeAudioFramework mAudioFramework; + private AudioManagerWrapper mAudioManager; @Before public void setUp() { - MockitoAnnotations.initMocks(this); - Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); + mAudioFramework = new FakeAudioFramework(); + mAudioManager = spy(mAudioFramework.getAudioManager()); + mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), - new FakeAudioDeviceVolumeManagerWrapper()) { + mAudioManager, mAudioFramework.getAudioDeviceVolumeManager()) { @Override void wakeUp() { mWokenUp = true; @@ -167,11 +166,6 @@ public class HdmiCecLocalDeviceTvTest { } @Override - AudioManager getAudioManager() { - return mAudioManager; - } - - @Override void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) { mDeviceEventListeners.add(new DeviceEventListener(device, status)); } @@ -967,7 +961,7 @@ public class HdmiCecLocalDeviceTvTest { @Test public void receiveSetAudioVolumeLevel_samNotActivated_noFeatureAbort_volumeChanges() { - when(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(25); + mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, 25); // Max volume of STREAM_MUSIC is retrieved on boot mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java index c53a7a708cfd..bdf3a5ffbd84 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java @@ -16,6 +16,11 @@ package com.android.server.hdmi; +import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; +import static com.android.server.hdmi.Constants.ADDR_BROADCAST; +import static com.android.server.hdmi.Constants.ADDR_RECORDER_1; +import static com.android.server.hdmi.Constants.ADDR_RECORDER_2; +import static com.android.server.hdmi.Constants.ADDR_RECORDER_3; import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_DESTINATION; import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_PARAMETER; import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_PARAMETER_LONG; @@ -38,7 +43,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import java.util.Arrays; import java.util.Collections; +import java.util.List; /** Tests for {@link com.android.server.hdmi.HdmiCecMessageValidator} class. */ @SmallTest @@ -51,9 +58,11 @@ public class HdmiCecMessageValidatorTest { @Before public void setUp() throws Exception { + FakeAudioFramework audioFramework = new FakeAudioFramework(); + HdmiControlService mHdmiControlService = new HdmiControlService( InstrumentationRegistry.getTargetContext(), Collections.emptyList(), - new FakeAudioDeviceVolumeManagerWrapper()); + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()); mHdmiControlService.setIoLooper(mTestLooper.getLooper()); } @@ -640,6 +649,22 @@ public class HdmiCecMessageValidatorTest { assertMessageValidity("4F:80:12:00:50:50").isEqualTo(ERROR_PARAMETER); } + @Test + public void isValid_activeSource() { + // Only source devices should broadcast <Active Source> messages. + List<Integer> nonSourceDevicesAddresses = Arrays.asList(ADDR_RECORDER_1, ADDR_RECORDER_2, + ADDR_AUDIO_SYSTEM, ADDR_RECORDER_3); + + for (int i = 0; i < ADDR_BROADCAST; ++i) { + String message = Integer.toHexString(i) + "F:82:10:00"; + if (nonSourceDevicesAddresses.contains(i)) { + assertMessageValidity(message).isEqualTo(ERROR_SOURCE); + } else { + assertMessageValidity(message).isEqualTo(OK); + } + } + } + private IntegerSubject assertMessageValidity(String message) { return assertThat(HdmiUtils.buildMessage(message).getValidationResult()); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java index d341153ac0ce..1ad9ce02daa3 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java @@ -67,8 +67,11 @@ public class HdmiCecNetworkTest { @Before public void setUp() throws Exception { mContext = InstrumentationRegistry.getTargetContext(); + + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(mContext, Collections.emptyList(), - new FakeAudioDeviceVolumeManagerWrapper()) { + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) { @Override void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) { mDeviceEventListenerStatuses.add(status); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java index 55e8b20ca7f0..c002067ae9e7 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java @@ -64,9 +64,11 @@ public class HdmiCecPowerStatusControllerTest { Context contextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); Looper myLooper = mTestLooper.getLooper(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(contextSpy, Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), - new FakeAudioDeviceVolumeManagerWrapper()) { + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isCecControlEnabled() { return true; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java index fd6eb9286651..0e6b412e330d 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -49,7 +49,6 @@ import android.hardware.hdmi.HdmiPortInfo; import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener; import android.hardware.hdmi.IHdmiControlStatusChangeListener; import android.hardware.hdmi.IHdmiVendorCommandListener; -import android.media.AudioManager; import android.os.Binder; import android.os.Looper; import android.os.RemoteException; @@ -64,9 +63,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Arrays; @@ -96,19 +93,17 @@ public class HdmiControlServiceTest { private HdmiPortInfo[] mHdmiPortInfo; private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>(); - @Mock protected AudioManager mAudioManager; - @Before public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy); mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, mLocalDeviceTypes, - new FakeAudioDeviceVolumeManagerWrapper())); + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager())); doNothing().when(mHdmiControlServiceSpy) .writeStringSystemProperty(anyString(), anyString()); @@ -171,7 +166,6 @@ public class HdmiControlServiceTest { mPowerManager = new FakePowerManagerWrapper(mContextSpy); mHdmiControlServiceSpy.setPowerManager(mPowerManager); mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); - mHdmiControlServiceSpy.setAudioManager(mAudioManager); mHdmiControlServiceSpy.setEarcSupported(true); mTestLooper.dispatchAll(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java index c3aec841ac3d..185f90f4e803 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java @@ -28,6 +28,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -35,7 +36,6 @@ import android.content.Context; import android.hardware.hdmi.HdmiDeviceInfo; import android.media.AudioDescriptor; import android.media.AudioDeviceAttributes; -import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -49,7 +49,6 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -78,8 +77,7 @@ public class HdmiEarcLocalDeviceTxTest { private Looper mMyLooper; private TestLooper mTestLooper = new TestLooper(); - @Mock - private AudioManager mAudioManager; + private AudioManagerWrapper mAudioManager; @Captor ArgumentCaptor<AudioDeviceAttributes> mAudioAttributesCaptor; @@ -91,10 +89,13 @@ public class HdmiEarcLocalDeviceTxTest { Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mAudioManager = spy(audioFramework.getAudioManager()); + mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), - new FakeAudioDeviceVolumeManagerWrapper()) { + mAudioManager, audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isCecControlEnabled() { return true; @@ -114,11 +115,6 @@ public class HdmiEarcLocalDeviceTxTest { boolean isPowerStandby() { return false; } - - @Override - AudioManager getAudioManager() { - return mAudioManager; - } }; mHdmiControlService.setIoLooper(mMyLooper); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java index b0e8ca75d2d8..1172a8744da7 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java @@ -31,7 +31,6 @@ import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.tv.cec.V1_0.SendMessageResult; -import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -88,20 +87,11 @@ public class OneTouchPlayActionTest { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); mHdmiCecConfig = new FakeHdmiCecConfig(mContextSpy); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(mContextSpy, Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), - new FakeAudioDeviceVolumeManagerWrapper()) { - @Override - AudioManager getAudioManager() { - return new AudioManager() { - @Override - public void setWiredDeviceConnectionState( - int type, int state, String address, String name) { - // Do nothing. - } - }; - } - + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isPowerStandby() { return false; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvbTest.java index 64186028e6ed..43ab804e04be 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvbTest.java @@ -16,10 +16,12 @@ package com.android.server.hdmi; +import static com.google.common.truth.Truth.assertThat; + import android.hardware.hdmi.DeviceFeatures; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; -import android.media.AudioDeviceAttributes; +import android.media.AudioManager; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; @@ -33,33 +35,13 @@ import org.junit.runners.JUnit4; import java.util.Arrays; /** - * Tests for Absolute Volume Control where the local device is a Playback device and the + * Tests for absolute volume behavior where the local device is a Playback device and the * System Audio device is an Audio System. */ @SmallTest @Presubmit @RunWith(JUnit4.class) -public class PlaybackDeviceToAudioSystemAvcTest extends BaseAbsoluteVolumeControlTest { - - @Override - protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) { - return new HdmiCecLocalDevicePlayback(hdmiControlService); - } - - @Override - protected int getPhysicalAddress() { - return 0x1100; - } - - @Override - protected int getDeviceType() { - return HdmiDeviceInfo.DEVICE_PLAYBACK; - } - - @Override - protected AudioDeviceAttributes getAudioOutputDevice() { - return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI; - } +public class PlaybackDeviceToAudioSystemAvbTest extends BasePlaybackDeviceAvbTest { @Override protected int getSystemAudioDeviceLogicalAddress() { @@ -72,17 +54,18 @@ public class PlaybackDeviceToAudioSystemAvcTest extends BaseAbsoluteVolumeContro } /** - * AVC is disabled if the Audio System disables System Audio mode, and the TV has unknown + * AVB is disabled if the Audio System disables System Audio mode, and the TV has unknown * support for <Set Audio Volume Level>. It is enabled once the TV confirms support for * <Set Audio Volume Level> and sends <Report Audio Status>. */ @Test public void switchToTv_absoluteVolumeControlDisabledUntilAllConditionsMet() { - enableAbsoluteVolumeControl(); + enableAbsoluteVolumeBehavior(); - // Audio System disables System Audio Mode. AVC should be disabled. + // Audio System disables System Audio Mode. AVB should be disabled. receiveSetSystemAudioMode(false); - verifyAbsoluteVolumeDisabled(); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); // TV reports support for <Set Audio Volume Level> mNativeWrapper.onCecMessage(ReportFeaturesMessage.build( @@ -102,6 +85,7 @@ public class PlaybackDeviceToAudioSystemAvcTest extends BaseAbsoluteVolumeContro false)); mTestLooper.dispatchAll(); - verifyAbsoluteVolumeEnabled(); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvbTest.java index 504c3bc2626a..9b343e34706a 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvbTest.java @@ -16,12 +16,14 @@ package com.android.server.hdmi; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.clearInvocations; import android.hardware.hdmi.DeviceFeatures; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; -import android.media.AudioDeviceAttributes; +import android.media.AudioManager; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; @@ -34,33 +36,13 @@ import java.util.Arrays; import java.util.Collections; /** - * Tests for Absolute Volume Control where the local device is a Playback device and the + * Tests for absolute volume behavior where the local device is a Playback device and the * System Audio device is a TV. */ @SmallTest @Presubmit @RunWith(JUnit4.class) -public class PlaybackDeviceToTvAvcTest extends BaseAbsoluteVolumeControlTest { - - @Override - protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) { - return new HdmiCecLocalDevicePlayback(hdmiControlService); - } - - @Override - protected int getPhysicalAddress() { - return 0x1100; - } - - @Override - protected int getDeviceType() { - return HdmiDeviceInfo.DEVICE_PLAYBACK; - } - - @Override - protected AudioDeviceAttributes getAudioOutputDevice() { - return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI; - } +public class PlaybackDeviceToTvAvbTest extends BasePlaybackDeviceAvbTest { @Override protected int getSystemAudioDeviceLogicalAddress() { @@ -73,17 +55,18 @@ public class PlaybackDeviceToTvAvcTest extends BaseAbsoluteVolumeControlTest { } /** - * AVC is disabled when an Audio System with unknown support for <Set Audio Volume Level> + * AVB is disabled when an Audio System with unknown support for <Set Audio Volume Level> * becomes the System Audio device. It is enabled once the Audio System reports that it * supports <Set Audio Volume Level> and sends <Report Audio Status>. */ @Test public void switchToAudioSystem_absoluteVolumeControlDisabledUntilAllConditionsMet() { - enableAbsoluteVolumeControl(); + enableAbsoluteVolumeBehavior(); - // Audio System enables System Audio Mode. AVC should be disabled. + // Audio System enables System Audio Mode. AVB should be disabled. receiveSetSystemAudioMode(true); - verifyAbsoluteVolumeDisabled(); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); clearInvocations(mAudioManager, mAudioDeviceVolumeManager); @@ -105,6 +88,7 @@ public class PlaybackDeviceToTvAvcTest extends BaseAbsoluteVolumeControlTest { false)); mTestLooper.dispatchAll(); - verifyAbsoluteVolumeEnabled(); + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java index 89743cdeabf5..9f0a44ce008a 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java @@ -30,7 +30,6 @@ import android.content.ContextWrapper; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; -import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -65,20 +64,11 @@ public class PowerStatusMonitorActionTest { public void setUp() throws Exception { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(mContextSpy, Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), - new FakeAudioDeviceVolumeManagerWrapper()) { - @Override - AudioManager getAudioManager() { - return new AudioManager() { - @Override - public void setWiredDeviceConnectionState( - int type, int state, String address, String name) { - // Do nothing. - } - }; - } - + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isPowerStandby() { return false; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java index 1c193411b932..043db1eb298d 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java @@ -95,9 +95,12 @@ public class RequestSadActionTest { Context context = InstrumentationRegistry.getTargetContext(); mMyLooper = mTestLooper.getLooper(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(context, Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), - new FakeAudioDeviceVolumeManagerWrapper()) { + audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isCecControlEnabled() { return true; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java index 5b1bdf6916da..1bc99b6b8ffb 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java @@ -146,10 +146,13 @@ public class RoutingControlActionTest { HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(context); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(InstrumentationRegistry.getTargetContext(), Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), - new FakeAudioDeviceVolumeManagerWrapper()) { + audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isCecControlEnabled() { return true; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java index cac781509a93..a73f4aa35cf9 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java @@ -80,9 +80,11 @@ public class SetAudioVolumeLevelDiscoveryActionTest { mContextSpy = spy(new ContextWrapper( InstrumentationRegistry.getInstrumentation().getTargetContext())); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK), - new FakeAudioDeviceVolumeManagerWrapper())); + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager())); doNothing().when(mHdmiControlServiceSpy) .writeStringSystemProperty(anyString(), anyString()); @@ -152,11 +154,11 @@ public class SetAudioVolumeLevelDiscoveryActionTest { mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); mTestLooper.dispatchAll(); - @DeviceFeatures.FeatureSupportStatus int avcSupport = + @DeviceFeatures.FeatureSupportStatus int savlSupport = mHdmiControlServiceSpy.getHdmiCecNetwork().getCecDeviceInfo(Constants.ADDR_TV) .getDeviceFeatures().getSetAudioVolumeLevelSupport(); - assertThat(avcSupport).isEqualTo(FEATURE_SUPPORTED); + assertThat(savlSupport).isEqualTo(FEATURE_SUPPORTED); assertThat(mTestCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); } @@ -172,11 +174,11 @@ public class SetAudioVolumeLevelDiscoveryActionTest { Constants.ABORT_UNRECOGNIZED_OPCODE)); mTestLooper.dispatchAll(); - @DeviceFeatures.FeatureSupportStatus int avcSupport = + @DeviceFeatures.FeatureSupportStatus int savlSupport = mHdmiControlServiceSpy.getHdmiCecNetwork().getCecDeviceInfo(Constants.ADDR_TV) .getDeviceFeatures().getSetAudioVolumeLevelSupport(); - assertThat(avcSupport).isEqualTo(FEATURE_NOT_SUPPORTED); + assertThat(savlSupport).isEqualTo(FEATURE_NOT_SUPPORTED); assertThat(mTestCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS); } @@ -189,11 +191,11 @@ public class SetAudioVolumeLevelDiscoveryActionTest { mPlaybackDevice.addAndStartAction(mAction); mTestLooper.dispatchAll(); - @DeviceFeatures.FeatureSupportStatus int avcSupport = + @DeviceFeatures.FeatureSupportStatus int savlSupport = mHdmiControlServiceSpy.getHdmiCecNetwork().getCecDeviceInfo(Constants.ADDR_TV) .getDeviceFeatures().getSetAudioVolumeLevelSupport(); - assertThat(avcSupport).isEqualTo(FEATURE_SUPPORT_UNKNOWN); + assertThat(savlSupport).isEqualTo(FEATURE_SUPPORT_UNKNOWN); assertThat(mTestCallback.getResult()).isEqualTo( HdmiControlManager.RESULT_COMMUNICATION_FAILED); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java index c40cd0eeaf7e..c3beff74ca1c 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java @@ -29,7 +29,6 @@ import android.content.Context; import android.content.ContextWrapper; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; -import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -68,20 +67,11 @@ public class SystemAudioAutoInitiationActionTest { Looper myLooper = mTestLooper.getLooper(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + mHdmiControlService = new HdmiControlService(mContextSpy, Collections.singletonList(HdmiDeviceInfo.DEVICE_TV), - new FakeAudioDeviceVolumeManagerWrapper()) { - @Override - AudioManager getAudioManager() { - return new AudioManager() { - @Override - public void setWiredDeviceConnectionState( - int type, int state, String address, String name) { - // Do nothing. - } - }; - } - + audioFramework.getAudioManager(), audioFramework.getAudioDeviceVolumeManager()) { @Override boolean isPowerStandby() { return false; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java index b13ef4fa7014..f801f8853980 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java @@ -24,7 +24,6 @@ import android.annotation.Nullable; import android.content.Context; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.tv.cec.V1_0.SendMessageResult; -import android.media.AudioManager; import android.os.Looper; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -68,8 +67,11 @@ public class SystemAudioInitiationActionFromAvrTest { Context context = InstrumentationRegistry.getTargetContext(); + FakeAudioFramework audioFramework = new FakeAudioFramework(); + HdmiControlService hdmiControlService = new HdmiControlService(context, - Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) { + Collections.emptyList(), audioFramework.getAudioManager(), + audioFramework.getAudioDeviceVolumeManager()) { @Override void sendCecCommand( HdmiCecMessage command, @Nullable SendMessageCallback callback) { @@ -100,36 +102,6 @@ public class SystemAudioInitiationActionFromAvrTest { } @Override - AudioManager getAudioManager() { - return new AudioManager() { - - @Override - public int setHdmiSystemAudioSupported(boolean on) { - return 0; - } - - @Override - public int getStreamVolume(int streamType) { - return 0; - } - - @Override - public boolean isStreamMute(int streamType) { - return false; - } - - @Override - public int getStreamMaxVolume(int streamType) { - return 100; - } - - @Override - public void adjustStreamVolume( - int streamType, int direction, int flags) {} - }; - } - - @Override boolean isPowerStandby() { return false; } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java new file mode 100644 index 000000000000..079ef2e36673 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.hdmi; + +import static com.android.server.hdmi.HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.hardware.hdmi.DeviceFeatures; +import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.HdmiDeviceInfo; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceVolumeManager; +import android.media.AudioManager; +import android.media.VolumeInfo; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for absolute volume behavior where the local device is a TV and the System Audio device + * is an Audio System. Assumes that the TV uses ARC (rather than eARC). + */ +@SmallTest +@Presubmit +@RunWith(JUnit4.class) +public class TvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehaviorTest { + + @Override + protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) { + return new HdmiCecLocalDeviceTv(hdmiControlService); + } + + @Override + protected int getPhysicalAddress() { + return 0x0000; + } + + @Override + protected int getDeviceType() { + return HdmiDeviceInfo.DEVICE_TV; + } + + @Override + protected AudioDeviceAttributes getAudioOutputDevice() { + return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI_ARC; + } + + @Override + protected int getSystemAudioDeviceLogicalAddress() { + return Constants.ADDR_AUDIO_SYSTEM; + } + + @Override + protected int getSystemAudioDeviceType() { + return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM; + } + + /** + * TVs start the process for adopting adjust-only AVB if the System Audio device doesn't + * support <Set Audio Volume Level> + */ + @Test + public void savlNotSupported_allOtherConditionsMet_giveAudioStatusSent() { + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); + enableSystemAudioModeIfNeeded(); + verifyGiveAudioStatusNeverSent(); + + receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED); + verifyGiveAudioStatusSent(); + } + + @Test + public void savlNotSupported_systemAudioDeviceSendsReportAudioStatus_adjustOnlyAvbEnabled() { + mAudioManager.setDeviceVolumeBehavior(getAudioOutputDevice(), + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED); + enableSystemAudioModeIfNeeded(); + receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED); + + // Adjust-only AVB should not be enabled before receiving <Report Audio Status> + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL); + + receiveReportAudioStatus(20, false); + + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); + + verify(mAudioDeviceVolumeManager).setDeviceAbsoluteVolumeAdjustOnlyBehavior( + eq(getAudioOutputDevice()), + eq(new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setVolumeIndex(20) + .setMuted(false) + .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) + .setMinVolumeIndex(AudioStatus.MIN_VOLUME) + .build()), + any(), any(), anyBoolean()); + } + + + @Test + public void avbEnabled_savlNotSupported_receiveReportAudioStatus_switchToAdjustOnlyAvb() { + enableAbsoluteVolumeBehavior(); + + receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED); + + receiveReportAudioStatus(40, true); + + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); + + verify(mAudioDeviceVolumeManager).setDeviceAbsoluteVolumeAdjustOnlyBehavior( + eq(getAudioOutputDevice()), + eq(new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setVolumeIndex(40) + .setMuted(true) + .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) + .setMinVolumeIndex(AudioStatus.MIN_VOLUME) + .build()), + any(), any(), anyBoolean()); + } + + @Test + public void avbEnabled_savlFeatureAborted_receiveReportAudioStatus_switchToAdjustOnlyAvb() { + enableAbsoluteVolumeBehavior(); + + mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildFeatureAbortCommand( + getSystemAudioDeviceLogicalAddress(), getLogicalAddress(), + Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL, Constants.ABORT_UNRECOGNIZED_OPCODE)); + mTestLooper.dispatchAll(); + + receiveReportAudioStatus(40, true); + + assertThat(mAudioManager.getDeviceVolumeBehavior(getAudioOutputDevice())).isEqualTo( + AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); + + verify(mAudioDeviceVolumeManager).setDeviceAbsoluteVolumeAdjustOnlyBehavior( + eq(getAudioOutputDevice()), + eq(new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setVolumeIndex(40) + .setMuted(true) + .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) + .setMinVolumeIndex(AudioStatus.MIN_VOLUME) + .build()), + any(), any(), anyBoolean()); + } + + @Test + public void adjustOnlyAvbEnabled_receiveReportAudioStatus_notifiesVolumeOrMuteChanges() { + enableAdjustOnlyAbsoluteVolumeBehavior(); + + // New volume and mute status: sets both + receiveReportAudioStatus(20, true); + verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(5), + anyInt()); + verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC), + eq(AudioManager.ADJUST_MUTE), anyInt()); + clearInvocations(mAudioManager); + + // New volume only: sets volume only + receiveReportAudioStatus(32, true); + verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8), + anyInt()); + verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC), + eq(AudioManager.ADJUST_MUTE), anyInt()); + clearInvocations(mAudioManager); + + // New mute status only: sets mute only + receiveReportAudioStatus(32, false); + verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8), + anyInt()); + verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC), + eq(AudioManager.ADJUST_UNMUTE), anyInt()); + clearInvocations(mAudioManager); + + // Repeat of earlier message: sets neither volume nor mute + receiveReportAudioStatus(32, false); + verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8), + anyInt()); + verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC), + eq(AudioManager.ADJUST_UNMUTE), anyInt()); + + // Volume not within range [0, 100]: sets neither volume nor mute + receiveReportAudioStatus(127, true); + verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(), + anyInt()); + verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(), + anyInt()); + } + + @Test + public void adjustOnlyAvbEnabled_audioDeviceVolumeAdjusted_sendsUcpAndGiveAudioStatus() { + enableAdjustOnlyAbsoluteVolumeBehavior(); + mNativeWrapper.clearResultMessages(); + + mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted( + getAudioOutputDevice(), + new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) + .setMinVolumeIndex(AudioStatus.MIN_VOLUME) + .build(), + AudioManager.ADJUST_RAISE, + AudioDeviceVolumeManager.ADJUST_MODE_NORMAL + ); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains( + HdmiCecMessageBuilder.buildUserControlPressed(getLogicalAddress(), + getSystemAudioDeviceLogicalAddress(), CEC_KEYCODE_VOLUME_UP)); + assertThat(mNativeWrapper.getResultMessages()).contains( + HdmiCecMessageBuilder.buildUserControlReleased(getLogicalAddress(), + getSystemAudioDeviceLogicalAddress())); + assertThat(mNativeWrapper.getResultMessages()).contains( + HdmiCecMessageBuilder.buildGiveAudioStatus(getLogicalAddress(), + getSystemAudioDeviceLogicalAddress())); + } + + @Test + public void adjustOnlyAvbEnabled_audioDeviceVolumeChanged_doesNotSendSetAudioVolumeLevel() { + enableAdjustOnlyAbsoluteVolumeBehavior(); + + mNativeWrapper.clearResultMessages(); + + mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged( + getAudioOutputDevice(), + new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setVolumeIndex(20) + .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) + .setMinVolumeIndex(AudioStatus.MIN_VOLUME) + .build() + ); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).isEmpty(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java deleted file mode 100644 index 41c0e0d29879..000000000000 --- a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.hdmi; - -import android.hardware.hdmi.HdmiDeviceInfo; -import android.media.AudioDeviceAttributes; -import android.platform.test.annotations.Presubmit; - -import androidx.test.filters.SmallTest; - -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * Tests for Absolute Volume Control where the local device is a TV and the System Audio device - * is an Audio System. Assumes that the TV uses ARC (rather than eARC). - */ -@SmallTest -@Presubmit -@RunWith(JUnit4.class) -public class TvToAudioSystemAvcTest extends BaseAbsoluteVolumeControlTest { - - @Override - protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) { - return new HdmiCecLocalDeviceTv(hdmiControlService); - } - - @Override - protected int getPhysicalAddress() { - return 0x0000; - } - - @Override - protected int getDeviceType() { - return HdmiDeviceInfo.DEVICE_TV; - } - - @Override - protected AudioDeviceAttributes getAudioOutputDevice() { - return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI_ARC; - } - - @Override - protected int getSystemAudioDeviceLogicalAddress() { - return Constants.ADDR_AUDIO_SYSTEM; - } - - @Override - protected int getSystemAudioDeviceType() { - return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM; - } -} diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index 275533fb1c37..b91a6cbf05a0 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -139,7 +139,7 @@ public class MediaProjectionManagerServiceTest { doReturn(mPackageManager).when(mContext).getPackageManager(); mClock = new OffsettableClock.Stopped(); - mWaitingDisplaySession.setWaitingToRecord(true); + mWaitingDisplaySession.setWaitingForConsent(true); mWaitingDisplaySession.setVirtualDisplayId(5); mAppInfo.targetSdkVersion = 32; @@ -484,7 +484,7 @@ public class MediaProjectionManagerServiceTest { mSessionCaptor.capture()); // Examine latest value. final ContentRecordingSession capturedSession = mSessionCaptor.getValue(); - assertThat(capturedSession.isWaitingToRecord()).isFalse(); + assertThat(capturedSession.isWaitingForConsent()).isFalse(); assertThat(capturedSession.getVirtualDisplayId()).isEqualTo(INVALID_DISPLAY); } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java index 20e2692cb747..bfd407216c3b 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java @@ -27,6 +27,7 @@ import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_FINISH; import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_NONE; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_GRANT_ADMIN; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_CREATE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_LIFECYCLE; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_REMOVE; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_START; import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_STOP; @@ -499,6 +500,27 @@ public class UserJourneyLoggerTest { 0x00000402, ERROR_CODE_UNSPECIFIED, 3); } + @Test + public void testUserLifecycleJourney() { + final long startTime = System.currentTimeMillis(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .startSessionForDelayedJourney(10, USER_JOURNEY_USER_LIFECYCLE, startTime); + + + final UserLifecycleJourneyReportedCaptor report = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logDelayedUserJourneyFinishWithError(0, targetUser, + USER_JOURNEY_USER_LIFECYCLE, ERROR_CODE_UNSPECIFIED); + + + report.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_LIFECYCLE, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + assertThat(report.mElapsedTime.getValue() > 0L).isTrue(); + } + static class UserLifecycleJourneyReportedCaptor { ArgumentCaptor<Long> mSessionId = ArgumentCaptor.forClass(Long.class); ArgumentCaptor<Integer> mJourney = ArgumentCaptor.forClass(Integer.class); @@ -507,6 +529,7 @@ public class UserJourneyLoggerTest { ArgumentCaptor<Integer> mUserType = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<Integer> mUserFlags = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<Integer> mErrorCode = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Long> mElapsedTime = ArgumentCaptor.forClass(Long.class); public void captureAndAssert(UserJourneyLogger mUserJourneyLogger, long sessionId, int journey, int originalUserId, @@ -518,7 +541,8 @@ public class UserJourneyLoggerTest { mTargetUserId.capture(), mUserType.capture(), mUserFlags.capture(), - mErrorCode.capture()); + mErrorCode.capture(), + mElapsedTime.capture()); assertThat(mSessionId.getValue()).isEqualTo(sessionId); assertThat(mJourney.getValue()).isEqualTo(journey); @@ -577,4 +601,4 @@ public class UserJourneyLoggerTest { state, errorCode, 1); } } -} +}
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java index de82854d42ee..b2843d82a08a 100644 --- a/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java @@ -422,8 +422,7 @@ public class DexMetadataHelperTest { null /* splitNames */, null /* isFeatureSplits */, null /* usesSplitNames */, null /* configForSplit */, null /* splitApkPaths */, null /* splitRevisionCodes */, baseApk.getTargetSdkVersion(), - null /* requiredSplitTypes */, null /* splitTypes */, - false /* allowUpdateOwnership */); + null /* requiredSplitTypes */, null /* splitTypes */); Assert.assertEquals(dm.length(), DexMetadataHelper.getPackageDexMetadataSize(pkgLite)); } diff --git a/services/tests/servicestests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java b/services/tests/servicestests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java index 76b6a820e4a7..d1814199a0b1 100644 --- a/services/tests/servicestests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java +++ b/services/tests/servicestests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java @@ -23,8 +23,6 @@ import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_UNKNOWN; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_WIFI; -import static com.android.server.power.stats.wakeups.CpuWakeupStats.WAKEUP_REASON_HALF_WINDOW_MS; - import static com.google.common.truth.Truth.assertThat; import android.content.Context; @@ -55,7 +53,7 @@ public class CpuWakeupStatsTest { private static final String KERNEL_REASON_SENSOR_IRQ = "15 test.sensor.device"; private static final String KERNEL_REASON_CELLULAR_DATA_IRQ = "18 test.cellular_data.device"; private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device"; - private static final String KERNEL_REASON_UNKNOWN = "free-form-reason test.alarm.device"; + private static final String KERNEL_REASON_UNKNOWN_FORMAT = "free-form-reason test.alarm.device"; private static final String KERNEL_REASON_ALARM_ABNORMAL = "-1 test.alarm.device"; private static final String KERNEL_REASON_ABORT = "Abort: due to test.alarm.device"; @@ -85,30 +83,29 @@ public class CpuWakeupStatsTest { @Test public void removesOldWakeups() { - // The xml resource doesn't matter for this test. - final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_1, mHandler); + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long retention = obj.mConfig.WAKEUP_STATS_RETENTION_MS; final Set<Long> timestamps = new HashSet<>(); final long firstWakeup = 453192; - obj.noteWakeupTimeAndReason(firstWakeup, 32, KERNEL_REASON_UNKNOWN_IRQ); + // Reasons do not matter for this test, as long as they map to known subsystems + obj.noteWakeupTimeAndReason(firstWakeup, 32, KERNEL_REASON_ALARM_IRQ); timestamps.add(firstWakeup); for (int i = 1; i < 1000; i++) { final long delta = mRandom.nextLong(retention); if (timestamps.add(firstWakeup + delta)) { - obj.noteWakeupTimeAndReason(firstWakeup + delta, i, KERNEL_REASON_UNKNOWN_IRQ); + obj.noteWakeupTimeAndReason(firstWakeup + delta, i, KERNEL_REASON_SENSOR_IRQ); } } assertThat(obj.mWakeupEvents.size()).isEqualTo(timestamps.size()); - obj.noteWakeupTimeAndReason(firstWakeup + retention + 1242, 231, - KERNEL_REASON_UNKNOWN_IRQ); + obj.noteWakeupTimeAndReason(firstWakeup + retention, 231, KERNEL_REASON_WIFI_IRQ); assertThat(obj.mWakeupEvents.size()).isEqualTo(timestamps.size()); for (int i = 0; i < 100; i++) { final long now = mRandom.nextLong(retention + 1, 100 * retention); - obj.noteWakeupTimeAndReason(now, i, KERNEL_REASON_UNKNOWN_IRQ); + obj.noteWakeupTimeAndReason(now, i, KERNEL_REASON_SOUND_TRIGGER_IRQ); assertThat(obj.mWakeupEvents.closestIndexOnOrBefore(now - retention)).isLessThan(0); } } @@ -201,9 +198,9 @@ public class CpuWakeupStatsTest { // Outside the window, so should be ignored. obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, - wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_1); + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, - wakeupTime + WAKEUP_REASON_HALF_WINDOW_MS + 1, TEST_UID_2); + wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2); // Should be attributed obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3, TEST_UID_5); @@ -234,9 +231,9 @@ public class CpuWakeupStatsTest { // Outside the window, so should be ignored. obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, - wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_1); + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, - wakeupTime + WAKEUP_REASON_HALF_WINDOW_MS + 1, TEST_UID_2); + wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2); // Should be attributed obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, wakeupTime + 5, TEST_UID_3, TEST_UID_5); @@ -268,9 +265,9 @@ public class CpuWakeupStatsTest { // Outside the window, so should be ignored. obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, - wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_1); + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, - wakeupTime + WAKEUP_REASON_HALF_WINDOW_MS + 1, TEST_UID_2); + wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2); // Should be attributed obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime + 3, TEST_UID_4, TEST_UID_5); @@ -300,9 +297,9 @@ public class CpuWakeupStatsTest { // Alarm activity // Outside the window, so should be ignored. obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, - wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_1); + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, - wakeupTime + WAKEUP_REASON_HALF_WINDOW_MS + 1, TEST_UID_2); + wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2); // Should be attributed obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4, @@ -311,9 +308,9 @@ public class CpuWakeupStatsTest { // Wifi activity // Outside the window, so should be ignored. obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, - wakeupTime - WAKEUP_REASON_HALF_WINDOW_MS - 1, TEST_UID_4); + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_4); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, - wakeupTime + WAKEUP_REASON_HALF_WINDOW_MS + 1, TEST_UID_3); + wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_3); // Should be attributed obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime + 2, TEST_UID_1); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime - 1, TEST_UID_2, @@ -347,33 +344,67 @@ public class CpuWakeupStatsTest { } @Test - public void unknownIrqAttribution() { + public void unknownIrqSoloIgnored() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 92123410; obj.noteWakeupTimeAndReason(wakeupTime, 24, KERNEL_REASON_UNKNOWN_IRQ); - assertThat(obj.mWakeupEvents.size()).isEqualTo(1); + assertThat(obj.mWakeupEvents.size()).isEqualTo(0); - // Unrelated subsystems, should not be attributed obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime - 3, TEST_UID_4, TEST_UID_5); + // Any nearby activity should not end up in the attribution map. + assertThat(obj.mWakeupAttribution.size()).isEqualTo(0); + } + + @Test + public void unknownAndWifiAttribution() { + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); + final long wakeupTime = 92123410; + + populateDefaultProcStates(obj); + + obj.noteWakeupTimeAndReason(wakeupTime, 24, + KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_WIFI_IRQ); + + // Wifi activity + // Outside the window, so should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_4); + // Should be attributed + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime + 2, TEST_UID_1); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_WIFI, wakeupTime - 1, TEST_UID_3, + TEST_UID_5); + + // Unrelated, should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); + final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime); assertThat(attribution).isNotNull(); - assertThat(attribution.size()).isEqualTo(1); + assertThat(attribution.size()).isEqualTo(2); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_WIFI)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_1)).isEqualTo( + TEST_PROC_STATE_1); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).indexOfKey(TEST_UID_2)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_3)).isEqualTo( + TEST_PROC_STATE_3); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).indexOfKey(TEST_UID_4)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_WIFI).get(TEST_UID_5)).isEqualTo( + TEST_PROC_STATE_5); assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue(); - final SparseIntArray uidProcStates = attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN); - assertThat(uidProcStates == null || uidProcStates.size() == 0).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isNull(); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isFalse(); } @Test - public void unknownWakeupIgnored() { + public void unknownFormatWakeupIgnored() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 72123210; - obj.noteWakeupTimeAndReason(wakeupTime, 34, KERNEL_REASON_UNKNOWN); + obj.noteWakeupTimeAndReason(wakeupTime, 34, KERNEL_REASON_UNKNOWN_FORMAT); // Should be ignored as this type of wakeup is not known. assertThat(obj.mWakeupEvents.size()).isEqualTo(0); diff --git a/services/tests/servicestests/src/com/android/server/power/stats/wakeups/WakingActivityHistoryTest.java b/services/tests/servicestests/src/com/android/server/power/stats/wakeups/WakingActivityHistoryTest.java index cba7dbe2d02b..b02618e97b11 100644 --- a/services/tests/servicestests/src/com/android/server/power/stats/wakeups/WakingActivityHistoryTest.java +++ b/services/tests/servicestests/src/com/android/server/power/stats/wakeups/WakingActivityHistoryTest.java @@ -28,8 +28,11 @@ import com.android.server.power.stats.wakeups.CpuWakeupStats.WakingActivityHisto import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.ThreadLocalRandom; + @RunWith(AndroidJUnit4.class) public class WakingActivityHistoryTest { + private volatile long mTestRetention = 54; private static boolean areSame(SparseIntArray a, SparseIntArray b) { if (a == b) { @@ -55,7 +58,7 @@ public class WakingActivityHistoryTest { @Test public void recordActivityAppendsUids() { - final WakingActivityHistory history = new WakingActivityHistory(); + final WakingActivityHistory history = new WakingActivityHistory(() -> Long.MAX_VALUE); final int subsystem = 42; final long timestamp = 54; @@ -100,7 +103,7 @@ public class WakingActivityHistoryTest { @Test public void recordActivityDoesNotDeleteExistingUids() { - final WakingActivityHistory history = new WakingActivityHistory(); + final WakingActivityHistory history = new WakingActivityHistory(() -> Long.MAX_VALUE); final int subsystem = 42; long timestamp = 101; @@ -151,4 +154,120 @@ public class WakingActivityHistoryTest { assertThat(recordedUids.get(62, -1)).isEqualTo(31); assertThat(recordedUids.get(85, -1)).isEqualTo(39); } + + @Test + public void removeBetween() { + final WakingActivityHistory history = new WakingActivityHistory(() -> Long.MAX_VALUE); + + final int subsystem = 43; + + final SparseIntArray uids = new SparseIntArray(); + uids.put(1, 17); + uids.put(15, 2); + uids.put(62, 31); + history.recordActivity(subsystem, 123, uids); + + uids.put(54, 91); + history.recordActivity(subsystem, 150, uids); + + uids.put(101, 32); + uids.delete(1); + history.recordActivity(subsystem, 191, uids); + + SparseIntArray removedUids = history.removeBetween(subsystem, 100, 122); + assertThat(removedUids).isNull(); + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(3); + + removedUids = history.removeBetween(subsystem, 124, 149); + assertThat(removedUids).isNull(); + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(3); + + removedUids = history.removeBetween(subsystem, 151, 190); + assertThat(removedUids).isNull(); + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(3); + + removedUids = history.removeBetween(subsystem, 192, 240); + assertThat(removedUids).isNull(); + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(3); + + + // Removing from a different subsystem should do nothing. + removedUids = history.removeBetween(subsystem + 1, 0, 300); + assertThat(removedUids).isNull(); + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(3); + + removedUids = history.removeBetween(subsystem, 0, 300); + assertThat(removedUids.size()).isEqualTo(5); + assertThat(removedUids.get(1, -1)).isEqualTo(17); + assertThat(removedUids.get(15, -1)).isEqualTo(2); + assertThat(removedUids.get(62, -1)).isEqualTo(31); + assertThat(removedUids.get(54, -1)).isEqualTo(91); + assertThat(removedUids.get(101, -1)).isEqualTo(32); + + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(0); + + history.recordActivity(subsystem, 23, uids); + uids.put(31, 123); + history.recordActivity(subsystem, 49, uids); + uids.put(177, 432); + history.recordActivity(subsystem, 89, uids); + + removedUids = history.removeBetween(subsystem, 23, 23); + assertThat(removedUids.size()).isEqualTo(4); + assertThat(removedUids.get(15, -1)).isEqualTo(2); + assertThat(removedUids.get(62, -1)).isEqualTo(31); + assertThat(removedUids.get(54, -1)).isEqualTo(91); + assertThat(removedUids.get(101, -1)).isEqualTo(32); + + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(2); + + removedUids = history.removeBetween(subsystem, 49, 54); + assertThat(removedUids.size()).isEqualTo(5); + assertThat(removedUids.get(15, -1)).isEqualTo(2); + assertThat(removedUids.get(62, -1)).isEqualTo(31); + assertThat(removedUids.get(54, -1)).isEqualTo(91); + assertThat(removedUids.get(101, -1)).isEqualTo(32); + assertThat(removedUids.get(31, -1)).isEqualTo(123); + + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(1); + + removedUids = history.removeBetween(subsystem, 23, 89); + assertThat(removedUids.size()).isEqualTo(6); + assertThat(removedUids.get(15, -1)).isEqualTo(2); + assertThat(removedUids.get(62, -1)).isEqualTo(31); + assertThat(removedUids.get(54, -1)).isEqualTo(91); + assertThat(removedUids.get(101, -1)).isEqualTo(32); + assertThat(removedUids.get(31, -1)).isEqualTo(123); + assertThat(removedUids.get(177, -1)).isEqualTo(432); + + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(0); + } + + @Test + public void deletesActivityPastRetention() { + final WakingActivityHistory history = new WakingActivityHistory(() -> mTestRetention); + final int subsystem = 49; + + mTestRetention = 454; + + final long firstTime = 342; + for (int i = 0; i < mTestRetention; i++) { + history.recordActivity(subsystem, firstTime + i, new SparseIntArray()); + } + assertThat(history.mWakingActivity.get(subsystem)).isNotNull(); + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(mTestRetention); + + history.recordActivity(subsystem, firstTime + mTestRetention + 7, new SparseIntArray()); + assertThat(history.mWakingActivity.get(subsystem).size()).isEqualTo(mTestRetention - 7); + + final ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (int i = 0; i < 100; i++) { + final long time = random.nextLong(firstTime + mTestRetention + 100, + 456 * mTestRetention); + history.recordActivity(subsystem, time, new SparseIntArray()); + assertThat(history.mWakingActivity.get(subsystem).closestIndexOnOrBefore( + time - mTestRetention)).isLessThan(0); + } + } } 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 025315ec42e6..6ef81f6c6211 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -2194,6 +2194,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { any(), anyString(), anyInt(), anyString(), anyInt())) .thenReturn(SHOW_IMMEDIATELY); mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); final NotificationRecord child = generateNotificationRecord( @@ -2220,6 +2221,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { any(), anyString(), anyInt(), anyString(), anyInt())) .thenReturn(SHOW_IMMEDIATELY); mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); parent.getNotification().flags |= FLAG_FOREGROUND_SERVICE; @@ -2243,6 +2245,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelNotificationWithTag_fromApp_canCancelOngoingNoClearChild() throws Exception { mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); final NotificationRecord child = generateNotificationRecord( @@ -2266,6 +2269,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelNotificationWithTag_fromApp_canCancelOngoingNoClearParent() throws Exception { mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); parent.getNotification().flags |= FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; @@ -2292,6 +2296,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { any(), anyString(), anyInt(), anyString(), anyInt())) .thenReturn(SHOW_IMMEDIATELY); mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); final NotificationRecord child = generateNotificationRecord( @@ -2320,6 +2325,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { any(), anyString(), anyInt(), anyString(), anyInt())) .thenReturn(SHOW_IMMEDIATELY); mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); parent.getNotification().flags |= FLAG_FOREGROUND_SERVICE; @@ -2345,6 +2351,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelAllNotifications_fromApp_canCancelOngoingNoClearChild() throws Exception { mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); final NotificationRecord child = generateNotificationRecord( @@ -2370,6 +2377,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelAllNotifications_fromApp_canCancelOngoingNoClearParent() throws Exception { mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); parent.getNotification().flags |= FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; @@ -3209,7 +3217,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { eq(channel2.getId()), anyBoolean())) .thenReturn(channel2); when(mPreferencesHelper.createNotificationChannel(eq(PKG), anyInt(), - eq(channel2), anyBoolean(), anyBoolean())) + eq(channel2), anyBoolean(), anyBoolean(), anyInt(), anyBoolean())) .thenReturn(true); reset(mListeners); @@ -3268,7 +3276,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { eq(mTestNotificationChannel.getId()), anyBoolean())) .thenReturn(mTestNotificationChannel); when(mPreferencesHelper.deleteNotificationChannel(eq(PKG), anyInt(), - eq(mTestNotificationChannel.getId()))).thenReturn(true); + eq(mTestNotificationChannel.getId()), anyInt(), anyBoolean())).thenReturn(true); reset(mListeners); mBinderService.deleteNotificationChannel(PKG, mTestNotificationChannel.getId()); verify(mListeners, times(1)).notifyNotificationChannelChanged(eq(PKG), @@ -3367,7 +3375,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { null, PKG, Process.myUserHandle(), mTestNotificationChannel); verify(mPreferencesHelper, times(1)).updateNotificationChannel( - anyString(), anyInt(), any(), anyBoolean()); + anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean()); verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), eq(Process.myUserHandle()), eq(mTestNotificationChannel), @@ -3389,7 +3397,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } verify(mPreferencesHelper, never()).updateNotificationChannel( - anyString(), anyInt(), any(), anyBoolean()); + anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean()); verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), eq(Process.myUserHandle()), eq(mTestNotificationChannel), @@ -3415,7 +3423,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } verify(mPreferencesHelper, never()).updateNotificationChannel( - anyString(), anyInt(), any(), anyBoolean()); + anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean()); verify(mListeners, never()).notifyNotificationChannelChanged(eq(PKG), eq(Process.myUserHandle()), eq(mTestNotificationChannel), @@ -4895,7 +4903,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mLocaleChangeReceiver.onReceive(mContext, new Intent(Intent.ACTION_LOCALE_CHANGED)); - verify(mZenModeHelper, times(1)).updateDefaultZenRules(); + verify(mZenModeHelper, times(1)).updateDefaultZenRules( + anyInt(), anyBoolean()); } @Test @@ -6759,6 +6768,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -6781,6 +6791,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -6800,6 +6811,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -6829,6 +6841,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(false); // rate limit reached setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -6852,6 +6865,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -6886,6 +6900,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -6905,6 +6920,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -6924,6 +6940,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -6954,6 +6971,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(false); // rate limit reached setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); setAppInForegroundForToasts(mUid, false); @@ -6976,6 +6994,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(false); // rate limit reached setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); setAppInForegroundForToasts(mUid, true); @@ -6997,6 +7016,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(false); // rate limit reached setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, true); setAppInForegroundForToasts(mUid, false); @@ -7018,6 +7038,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(false); // rate limit reached setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); setAppInForegroundForToasts(mUid, false); @@ -7076,6 +7097,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -7097,6 +7119,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -7200,6 +7223,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); mockIsUserVisible(DEFAULT_DISPLAY, false); @@ -7220,6 +7244,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -7242,6 +7267,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -7286,6 +7312,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); @@ -8177,7 +8204,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(rule, "com.android.settings"); // verify that zen mode helper gets passed in a package name of "android" - verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString()); + verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString(), + anyInt(), eq(true)); // system call counts as "is system or system ui" } @Test @@ -8198,7 +8226,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(rule, "com.android.settings"); // verify that zen mode helper gets passed in a package name of "android" - verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString()); + verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString(), + anyInt(), eq(true)); // system call counts as "system or system ui" } @Test @@ -8218,7 +8247,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // verify that zen mode helper gets passed in the package name from the arg, not the owner verify(mockZenModeHelper).addAutomaticZenRule( - eq("another.package"), eq(rule), anyString()); + eq("another.package"), eq(rule), anyString(), anyInt(), + eq(false)); // doesn't count as a system/systemui call } @Test @@ -10037,8 +10067,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMatchesCallFilter_noPermissionShouldThrow() throws Exception { - // set the testable NMS to not system uid + // set the testable NMS to not system uid/appid mService.isSystemUid = false; + mService.isSystemAppId = false; // make sure a caller without listener access or read_contacts permission can't call // matchesCallFilter. @@ -10077,6 +10108,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMatchesCallFilter_hasListenerPermission() throws Exception { mService.isSystemUid = false; + mService.isSystemAppId = false; // make sure a caller with only listener access and not read_contacts permission can call // matchesCallFilter. @@ -10095,6 +10127,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMatchesCallFilter_hasContactsPermission() throws Exception { mService.isSystemUid = false; + mService.isSystemAppId = false; // make sure a caller with only read_contacts permission and not listener access can call // matchesCallFilter. @@ -11225,6 +11258,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mJsi.isNotificationAssociatedWithAnyUserInitiatedJobs(anyInt(), anyInt(), anyString())) .thenReturn(true); mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); final NotificationRecord child = generateNotificationRecord( @@ -11249,6 +11283,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mJsi.isNotificationAssociatedWithAnyUserInitiatedJobs(anyInt(), anyInt(), anyString())) .thenReturn(true); mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); parent.getNotification().flags |= FLAG_USER_INITIATED_JOB; @@ -11273,6 +11308,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mJsi.isNotificationAssociatedWithAnyUserInitiatedJobs(anyInt(), anyInt(), anyString())) .thenReturn(true); mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); final NotificationRecord child = generateNotificationRecord( @@ -11299,6 +11335,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mJsi.isNotificationAssociatedWithAnyUserInitiatedJobs(anyInt(), anyInt(), anyString())) .thenReturn(true); mService.isSystemUid = false; + mService.isSystemAppId = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); parent.getNotification().flags |= FLAG_USER_INITIATED_JOB; @@ -11722,6 +11759,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private void allowTestPackageToToast() throws Exception { assertWithMessage("toast queue").that(mService.mToastQueue).isEmpty(); mService.isSystemUid = false; + mService.isSystemAppId = false; setToastRateIsWithinQuota(true); setIfPackageHasPermissionToAvoidToastRateLimiting(TEST_PACKAGE, false); // package is not suspended diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index c78b03e4b5cb..48ad86da1bc5 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -60,6 +60,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -421,13 +422,15 @@ public class PreferencesHelperTest extends UiServiceTestCase { int uid0 = 1001; setUpPackageWithUid(package0, uid0); NotificationChannel channel0 = new NotificationChannel("id0", "name0", IMPORTANCE_HIGH); - assertTrue(mHelper.createNotificationChannel(package0, uid0, channel0, true, false)); + assertTrue(mHelper.createNotificationChannel(package0, uid0, channel0, true, false, + uid0, false)); String package10 = "test.package.user10"; int uid10 = 1001001; setUpPackageWithUid(package10, uid10); NotificationChannel channel10 = new NotificationChannel("id10", "name10", IMPORTANCE_HIGH); - assertTrue(mHelper.createNotificationChannel(package10, uid10, channel10, true, false)); + assertTrue(mHelper.createNotificationChannel(package10, uid10, channel10, true, false, + uid10, false)); ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> appPermissions = new ArrayMap<>(); appPermissions.put(new Pair<>(uid0, package0), new Pair<>(false, false)); @@ -459,7 +462,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { int uid0 = 1001; setUpPackageWithUid(package0, uid0); NotificationChannel channel0 = new NotificationChannel("id0", "name0", IMPORTANCE_HIGH); - assertTrue(mHelper.createNotificationChannel(package0, uid0, channel0, true, false)); + assertTrue(mHelper.createNotificationChannel(package0, uid0, channel0, true, false, + uid0, false)); ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> appPermissions = new ArrayMap<>(); appPermissions.put(new Pair<>(uid0, package0), new Pair<>(true, false)); @@ -506,10 +510,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel2.setConversationId("id1", "conversation"); channel2.setDemoted(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false)); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false)); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true, + UID_N_MR1, false); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false)); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false, + UID_N_MR1, false)); mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true); @@ -569,12 +577,18 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel3 = new NotificationChannel("id3", "NAM3", IMPORTANCE_HIGH); channel3.enableVibration(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false); - mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false, + UID_N_MR1, false); mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true); mHelper.setInvalidMessageSent(PKG_P, UID_P); @@ -1040,12 +1054,18 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel3 = new NotificationChannel("id3", "NAM3", IMPORTANCE_HIGH); channel3.enableVibration(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false); - mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false, + UID_N_MR1, false); mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true); mHelper.setInvalidMessageSent(PKG_P, UID_P); @@ -1120,12 +1140,18 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel3 = new NotificationChannel("id3", "NAM3", IMPORTANCE_HIGH); channel3.enableVibration(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false); - mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false, + UID_N_MR1, false); mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true); mHelper.setInvalidMessageSent(PKG_P, UID_P); @@ -1202,12 +1228,18 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel3 = new NotificationChannel("id3", "NAM3", IMPORTANCE_HIGH); channel3.enableVibration(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false); - mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false, + UID_N_MR1, false); mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true); mHelper.setInvalidMessageSent(PKG_P, UID_P); @@ -1285,7 +1317,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); channel.setSound(SOUND_URI, mAudioAttributes); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM, channel.getId()); @@ -1312,7 +1345,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); channel.setSound(SOUND_URI, mAudioAttributes); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM, channel.getId()); @@ -1334,7 +1368,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); channel.setSound(SOUND_URI, mAudioAttributes); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM, channel.getId()); @@ -1359,7 +1394,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); channel.setSound(SOUND_URI, mAudioAttributes); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM, channel.getId()); @@ -1437,7 +1473,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); channel.setSound(null, mAudioAttributes); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM, channel.getId()); @@ -1466,7 +1503,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); channel.setSound(ANDROID_RES_SOUND_URI, mAudioAttributes); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM, channel.getId()); @@ -1501,7 +1539,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); channel.setSound(FILE_SOUND_URI, mAudioAttributes); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, true, USER_SYSTEM, channel.getId()); @@ -1527,14 +1566,21 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("id3", "name3", IMPORTANCE_LOW); channel3.setGroup(ncg.getId()); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false); - - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId()); - mHelper.deleteNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg.getId()); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false, + UID_N_MR1, false); + + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), + UID_N_MR1, false); + mHelper.deleteNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg.getId(), + UID_N_MR1, false); assertEquals(channel2, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2.getId(), false)); @@ -1578,7 +1624,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { UID_N_MR1, NotificationChannel.DEFAULT_CHANNEL_ID, false); defaultChannel.setImportance(NotificationManager.IMPORTANCE_LOW); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, defaultChannel, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, defaultChannel, true, + UID_N_MR1, false); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, false, UserHandle.USER_ALL, NotificationChannel.DEFAULT_CHANNEL_ID); @@ -1641,7 +1688,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testDeletesDefaultChannelAfterChannelIsCreated() throws Exception { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, - new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false); + new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false, + UID_N_MR1, false); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, false, UserHandle.USER_ALL, NotificationChannel.DEFAULT_CHANNEL_ID, "bananas"); @@ -1661,7 +1709,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, false, UserHandle.USER_ALL, NotificationChannel.DEFAULT_CHANNEL_ID, "bananas"); mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, - new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false); + new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false, + UID_N_MR1, false); loadStreamXml(baos, false, UserHandle.USER_ALL); @@ -1674,7 +1723,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { try { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE - 1), - true, false); + true, false, UID_N_MR1, false); fail("Was allowed to create a channel with invalid importance"); } catch (IllegalArgumentException e) { // yay @@ -1682,7 +1731,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { try { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel("bananas", "bananas", IMPORTANCE_UNSPECIFIED), - true, false); + true, false, UID_N_MR1, false); fail("Was allowed to create a channel with invalid importance"); } catch (IllegalArgumentException e) { // yay @@ -1690,57 +1739,61 @@ public class PreferencesHelperTest extends UiServiceTestCase { try { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX + 1), - true, false); + true, false, UID_N_MR1, false); fail("Was allowed to create a channel with invalid importance"); } catch (IllegalArgumentException e) { // yay } assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, - new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE), true, false)); + new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE), true, false, + UID_N_MR1, false)); assertFalse(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, - new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX), true, false)); + new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX), true, false, + UID_N_MR1, false)); } @Test public void testUpdateChannel_downgradeImportance() { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel("bananas", "bananas", IMPORTANCE_DEFAULT), - true, false); + true, false, UID_N_MR1, false); assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, - new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false)); + new NotificationChannel("bananas", "bananas", IMPORTANCE_LOW), true, false, + UID_N_MR1, false)); } @Test public void testUpdateChannel_upgradeImportance_ignored() { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel("bananas", "bananas", IMPORTANCE_DEFAULT), - true, false); + true, false, UID_N_MR1, false); assertFalse(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, - new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX), true, false)); + new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX), true, false, + UID_N_MR1, false)); } @Test public void testUpdateChannel_badImportance() { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel("bananas", "bananas", IMPORTANCE_DEFAULT), - true, false); + true, false, UID_N_MR1, false); assertThrows(IllegalArgumentException.class, () -> mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel("bananas", "bananas", IMPORTANCE_NONE - 1), true, - false)); + false, UID_N_MR1, false)); assertThrows(IllegalArgumentException.class, () -> mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel("bananas", "bananas", IMPORTANCE_UNSPECIFIED), true, - false)); + false, UID_N_MR1, false)); assertThrows(IllegalArgumentException.class, () -> mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel("bananas", "bananas", IMPORTANCE_MAX + 1), true, - false)); + false, UID_N_MR1, false)); } @Test @@ -1753,7 +1806,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel.setBypassDnd(true); channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, false, false)); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, false, false, + SYSTEM_UID, true)); // same id, try to update all fields final NotificationChannel channel2 = @@ -1763,7 +1817,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel2.setBypassDnd(false); channel2.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, + SYSTEM_UID, true); // all fields should be changed assertEquals(channel2, @@ -1788,7 +1843,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { defaultChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); mHelper.setAppImportanceLocked(PKG_N_MR1, UID_N_MR1); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, defaultChannel, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, defaultChannel, true, + SYSTEM_UID, true); // ensure app level fields are changed assertFalse(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1)); @@ -1801,7 +1857,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testUpdate_postUpgrade_noUpdateAppFields() throws Exception { final NotificationChannel channel = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false, + SYSTEM_UID, true); assertTrue(mHelper.canShowBadge(PKG_O, UID_O)); assertEquals(Notification.PRIORITY_DEFAULT, mHelper.getPackagePriority(PKG_O, UID_O)); assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, @@ -1812,7 +1869,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel.setBypassDnd(true); channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - mHelper.updateNotificationChannel(PKG_O, UID_O, channel, true); + mHelper.updateNotificationChannel(PKG_O, UID_O, channel, true, + SYSTEM_UID, true); // ensure app level fields are not changed assertTrue(mHelper.canShowBadge(PKG_O, UID_O)); @@ -1825,7 +1883,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testUpdate_preUpgrade_noUpdateAppFieldsWithMultipleChannels() throws Exception { final NotificationChannel channel = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, false, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, false, false, + SYSTEM_UID, true); assertTrue(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1)); assertEquals(Notification.PRIORITY_DEFAULT, mHelper.getPackagePriority(PKG_N_MR1, UID_N_MR1)); assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE, @@ -1836,7 +1895,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel.setBypassDnd(true); channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, + SYSTEM_UID, true); NotificationChannel defaultChannel = mHelper.getNotificationChannel( PKG_N_MR1, UID_N_MR1, NotificationChannel.DEFAULT_CHANNEL_ID, false); @@ -1846,7 +1906,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { defaultChannel.setBypassDnd(true); defaultChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, defaultChannel, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, defaultChannel, true, + SYSTEM_UID, true); // ensure app level fields are not changed assertTrue(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1)); @@ -1877,7 +1938,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { } channel.lockFields(lockMask); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false)); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false)); NotificationChannel savedChannel = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), false); @@ -1909,7 +1971,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { } channel.lockFields(lockMask); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); NotificationChannel savedChannel = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), false); @@ -1936,13 +1999,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testLockFields_soundAndVibration() { - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false, + UID_N_MR1, false); final NotificationChannel update1 = getChannel(); update1.setSound(new Uri.Builder().scheme("test").build(), new AudioAttributes.Builder().build()); update1.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update1, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update1, true, SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_PRIORITY | NotificationChannel.USER_LOCKED_SOUND, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update1.getId(), false) @@ -1950,7 +2014,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel update2 = getChannel(); update2.enableVibration(true); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update2, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update2, true, SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_PRIORITY | NotificationChannel.USER_LOCKED_SOUND | NotificationChannel.USER_LOCKED_VIBRATION, @@ -1960,18 +2024,19 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testLockFields_vibrationAndLights() { - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false, + SYSTEM_UID, true); final NotificationChannel update1 = getChannel(); update1.setVibrationPattern(new long[]{7945, 46 ,246}); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update1, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update1, true, SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_VIBRATION, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update1.getId(), false) .getUserLockedFields()); final NotificationChannel update2 = getChannel(); update2.enableLights(true); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update2, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update2, true, SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_VIBRATION | NotificationChannel.USER_LOCKED_LIGHTS, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update2.getId(), false) @@ -1980,18 +2045,20 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testLockFields_lightsAndImportance() { - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false, + UID_N_MR1, false); final NotificationChannel update1 = getChannel(); update1.setLightColor(Color.GREEN); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update1, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update1, true, SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_LIGHTS, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update1.getId(), false) .getUserLockedFields()); final NotificationChannel update2 = getChannel(); update2.setImportance(IMPORTANCE_DEFAULT); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update2, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update2, true, + SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_LIGHTS | NotificationChannel.USER_LOCKED_IMPORTANCE, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update2.getId(), false) @@ -2000,21 +2067,22 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testLockFields_visibilityAndDndAndBadge() { - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false, + UID_N_MR1, false); assertEquals(0, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel().getId(), false) .getUserLockedFields()); final NotificationChannel update1 = getChannel(); update1.setBypassDnd(true); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update1, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update1, true, SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_PRIORITY, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update1.getId(), false) .getUserLockedFields()); final NotificationChannel update2 = getChannel(); update2.setLockscreenVisibility(Notification.VISIBILITY_SECRET); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update2, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update2, true, SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_PRIORITY | NotificationChannel.USER_LOCKED_VISIBILITY, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update2.getId(), false) @@ -2022,7 +2090,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { final NotificationChannel update3 = getChannel(); update3.setShowBadge(false); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update3, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update3, true, SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_PRIORITY | NotificationChannel.USER_LOCKED_VISIBILITY | NotificationChannel.USER_LOCKED_SHOW_BADGE, @@ -2032,14 +2100,16 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testLockFields_allowBubble() { - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false, + UID_N_MR1, false); assertEquals(0, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel().getId(), false) .getUserLockedFields()); final NotificationChannel update = getChannel(); update.setAllowBubbles(true); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update, true, + SYSTEM_UID, true); assertEquals(NotificationChannel.USER_LOCKED_ALLOW_BUBBLE, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update.getId(), false) .getUserLockedFields()); @@ -2047,15 +2117,19 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testDeleteNonExistentChannel() throws Exception { - mHelper.deleteNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, "does not exist"); + mHelper.deleteNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, "does not exist", + UID_N_MR1, false); } @Test public void testDoubleDeleteChannel() throws Exception { NotificationChannel channel = getChannel(); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId()); - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId()); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), + UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), + UID_N_MR1, false); assertEquals(2, mLogger.getCalls().size()); assertEquals( NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_CREATED, @@ -2076,8 +2150,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel.enableVibration(true); channel.setVibrationPattern(new long[]{100, 67, 145, 156}); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId()); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), + UID_N_MR1, false); // Does not return deleted channel NotificationChannel response = @@ -2105,10 +2181,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel2 = new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_HIGH); channelMap.put(channel2.getId(), channel2); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false, + UID_N_MR1, false); - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId()); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), + UID_N_MR1, false); // Returns only non-deleted channels List<NotificationChannel> channels = @@ -2138,12 +2217,17 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_HIGH); NotificationChannel channel3 = new NotificationChannel("id5", "a", NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false); - - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId()); - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3.getId()); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false, + UID_N_MR1, false); + + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), + UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3.getId(), + UID_N_MR1, false); assertEquals(2, mHelper.getDeletedChannelCount(PKG_N_MR1, UID_N_MR1)); assertEquals(0, mHelper.getDeletedChannelCount("pkg2", UID_O)); @@ -2157,11 +2241,15 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("id4", "a", NotificationManager.IMPORTANCE_NONE); NotificationChannel channel3 = new NotificationChannel("id5", "a", NotificationManager.IMPORTANCE_NONE); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false, + UID_N_MR1, false); - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3.getId()); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3.getId(), + UID_N_MR1, false); assertEquals(1, mHelper.getBlockedChannelCount(PKG_N_MR1, UID_N_MR1)); assertEquals(0, mHelper.getBlockedChannelCount("pkg2", UID_O)); @@ -2180,7 +2268,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_MAX); channel.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, 111, channel, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, 111, channel, true, true, + 111, false); assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, uid).getList().size()); @@ -2194,15 +2283,18 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationManager.IMPORTANCE_MAX); channel1.setBypassDnd(true); channel1.setGroup(ncg.getId()); - mHelper.createNotificationChannelGroup(PKG_N_MR1, uid, ncg, /* fromTargetApp */ true); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel1, true, /*has DND access*/ true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, uid, ncg, /* fromTargetApp */ true, + uid, false); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel1, true, /*has DND access*/ true, + uid, false); assertEquals(1, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, uid).getList().size()); // disable group ncg.setBlocked(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, uid, ncg, /* fromTargetApp */ false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, uid, ncg, /* fromTargetApp */ false, + SYSTEM_UID, true); assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, uid).getList().size()); } @@ -2220,9 +2312,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel2.setBypassDnd(true); channel3.setBypassDnd(true); // has DND access, so can set bypassDnd attribute - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel1, true, /*has DND access*/ true); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel3, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel1, true, /*has DND access*/ true, + uid, false); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, + uid, false); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel3, true, true, + uid, false); assertEquals(3, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, uid).getList().size()); @@ -2247,29 +2342,31 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected result: areChannelsBypassingDnd = false // setNotificationPolicy isn't called since areChannelsBypassingDnd was already false NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false, + uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); // create notification channel that can bypass dnd // expected result: areChannelsBypassingDnd = true NotificationChannel channel2 = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); channel2.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, + uid, false); assertTrue(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); // delete channels - mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId()); + mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false); assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); - mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId()); + mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); } @@ -2282,18 +2379,20 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected result: areChannelsBypassingDnd = false // setNotificationPolicy isn't called since areChannelsBypassingDnd was already false NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false, + uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); // Recreate a channel & now the app has dnd access granted and can set the bypass dnd field NotificationChannel update = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); update.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, uid, update, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, update, true, true, + uid, false); assertTrue(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); } @@ -2306,29 +2405,31 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected result: areChannelsBypassingDnd = false // setNotificationPolicy isn't called since areChannelsBypassingDnd was already false NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false, + uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); // create notification channel that can bypass dnd, using local app level settings // expected result: areChannelsBypassingDnd = true NotificationChannel channel2 = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); channel2.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, + uid, false); assertTrue(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); // delete channels - mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId()); + mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false); assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); - mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId()); + mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); } @@ -2350,13 +2451,16 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected result: areChannelsBypassingDnd = false NotificationChannelGroup group = new NotificationChannelGroup("group", "group"); group.setBlocked(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, uid, group, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, uid, group, false, + SYSTEM_UID, true); NotificationChannel channel2 = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); channel2.setGroup("group"); channel2.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, + uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), + anyInt(), anyBoolean()); resetZenModeHelper(); } @@ -2377,9 +2481,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected result: areChannelsBypassingDnd = false NotificationChannel channel2 = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); channel2.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, + uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); } @@ -2400,9 +2505,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected result: areChannelsBypassingDnd = false NotificationChannel channel2 = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); channel2.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, + uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); } @@ -2415,25 +2521,26 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected result: areChannelsBypassingDnd = false // setNotificationPolicy isn't called since areChannelsBypassingDnd was already false NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false, + uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); // update channel so it CAN bypass dnd: // expected result: areChannelsBypassingDnd = true channel.setBypassDnd(true); - mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true); + mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true); assertTrue(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); // update channel so it can't bypass dnd: // expected result: areChannelsBypassingDnd = false channel.setBypassDnd(false); - mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true); + mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); } @@ -2448,7 +2555,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); } @@ -2461,7 +2568,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { mPermissionHelper, mLogger, mAppOpsManager, mStatsEventBuilderFactory, false); assertFalse(mHelper.areChannelsBypassingDnd()); - verify(mMockZenModeHelper, never()).setNotificationPolicy(any()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyBoolean()); resetZenModeHelper(); } @@ -2472,14 +2579,17 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("id2", "name2", IMPORTANCE_LOW); channel.setVibrationPattern(vibration); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId()); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), + UID_N_MR1, false); NotificationChannel newChannel = new NotificationChannel( channel.getId(), channel.getName(), NotificationManager.IMPORTANCE_HIGH); newChannel.setVibrationPattern(new long[]{100}); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false, + UID_N_MR1, false); // No long deleted, using old settings compareChannels(channel, @@ -2491,7 +2601,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertTrue(mHelper.onlyHasDefaultChannel(PKG_N_MR1, UID_N_MR1)); assertFalse(mHelper.onlyHasDefaultChannel(PKG_O, UID_O)); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false, + UID_N_MR1, false); assertFalse(mHelper.onlyHasDefaultChannel(PKG_N_MR1, UID_N_MR1)); } @@ -2499,7 +2610,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testCreateChannel_defaultChannelId() throws Exception { try { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel( - NotificationChannel.DEFAULT_CHANNEL_ID, "ha", IMPORTANCE_HIGH), true, false); + NotificationChannel.DEFAULT_CHANNEL_ID, "ha", IMPORTANCE_HIGH), true, false, + UID_N_MR1, false); fail("Allowed to create default channel"); } catch (IllegalArgumentException e) { // pass @@ -2513,7 +2625,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("id2", "name2", IMPORTANCE_LOW); channel.setVibrationPattern(vibration); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false)); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false)); NotificationChannel newChannel = new NotificationChannel( channel.getId(), channel.getName(), NotificationManager.IMPORTANCE_HIGH); @@ -2524,7 +2637,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { newChannel.setShowBadge(!channel.canShowBadge()); assertFalse(mHelper.createNotificationChannel( - PKG_N_MR1, UID_N_MR1, newChannel, true, false)); + PKG_N_MR1, UID_N_MR1, newChannel, true, false, + UID_N_MR1, false)); // Old settings not overridden compareChannels(channel, @@ -2542,7 +2656,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { final NotificationChannel channel = new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_DEFAULT); channel.setSound(sound, mAudioAttributes); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); assertEquals(sound, mHelper.getNotificationChannel( PKG_N_MR1, UID_N_MR1, channel.getId(), false).getSound()); } @@ -2554,8 +2669,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel2 = new NotificationChannel("id2", "name2", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, false, false, + UID_N_MR1, false); mHelper.permanentlyDeleteNotificationChannels(PKG_N_MR1, UID_N_MR1); @@ -2576,14 +2693,20 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("deleted", "belongs to deleted", IMPORTANCE_DEFAULT); groupedAndDeleted.setGroup("totally"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, deleted, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true, + UID_N_MR1, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, deleted, true, + UID_N_MR1, false); mHelper.createNotificationChannel( - PKG_N_MR1, UID_N_MR1, nonGroupedNonDeletedChannel, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, groupedAndDeleted, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, groupedButNotDeleted, true, false); + PKG_N_MR1, UID_N_MR1, nonGroupedNonDeletedChannel, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, groupedAndDeleted, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, groupedButNotDeleted, true, false, + UID_N_MR1, false); - mHelper.deleteNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, deleted.getId()); + mHelper.deleteNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, deleted.getId(), + UID_N_MR1, false); assertNull(mHelper.getNotificationChannelGroup(deleted.getId(), PKG_N_MR1, UID_N_MR1)); assertNotNull( @@ -2626,10 +2749,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { convo.setGroup("not"); convo.setConversationId("not deleted", "banana"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, base, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, convo, true, false); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, base, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, convo, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true, + UID_N_MR1, false); NotificationChannelGroup g = mHelper.getNotificationChannelGroup(notDeleted.getId(), PKG_N_MR1, UID_N_MR1); @@ -2679,7 +2806,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { // Deleted NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); assertTrue(mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1}, new int[]{UID_N_MR1})); @@ -2688,7 +2816,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { PKG_N_MR1, UID_N_MR1, true).getList().size()); // Not deleted - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); assertFalse(mHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_N_MR1}, new int[]{UID_N_MR1})); @@ -2698,9 +2827,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testOnPackageChanged_packageRemoval_groups() throws Exception { NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); NotificationChannelGroup ncg2 = new NotificationChannelGroup("group2", "name2"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true, + UID_N_MR1, false); mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1}, new int[]{ UID_N_MR1}); @@ -2712,7 +2843,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testOnPackageChange_downgradeTargetSdk() throws Exception { // create channel as api 26 - mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false, + UID_N_MR1, false); // install new app version targeting 25 final ApplicationInfo legacy = new ApplicationInfo(); @@ -2731,9 +2863,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testClearData() { ArraySet<Pair<String, Integer>> pkgPair = new ArraySet<>(); pkgPair.add(new Pair<>(PKG_O, UID_O)); - mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false, + UID_O, false); mHelper.createNotificationChannelGroup( - PKG_O, UID_O, new NotificationChannelGroup("1", "bye"), true); + PKG_O, UID_O, new NotificationChannelGroup("1", "bye"), true, + UID_O, false); mHelper.updateDefaultApps(UserHandle.getUserId(UID_O), null, pkgPair); mHelper.setNotificationDelegate(PKG_O, UID_O, "", 1); mHelper.setBubblesAllowed(PKG_O, UID_O, DEFAULT_BUBBLE_PREFERENCE); @@ -2750,7 +2884,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertEquals(0, mHelper.getNotificationChannelGroups(PKG_O, UID_O).size()); NotificationChannel channel = getChannel(); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, + UID_O, false); assertTrue(channel.isImportanceLockedByCriticalDeviceFunction()); } @@ -2764,7 +2899,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testCreateGroup() { NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); assertEquals(ncg, mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1).iterator().next()); verify(mHandler, never()).requestSort(); @@ -2781,7 +2917,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); channel1.setGroup("garbage"); try { - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); fail("Created a channel with a bad group"); } catch (IllegalArgumentException e) { } @@ -2791,11 +2928,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testCannotCreateChannel_goodGroup() { NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); channel1.setGroup(ncg.getId()); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); assertEquals(ncg.getId(), mHelper.getNotificationChannel( PKG_N_MR1, UID_N_MR1, channel1.getId(), false).getGroup()); @@ -2804,29 +2943,36 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testGetChannelGroups() { NotificationChannelGroup unused = new NotificationChannelGroup("unused", "s"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, unused, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, unused, true, + UID_N_MR1, false); NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); NotificationChannelGroup ncg2 = new NotificationChannelGroup("group2", "name2"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg2, true, + UID_N_MR1, false); NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); channel1.setGroup(ncg.getId()); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); NotificationChannel channel1a = new NotificationChannel("id1a", "name1", NotificationManager.IMPORTANCE_HIGH); channel1a.setGroup(ncg.getId()); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1a, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1a, true, false, + UID_N_MR1, false); NotificationChannel channel2 = new NotificationChannel("id2", "name1", NotificationManager.IMPORTANCE_HIGH); channel2.setGroup(ncg2.getId()); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false, + UID_N_MR1, false); NotificationChannel channel3 = new NotificationChannel("id3", "name1", NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false, + UID_N_MR1, false); List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups( PKG_N_MR1, UID_N_MR1, true, true, false).getList(); @@ -2855,16 +3001,19 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testGetChannelGroups_noSideEffects() { NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); channel1.setGroup(ncg.getId()); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1, true, true, false).getList(); channel1.setImportance(IMPORTANCE_LOW); - mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, + UID_N_MR1, false); List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups( PKG_N_MR1, UID_N_MR1, true, true, false).getList(); @@ -2880,14 +3029,17 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testGetChannelGroups_includeEmptyGroups() { NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true, + UID_N_MR1, false); NotificationChannelGroup ncgEmpty = new NotificationChannelGroup("group2", "name2"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncgEmpty, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncgEmpty, true, + UID_N_MR1, false); NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); channel1.setGroup(ncg.getId()); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups( PKG_N_MR1, UID_N_MR1, false, false, true).getList(); @@ -2906,13 +3058,15 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testCreateChannel_updateName() { NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false)); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false, + UID_N_MR1, false)); NotificationChannel actual = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false); assertEquals("hello", actual.getName()); nc = new NotificationChannel("id", "goodbye", IMPORTANCE_HIGH); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false)); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false, + UID_N_MR1, false)); actual = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false); assertEquals("goodbye", actual.getName()); @@ -2924,16 +3078,19 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testCreateChannel_addToGroup() { NotificationChannelGroup group = new NotificationChannelGroup("group", "group"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true, + UID_N_MR1, false); NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false)); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false, + UID_N_MR1, false)); NotificationChannel actual = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false); assertNull(actual.getGroup()); nc = new NotificationChannel("id", "hello", IMPORTANCE_HIGH); nc.setGroup(group.getId()); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false)); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false, + UID_N_MR1, false)); actual = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false); assertNotNull(actual.getGroup()); @@ -2969,14 +3126,16 @@ public class PreferencesHelperTest extends UiServiceTestCase { int numChannels = ThreadLocalRandom.current().nextInt(1, 10); for (int j = 0; j < numChannels; j++) { mHelper.createNotificationChannel(pkgName, UID_N_MR1, - new NotificationChannel("" + j, "a", IMPORTANCE_HIGH), true, false); + new NotificationChannel("" + j, "a", IMPORTANCE_HIGH), true, false, + UID_N_MR1, false); } expectedChannels.put(pkgName, numChannels); } // delete the first channel of the first package String pkg = expectedChannels.keyAt(0); - mHelper.deleteNotificationChannel("pkg" + 0, UID_N_MR1, "0"); + mHelper.deleteNotificationChannel("pkg" + 0, UID_N_MR1, "0", + UID_N_MR1, false); // dump should not include deleted channels int count = expectedChannels.get(pkg); expectedChannels.put(pkg, count - 1); @@ -3012,10 +3171,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { new NotificationChannel("id2", "name2", IMPORTANCE_LOW); NotificationChannel channel3 = new NotificationChannel("id3", "name3", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_P, UID_P, channel1, true, false); - mHelper.createNotificationChannel(PKG_P, UID_P, channel2, false, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false); - mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false); + mHelper.createNotificationChannel(PKG_P, UID_P, channel1, true, false, + UID_P, false); + mHelper.createNotificationChannel(PKG_P, UID_P, channel2, false, false, + UID_P, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, false, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_O, UID_O, getChannel(), true, false, + UID_N_MR1, false); // in the json array, all of the individual package preferences are simply elements in the // values array. this set is to collect expected outputs for each of our packages. @@ -3328,7 +3491,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testIsGroupBlocked_notBlocked() throws Exception { NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true, + UID_N_MR1, false); assertFalse(mHelper.isGroupBlocked(PKG_N_MR1, UID_N_MR1, group.getId())); } @@ -3336,9 +3500,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testIsGroupBlocked_blocked() throws Exception { NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true, + UID_N_MR1, false); group.setBlocked(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, false, + UID_N_MR1, false); assertTrue(mHelper.isGroupBlocked(PKG_N_MR1, UID_N_MR1, group.getId())); } @@ -3347,27 +3513,32 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testIsGroupBlocked_appCannotCreateAsBlocked() throws Exception { NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); group.setBlocked(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true, + UID_N_MR1, false); assertFalse(mHelper.isGroupBlocked(PKG_N_MR1, UID_N_MR1, group.getId())); NotificationChannelGroup group3 = group.clone(); group3.setBlocked(false); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group3, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group3, true, + UID_N_MR1, false); assertFalse(mHelper.isGroupBlocked(PKG_N_MR1, UID_N_MR1, group.getId())); } @Test public void testIsGroup_appCannotResetBlock() throws Exception { NotificationChannelGroup group = new NotificationChannelGroup("id", "name"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true, + UID_N_MR1, false); NotificationChannelGroup group2 = group.clone(); group2.setBlocked(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group2, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group2, false, + UID_N_MR1, false); assertTrue(mHelper.isGroupBlocked(PKG_N_MR1, UID_N_MR1, group.getId())); NotificationChannelGroup group3 = group.clone(); group3.setBlocked(false); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group3, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group3, true, + UID_N_MR1, false); assertTrue(mHelper.isGroupBlocked(PKG_N_MR1, UID_N_MR1, group.getId())); } @@ -3375,8 +3546,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testGetNotificationChannelGroupWithChannels() throws Exception { NotificationChannelGroup group = new NotificationChannelGroup("group", "group"); NotificationChannelGroup other = new NotificationChannelGroup("something else", "name"); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, other, true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true, + UID_N_MR1, false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, other, true, + UID_N_MR1, false); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_DEFAULT); a.setGroup(group.getId()); @@ -3386,11 +3559,16 @@ public class PreferencesHelperTest extends UiServiceTestCase { c.setGroup(group.getId()); NotificationChannel d = new NotificationChannel("d", "d", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, a, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, b, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, c, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, d, true, false); - mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, c.getId()); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, a, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, b, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, c, true, false, + UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, d, true, false, + UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, c.getId(), + UID_N_MR1, false); NotificationChannelGroup retrieved = mHelper.getNotificationChannelGroupWithChannels( PKG_N_MR1, UID_N_MR1, group.getId(), true); @@ -3409,7 +3587,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); test.setBypassDnd(true); - mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true, false); + mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true, false, + SYSTEM_UID, true); assertFalse(mHelper.getNotificationChannel(SYSTEM_PKG, SYSTEM_UID, "A", false) .canBypassDnd()); @@ -3420,7 +3599,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); test.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, test, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, test, true, true, + UID_N_MR1, false); assertTrue(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "A", false).canBypassDnd()); } @@ -3430,7 +3610,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); test.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, 1000, test, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, 1000, test, true, false, + UID_N_MR1, false); assertFalse(mHelper.getNotificationChannel(PKG_N_MR1, 1000, "A", false).canBypassDnd()); } @@ -3438,11 +3619,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testAndroidPkgCannotBypassDnd_update() throws Exception { NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true, false); + mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, test, true, false, + SYSTEM_UID, true); NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW); update.setBypassDnd(true); - assertFalse(mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, update, true, false)); + assertFalse(mHelper.createNotificationChannel(SYSTEM_PKG, SYSTEM_UID, update, true, false, + SYSTEM_UID, true)); assertFalse(mHelper.getNotificationChannel(SYSTEM_PKG, SYSTEM_UID, "A", false) .canBypassDnd()); @@ -3451,11 +3634,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testDndPkgCanBypassDnd_update() throws Exception { NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, test, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, test, true, true, + UID_N_MR1, false); NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW); update.setBypassDnd(true); - assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, update, true, true)); + assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, update, true, true, + UID_N_MR1, false)); assertTrue(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "A", false).canBypassDnd()); } @@ -3463,10 +3648,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testNormalPkgCannotBypassDnd_update() { NotificationChannel test = new NotificationChannel("A", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_N_MR1, 1000, test, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, 1000, test, true, false, + UID_N_MR1, false); NotificationChannel update = new NotificationChannel("A", "a", IMPORTANCE_LOW); update.setBypassDnd(true); - mHelper.createNotificationChannel(PKG_N_MR1, 1000, update, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, 1000, update, true, false, + UID_N_MR1, false); assertFalse(mHelper.getNotificationChannel(PKG_N_MR1, 1000, "A", false).canBypassDnd()); } @@ -3726,12 +3913,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O)); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, + UID_O, false); NotificationChannel update = new NotificationChannel("a", "a", IMPORTANCE_NONE); update.setAllowBubbles(false); - mHelper.updateNotificationChannel(PKG_O, UID_O, update, true); + mHelper.updateNotificationChannel(PKG_O, UID_O, update, true, + UID_O, false); assertEquals(IMPORTANCE_HIGH, mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance()); @@ -3745,12 +3934,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { toAdd.add(new Pair<>(PKG_O, UID_O)); mHelper.updateDefaultApps(0, null, toAdd); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); NotificationChannel update = new NotificationChannel("a", "a", IMPORTANCE_NONE); update.setAllowBubbles(false); - mHelper.updateNotificationChannel(PKG_O, UID_O, update, true); + mHelper.updateNotificationChannel(PKG_O, UID_O, update, true, UID_O, false); assertEquals(IMPORTANCE_HIGH, mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance()); @@ -3763,12 +3952,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_NONE); - mHelper.createNotificationChannel(PKG_O, UID_O, a, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, false, false, UID_O, false); NotificationChannel update = new NotificationChannel("a", "a", IMPORTANCE_HIGH); update.setAllowBubbles(false); - mHelper.updateNotificationChannel(PKG_O, UID_O, update, true); + mHelper.updateNotificationChannel(PKG_O, UID_O, update, true, UID_O, false); assertEquals(IMPORTANCE_HIGH, mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance()); @@ -3782,12 +3971,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); a.setBlockable(true); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); NotificationChannel update = new NotificationChannel("a", "a", IMPORTANCE_NONE); update.setAllowBubbles(false); - mHelper.updateNotificationChannel(PKG_O, UID_O, update, true); + mHelper.updateNotificationChannel(PKG_O, UID_O, update, true, UID_O, false); assertEquals(IMPORTANCE_NONE, mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance()); @@ -3800,12 +3989,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(false); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); NotificationChannel update = new NotificationChannel("a", "a", IMPORTANCE_NONE); update.setAllowBubbles(false); - mHelper.updateNotificationChannel(PKG_O, UID_O, update, true); + mHelper.updateNotificationChannel(PKG_O, UID_O, update, true, UID_O, false); assertEquals(IMPORTANCE_NONE, mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance()); @@ -3819,9 +4008,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW); NotificationChannel c = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT); // different uids, same package - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); - mHelper.createNotificationChannel(PKG_O, UID_O, b, false, false); - mHelper.createNotificationChannel(PKG_O, UserHandle.PER_USER_RANGE + 1, c, true, true); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); + mHelper.createNotificationChannel(PKG_O, UID_O, b, false, false, + SYSTEM_UID, true); + mHelper.createNotificationChannel(PKG_O, UserHandle.PER_USER_RANGE + 1, c, true, true, + UserHandle.PER_USER_RANGE + 1, false); UserInfo user = new UserInfo(); user.id = 0; @@ -3859,7 +4050,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.updateFixedImportance(users); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false) .isImportanceLockedByCriticalDeviceFunction()); @@ -3871,9 +4062,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW); NotificationChannel c = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT); // different uids, same package - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); - mHelper.createNotificationChannel(PKG_O, UID_O, b, false, false); - mHelper.createNotificationChannel(PKG_O, UserHandle.PER_USER_RANGE + 1, c, true, true); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); + mHelper.createNotificationChannel(PKG_O, UID_O, b, false, false, UID_O, false); + mHelper.createNotificationChannel(PKG_O, UserHandle.PER_USER_RANGE + 1, c, true, true, + UID_O, false); ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>(); toAdd.add(new Pair<>(PKG_O, UID_O)); @@ -3892,8 +4084,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testUpdateDefaultApps_add_onlyGivenPkg() { NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, b, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, b, false, false, UID_O, false); ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>(); toAdd.add(new Pair<>(PKG_O, UID_O)); @@ -3910,8 +4102,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW); // different uids, same package - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); - mHelper.createNotificationChannel(PKG_O, UID_O, b, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); + mHelper.createNotificationChannel(PKG_O, UID_O, b, false, false, SYSTEM_UID, true); ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>(); toAdd.add(new Pair<>(PKG_O, UID_O)); @@ -3936,8 +4128,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testUpdateDefaultApps_addAndRemove() { NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, b, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, + UID_O, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, b, false, false, + UID_N_MR1, false); ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>(); toAdd.add(new Pair<>(PKG_O, UID_O)); @@ -3975,7 +4169,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testUpdateDefaultApps_channelDoesNotExistYet() { NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>(); toAdd.add(new Pair<>(PKG_O, UID_O)); @@ -3984,7 +4178,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false) .isImportanceLockedByCriticalDeviceFunction()); - mHelper.createNotificationChannel(PKG_O, UID_O, b, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, b, true, false, UID_O, false); assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, b.getId(), false) .isImportanceLockedByCriticalDeviceFunction()); } @@ -3992,7 +4186,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testUpdateNotificationChannel_defaultAppLockedImportance() { NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>(); toAdd.add(new Pair<>(PKG_O, UID_O)); mHelper.updateDefaultApps(UserHandle.getUserId(UID_O), null, toAdd); @@ -4000,19 +4194,20 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel update = new NotificationChannel("a", "a", IMPORTANCE_NONE); update.setAllowBubbles(false); - mHelper.updateNotificationChannel(PKG_O, UID_O, update, true); + mHelper.updateNotificationChannel(PKG_O, UID_O, update, true, SYSTEM_UID, true); assertEquals(IMPORTANCE_HIGH, mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance()); assertEquals(false, mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).canBubble()); - mHelper.updateNotificationChannel(PKG_O, UID_O, update, false); + mHelper.updateNotificationChannel(PKG_O, UID_O, update, false, UID_O, false); assertEquals(IMPORTANCE_HIGH, mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance()); NotificationChannel updateImportanceLow = new NotificationChannel("a", "a", IMPORTANCE_LOW); - mHelper.updateNotificationChannel(PKG_O, UID_O, updateImportanceLow, true); + mHelper.updateNotificationChannel(PKG_O, UID_O, updateImportanceLow, true, + SYSTEM_UID, true); assertEquals(IMPORTANCE_LOW, mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance()); } @@ -4024,7 +4219,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.updateDefaultApps(UserHandle.getUserId(UID_O), null, toAdd); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); assertTrue(a.isImportanceLockedByCriticalDeviceFunction()); } @@ -4050,7 +4245,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O)); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); // Still locked by permission if not role assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false) @@ -4078,7 +4273,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O)); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false, UID_O, false); // Still locked by role if not permission assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false) @@ -4090,7 +4285,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, channel1, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel1, true, false, UID_O, false); // clear data ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, true, @@ -4144,14 +4339,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { for (int i = 0; i < NOTIFICATION_CHANNEL_COUNT_LIMIT; i++) { NotificationChannel channel = new NotificationChannel(String.valueOf(i), String.valueOf(i), NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, true); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, true, UID_O, false); } try { NotificationChannel channel = new NotificationChannel( String.valueOf(NOTIFICATION_CHANNEL_COUNT_LIMIT), String.valueOf(NOTIFICATION_CHANNEL_COUNT_LIMIT), NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, true); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, true, UID_O, false); fail("Allowed to create too many notification channels"); } catch (IllegalStateException e) { // great @@ -4167,7 +4362,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { for (int i = 0; i < NOTIFICATION_CHANNEL_COUNT_LIMIT; i++) { NotificationChannel channel = new NotificationChannel(String.valueOf(i), String.valueOf(i), NotificationManager.IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, true); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, true, UID_O, false); } final String xml = "<ranking version=\"1\">\n" @@ -4200,13 +4395,15 @@ public class PreferencesHelperTest extends UiServiceTestCase { for (int i = 0; i < NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT; i++) { NotificationChannelGroup group = new NotificationChannelGroup(String.valueOf(i), String.valueOf(i)); - mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, fromTargetApp); + mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, fromTargetApp, + UID_O, false); } try { NotificationChannelGroup group = new NotificationChannelGroup( String.valueOf(NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT), String.valueOf(NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT)); - mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, fromTargetApp); + mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, fromTargetApp, + UID_O, false); fail("Allowed to create too many notification channel groups"); } catch (IllegalStateException e) { // great @@ -4222,7 +4419,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { for (int i = 0; i < NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT; i++) { NotificationChannelGroup group = new NotificationChannelGroup(String.valueOf(i), String.valueOf(i)); - mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, true); + mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, true, + UID_O, false); } final String xml = "<ranking version=\"1\">\n" @@ -4300,13 +4498,15 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel parent = new NotificationChannel("parent", "messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false, + UID_O, false); NotificationChannel friend = new NotificationChannel(String.format( CONVERSATION_CHANNEL_ID_FORMAT, parent.getId(), conversationId), "messages", IMPORTANCE_DEFAULT); friend.setConversationId(parent.getId(), conversationId); - mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false, + UID_O, false); compareChannelsParentChild(parent, mHelper.getConversationNotificationChannel( PKG_O, UID_O, parent.getId(), conversationId, false, false), conversationId); @@ -4318,7 +4518,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel parent = new NotificationChannel("parent", "messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false, + UID_O, false); compareChannels(parent, mHelper.getConversationNotificationChannel( PKG_O, UID_O, parent.getId(), conversationId, true, false)); @@ -4335,7 +4536,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { friend.setConversationId(parentId, conversationId); try { - mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false, + UID_O, false); fail("allowed creation of conversation channel without a parent"); } catch (IllegalArgumentException e) { // good @@ -4429,9 +4631,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { mAppOpsManager, mStatsEventBuilderFactory, false); mHelper.createNotificationChannel( - PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false); - assertTrue(mHelper.deleteNotificationChannel(PKG_P, UID_P, "id")); - assertFalse(mHelper.deleteNotificationChannel(PKG_P, UID_P, "id")); + PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false, + UID_P, false); + assertTrue(mHelper.deleteNotificationChannel(PKG_P, UID_P, "id", + UID_P, false)); + assertFalse(mHelper.deleteNotificationChannel(PKG_P, UID_P, "id", + UID_P, false)); } @Test @@ -4441,8 +4646,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { mAppOpsManager, mStatsEventBuilderFactory, false); mHelper.createNotificationChannel( - PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false); - mHelper.deleteNotificationChannel(PKG_P, UID_P, "id"); + PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false, + UID_P, false); + mHelper.deleteNotificationChannel(PKG_P, UID_P, "id", UID_P, false); NotificationChannel nc1 = mHelper.getNotificationChannel(PKG_P, UID_P, "id", true); assertTrue(DateUtils.isToday(nc1.getDeletedTimeMs())); assertTrue(nc1.isDeleted()); @@ -4471,14 +4677,16 @@ public class PreferencesHelperTest extends UiServiceTestCase { mAppOpsManager, mStatsEventBuilderFactory, false); mHelper.createNotificationChannel( - PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false); - mHelper.deleteNotificationChannel(PKG_P, UID_P, "id"); + PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false, + UID_P, false); + mHelper.deleteNotificationChannel(PKG_P, UID_P, "id", UID_P, false); NotificationChannel nc1 = mHelper.getNotificationChannel(PKG_P, UID_P, "id", true); assertTrue(DateUtils.isToday(nc1.getDeletedTimeMs())); assertTrue(nc1.isDeleted()); mHelper.createNotificationChannel( - PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false); + PKG_P, UID_P, new NotificationChannel("id", "id", 2), true, false, + UID_P, false); nc1 = mHelper.getNotificationChannel(PKG_P, UID_P, "id", true); assertEquals(-1, nc1.getDeletedTimeMs()); assertFalse(nc1.isDeleted()); @@ -4513,29 +4721,35 @@ public class PreferencesHelperTest extends UiServiceTestCase { String convoId = "convo"; NotificationChannel messages = new NotificationChannel("messages", "Messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false, + UID_O, false); NotificationChannel calls = new NotificationChannel("calls", "Calls", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false, + UID_O, false); NotificationChannel p = new NotificationChannel("p calls", "Calls", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, UID_P, p, true, false); + mHelper.createNotificationChannel(PKG_P, UID_P, p, true, false, + UID_P, false); NotificationChannel channel = new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT); channel.setConversationId(messages.getId(), convoId); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, + UID_O, false); NotificationChannel diffConvo = new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT); diffConvo.setConversationId(p.getId(), "different convo"); - mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, true, false); + mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, true, false, + UID_O, false); NotificationChannel channel2 = new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT); channel2.setConversationId(calls.getId(), convoId); channel2.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false, + SYSTEM_UID, true); List<ConversationChannelWrapper> convos = mHelper.getConversations(IntArray.wrap(new int[] {0}), false); @@ -4551,23 +4765,26 @@ public class PreferencesHelperTest extends UiServiceTestCase { String convoId = "convo"; NotificationChannel messages = new NotificationChannel("messages", "Messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false, + UID_O, false); NotificationChannel messagesUser10 = new NotificationChannel("messages", "Messages", IMPORTANCE_DEFAULT); mHelper.createNotificationChannel( - PKG_O, UID_O + UserHandle.PER_USER_RANGE, messagesUser10, true, false); + PKG_O, UID_O + UserHandle.PER_USER_RANGE, messagesUser10, true, false, + UID_O + UserHandle.PER_USER_RANGE, false); NotificationChannel messagesFromB = new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT); messagesFromB.setConversationId(messages.getId(), "different convo"); - mHelper.createNotificationChannel(PKG_O, UID_O, messagesFromB, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, messagesFromB, true, false, UID_O, false); NotificationChannel messagesFromBUser10 = new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT); messagesFromBUser10.setConversationId(messagesUser10.getId(), "different convo"); mHelper.createNotificationChannel( - PKG_O, UID_O + UserHandle.PER_USER_RANGE, messagesFromBUser10, true, false); + PKG_O, UID_O + UserHandle.PER_USER_RANGE, messagesFromBUser10, true, false, + UID_O + UserHandle.PER_USER_RANGE, false); List<ConversationChannelWrapper> convos = @@ -4589,30 +4806,31 @@ public class PreferencesHelperTest extends UiServiceTestCase { String convoId = "convo"; NotificationChannel messages = new NotificationChannel("messages", "Messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false, UID_O, false); NotificationChannel calls = new NotificationChannel("calls", "Calls", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false, UID_O, false); NotificationChannel p = new NotificationChannel("p calls", "Calls", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, UID_P, p, true, false); + mHelper.createNotificationChannel(PKG_P, UID_P, p, true, false, UID_O, false); NotificationChannel channel = new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT); channel.setConversationId(messages.getId(), convoId); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); NotificationChannel diffConvo = new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT); diffConvo.setConversationId(p.getId(), "different convo"); diffConvo.setDemoted(true); - mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, true, false); + mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, true, false, UID_P, false); NotificationChannel channel2 = new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT); channel2.setConversationId(calls.getId(), convoId); channel2.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel2, false, false, + SYSTEM_UID, true); List<ConversationChannelWrapper> convos = mHelper.getConversations(IntArray.wrap(new int[] {0}), false); @@ -4628,30 +4846,31 @@ public class PreferencesHelperTest extends UiServiceTestCase { String convoId = "convo"; NotificationChannel messages = new NotificationChannel("messages", "Messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false, UID_O, false); NotificationChannel calls = new NotificationChannel("calls", "Calls", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false, UID_O, false); NotificationChannel p = new NotificationChannel("p calls", "Calls", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, UID_P, p, true, false); + mHelper.createNotificationChannel(PKG_P, UID_P, p, true, false, UID_P, false); NotificationChannel channel = new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT); channel.setConversationId(messages.getId(), convoId); channel.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false, UID_O, false); NotificationChannel diffConvo = new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT); diffConvo.setConversationId(p.getId(), "different convo"); diffConvo.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, false, false); + mHelper.createNotificationChannel(PKG_P, UID_P, diffConvo, false, false, + SYSTEM_UID, true); NotificationChannel channel2 = new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT); channel2.setConversationId(calls.getId(), convoId); - mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false, UID_O, false); List<ConversationChannelWrapper> convos = mHelper.getConversations(IntArray.wrap(new int[] {0}), true); @@ -4667,13 +4886,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { String convoId = "convo"; NotificationChannel messages = new NotificationChannel("messages", "Messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false, UID_O, false); NotificationChannel channel = new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT); channel.setConversationId(messages.getId(), convoId); channel.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, false, false, + SYSTEM_UID, true); mHelper.permanentlyDeleteNotificationChannel(PKG_O, UID_O, "messages"); @@ -4704,7 +4924,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testGetConversations_noConversations() { NotificationChannel channel = new NotificationChannel("not_convo", "not_convo", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); assertThat(mHelper.getConversations(PKG_O, UID_O)).isEmpty(); } @@ -4713,15 +4933,15 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testGetConversations_noDisabledGroups() { NotificationChannelGroup group = new NotificationChannelGroup("a", "a"); group.setBlocked(true); - mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, false); + mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, false, SYSTEM_UID, true); NotificationChannel parent = new NotificationChannel("parent", "p", 1); - mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false, UID_O, false); NotificationChannel channel = new NotificationChannel("convo", "convo", IMPORTANCE_DEFAULT); channel.setConversationId("parent", "convo"); channel.setGroup(group.getId()); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); assertThat(mHelper.getConversations(PKG_O, UID_O)).isEmpty(); } @@ -4729,12 +4949,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testGetConversations_noDeleted() { NotificationChannel parent = new NotificationChannel("parent", "p", 1); - mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false, UID_O, false); NotificationChannel channel = new NotificationChannel("convo", "convo", IMPORTANCE_DEFAULT); channel.setConversationId("parent", "convo"); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); - mHelper.deleteNotificationChannel(PKG_O, UID_O, channel.getId()); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); + mHelper.deleteNotificationChannel(PKG_O, UID_O, channel.getId(), UID_O, false); assertThat(mHelper.getConversations(PKG_O, UID_O)).isEmpty(); } @@ -4742,12 +4962,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testGetConversations_noDemoted() { NotificationChannel parent = new NotificationChannel("parent", "p", 1); - mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false, UID_O, false); NotificationChannel channel = new NotificationChannel("convo", "convo", IMPORTANCE_DEFAULT); channel.setConversationId("parent", "convo"); channel.setDemoted(true); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); assertThat(mHelper.getConversations(PKG_O, UID_O)).isEmpty(); } @@ -4755,26 +4975,26 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testGetConversations() { NotificationChannelGroup group = new NotificationChannelGroup("acct", "account_name"); - mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, true); + mHelper.createNotificationChannelGroup(PKG_O, UID_O, group, true, UID_O, false); NotificationChannel messages = new NotificationChannel("messages", "Messages", IMPORTANCE_DEFAULT); messages.setGroup(group.getId()); - mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false, UID_O, false); NotificationChannel calls = new NotificationChannel("calls", "Calls", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false, UID_O, false); NotificationChannel channel = new NotificationChannel("A person", "A lovely person", IMPORTANCE_DEFAULT); channel.setGroup(group.getId()); channel.setConversationId(messages.getId(), channel.getName().toString()); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); NotificationChannel channel2 = new NotificationChannel("B person", "B fabulous person", IMPORTANCE_DEFAULT); channel2.setConversationId(calls.getId(), channel2.getName().toString()); - mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false, UID_O, false); Map<String, NotificationChannel> expected = new HashMap<>(); expected.put(channel.getId(), channel); @@ -4807,35 +5027,36 @@ public class PreferencesHelperTest extends UiServiceTestCase { String convoIdC = "convoC"; NotificationChannel messages = new NotificationChannel("messages", "Messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, messages, true, false, UID_O, false); NotificationChannel calls = new NotificationChannel("calls", "Calls", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, calls, true, false, UID_O, false); NotificationChannel channel = new NotificationChannel("A person msgs", "messages from A", IMPORTANCE_DEFAULT); channel.setConversationId(messages.getId(), convoId); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); NotificationChannel noMatch = new NotificationChannel("B person msgs", "messages from B", IMPORTANCE_DEFAULT); noMatch.setConversationId(messages.getId(), "different convo"); - mHelper.createNotificationChannel(PKG_O, UID_O, noMatch, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, noMatch, true, false, UID_O, false); NotificationChannel channel2 = new NotificationChannel("A person calls", "calls from A", IMPORTANCE_DEFAULT); channel2.setConversationId(calls.getId(), convoId); - mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false, UID_O, false); NotificationChannel channel3 = new NotificationChannel("C person msgs", "msgs from C", IMPORTANCE_DEFAULT); channel3.setConversationId(messages.getId(), convoIdC); - mHelper.createNotificationChannel(PKG_O, UID_O, channel3, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel3, true, false, UID_O, false); assertEquals(channel, mHelper.getNotificationChannel(PKG_O, UID_O, channel.getId(), false)); assertEquals(channel2, mHelper.getNotificationChannel(PKG_O, UID_O, channel2.getId(), false)); - List<String> deleted = mHelper.deleteConversations(PKG_O, UID_O, Set.of(convoId, convoIdC)); + List<String> deleted = mHelper.deleteConversations(PKG_O, UID_O, Set.of(convoId, convoIdC), + UID_O, false); assertEquals(3, deleted.size()); assertEquals(messages, @@ -4950,12 +5171,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { String channelId = "parent"; String name = "messages"; NotificationChannel fodderA = new NotificationChannel("a", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_O, UID_O, fodderA, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, fodderA, true, false, UID_O, false); NotificationChannel channel = new NotificationChannel(channelId, name, IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); NotificationChannel fodderB = new NotificationChannel("b", "b", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, fodderB, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, fodderB, true, false, UID_O, false); ArrayList<StatsEvent> events = new ArrayList<>(); mHelper.pullPackageChannelPreferencesStats(events); @@ -4980,11 +5201,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testPullPackageChannelPreferencesStats_one_to_one() { NotificationChannel channelA = new NotificationChannel("a", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_O, UID_O, channelA, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channelA, true, false, UID_O, false); NotificationChannel channelB = new NotificationChannel("b", "b", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_O, UID_O, channelB, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channelB, true, false, UID_O, false); NotificationChannel channelC = new NotificationChannel("c", "c", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_O, UID_O, channelC, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channelC, true, false, UID_O, false); List<String> channels = new LinkedList<>(Arrays.asList("a", "b", "c")); @@ -5008,7 +5229,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel parent = new NotificationChannel("parent", "messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false, UID_O, false); String channelId = String.format( CONVERSATION_CHANNEL_ID_FORMAT, parent.getId(), conversationId); @@ -5016,7 +5237,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel friend = new NotificationChannel(channelId, name, IMPORTANCE_DEFAULT); friend.setConversationId(parent.getId(), conversationId); - mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false, UID_O, false); ArrayList<StatsEvent> events = new ArrayList<>(); mHelper.pullPackageChannelPreferencesStats(events); @@ -5038,14 +5259,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testPullPackageChannelPreferencesStats_conversation_demoted() { NotificationChannel parent = new NotificationChannel("parent", "messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false, UID_O, false); String channelId = String.format( CONVERSATION_CHANNEL_ID_FORMAT, parent.getId(), "friend"); NotificationChannel friend = new NotificationChannel(channelId, "conversation", IMPORTANCE_DEFAULT); friend.setConversationId(parent.getId(), "friend"); friend.setDemoted(true); - mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, friend, true, false, UID_O, false); ArrayList<StatsEvent> events = new ArrayList<>(); mHelper.pullPackageChannelPreferencesStats(events); @@ -5067,14 +5288,14 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testPullPackageChannelPreferencesStats_conversation_priority() { NotificationChannel parent = new NotificationChannel("parent", "messages", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, parent, true, false, UID_O, false); String channelId = String.format( CONVERSATION_CHANNEL_ID_FORMAT, parent.getId(), "friend"); NotificationChannel friend = new NotificationChannel(channelId, "conversation", IMPORTANCE_DEFAULT); friend.setConversationId(parent.getId(), "friend"); friend.setImportantConversation(true); - mHelper.createNotificationChannel(PKG_O, UID_O, friend, false, false); + mHelper.createNotificationChannel(PKG_O, UID_O, friend, false, false, SYSTEM_UID, true); ArrayList<StatsEvent> events = new ArrayList<>(); mHelper.pullPackageChannelPreferencesStats(events); @@ -5096,11 +5317,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testPullPackagePreferencesStats_postPermissionMigration() { // make sure there's at least one channel for each package we want to test NotificationChannel channelA = new NotificationChannel("a", "a", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channelA, true, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channelA, true, false, + UID_N_MR1, false); NotificationChannel channelB = new NotificationChannel("b", "b", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_O, UID_O, channelB, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channelB, true, false, UID_O, false); NotificationChannel channelC = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, UID_P, channelC, true, false); + mHelper.createNotificationChannel(PKG_P, UID_P, channelC, true, false, UID_P, false); // build a collection of app permissions that should be passed in and used ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> appPermissions = new ArrayMap<>(); @@ -5143,7 +5365,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testUnlockNotificationChannelImportance() { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); channel.lockFields(USER_LOCKED_IMPORTANCE); assertTrue((channel.getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0); @@ -5155,11 +5377,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testUnlockAllNotificationChannels() { NotificationChannel channelA = new NotificationChannel("a", "a", IMPORTANCE_LOW); - mHelper.createNotificationChannel(PKG_O, UID_O, channelA, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, channelA, true, false, UID_O, false); NotificationChannel channelB = new NotificationChannel("b", "b", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, UID_P, channelB, true, false); + mHelper.createNotificationChannel(PKG_P, UID_P, channelB, true, false, UID_P, false); NotificationChannel channelC = new NotificationChannel("c", "c", IMPORTANCE_HIGH); - mHelper.createNotificationChannel(PKG_P, UID_O, channelC, false, false); + mHelper.createNotificationChannel(PKG_P, UID_O, channelC, false, false, UID_O, false); channelA.lockFields(USER_LOCKED_IMPORTANCE); channelB.lockFields(USER_LOCKED_IMPORTANCE); @@ -5178,11 +5400,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void createNotificationChannel_updateDifferent_requestsSort() { NotificationChannel original = new NotificationChannel("id", "Bah", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, 0, original, true, false); + mHelper.createNotificationChannel(PKG_P, 0, original, true, false, 0, false); clearInvocations(mHandler); NotificationChannel updated = new NotificationChannel("id", "Wow", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, 0, updated, true, false); + mHelper.createNotificationChannel(PKG_P, 0, updated, true, false, 0, false); verify(mHandler).requestSort(); } @@ -5190,11 +5412,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void createNotificationChannel_updateSame_doesNotRequestSort() { NotificationChannel original = new NotificationChannel("id", "Bah", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, 0, original, true, false); + mHelper.createNotificationChannel(PKG_P, 0, original, true, false, 0, false); clearInvocations(mHandler); NotificationChannel same = new NotificationChannel("id", "Bah", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, 0, same, true, false); + mHelper.createNotificationChannel(PKG_P, 0, same, true, false, 0, false); verifyZeroInteractions(mHandler); } @@ -5202,11 +5424,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void updateNotificationChannel_different_requestsSort() { NotificationChannel original = new NotificationChannel("id", "Bah", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, 0, original, true, false); + mHelper.createNotificationChannel(PKG_P, 0, original, true, false, 0, false); clearInvocations(mHandler); NotificationChannel updated = new NotificationChannel("id", "Wow", IMPORTANCE_DEFAULT); - mHelper.updateNotificationChannel(PKG_P, 0, updated, false); + mHelper.updateNotificationChannel(PKG_P, 0, updated, false, 0, false); verify(mHandler).requestSort(); } @@ -5214,7 +5436,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void updateNotificationChannel_same_doesNotRequestSort() { NotificationChannel original = new NotificationChannel("id", "Bah", IMPORTANCE_DEFAULT); - mHelper.createNotificationChannel(PKG_P, 0, original, true, false); + mHelper.createNotificationChannel(PKG_P, 0, original, true, false, 0, false); clearInvocations(mHandler); // Note: Creating a NotificationChannel identical to the original is not equals(), because // of mOriginalImportance. So we create a "true copy" instead. @@ -5224,7 +5446,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel same = NotificationChannel.CREATOR.createFromParcel(parcel); parcel.recycle(); - mHelper.updateNotificationChannel(PKG_P, 0, same, false); + mHelper.updateNotificationChannel(PKG_P, 0, same, false, 0, false); verifyZeroInteractions(mHandler); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java index 61a6985d473e..9f4eee7e332f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java @@ -67,7 +67,13 @@ public class TestableNotificationManagerService extends NotificationManagerServi @Override protected boolean isCallerSystemOrPhone() { countSystemChecks++; - return isSystemUid; + return isSystemUid || isSystemAppId; + } + + @Override + protected boolean isCallerIsSystemOrSystemUi() { + countSystemChecks++; + return isSystemUid || isSystemAppId; } @Override diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java new file mode 100644 index 000000000000..4a1435f9ee64 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java @@ -0,0 +1,132 @@ +/* + * 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.server.notification; + +import android.content.pm.PackageManager; + +import com.android.os.dnd.DNDPolicyProto; + +import com.google.protobuf.InvalidProtocolBufferException; + +import java.util.ArrayList; +import java.util.List; + +/** + * ZenModeEventLoggerFake extends ZenModeEventLogger for ease of verifying logging output. This + * class behaves exactly the same as its parent class except that instead of actually logging, it + * stores the full information at time of log whenever something would be logged. + */ +public class ZenModeEventLoggerFake extends ZenModeEventLogger { + // A record of the contents of each event we'd log, stored by recording the ChangeState object + // at the time of the log. + private List<ZenStateChanges> mChanges = new ArrayList<>(); + + public ZenModeEventLoggerFake(PackageManager pm) { + super(pm); + } + + @Override + void logChanges() { + // current change state being logged + mChanges.add(mChangeState.copy()); + } + + // Reset the state of the logger (remove all changes). + public void reset() { + mChanges = new ArrayList<>(); + } + + // Returns the number of changes logged. + public int numLoggedChanges() { + return mChanges.size(); + } + + // is index i out of range for the set of changes we have + private boolean outOfRange(int i) { + return i < 0 || i >= mChanges.size(); + } + + // Throw an exception if provided index is out of range + private void checkInRange(int i) throws IllegalArgumentException { + if (outOfRange(i)) { + throw new IllegalArgumentException("invalid index for logged event: " + i); + } + } + + // Get the UiEvent ID of the i'th logged event. + public int getEventId(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).getEventId().getId(); + } + + // Get the previous zen mode associated with the change at event i. + public int getPrevZenMode(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).mPrevZenMode; + } + + // Get the new zen mode associated with the change at event i. + public int getNewZenMode(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).mNewZenMode; + } + + // Get the changed rule type associated with event i. + public int getChangedRuleType(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).getChangedRuleType(); + } + + public int getNumRulesActive(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).getNumRulesActive(); + } + + public boolean getFromSystemOrSystemUi(int i) throws IllegalArgumentException { + // While this isn't a logged output value, it's still helpful to check in tests. + checkInRange(i); + return mChanges.get(i).mFromSystemOrSystemUi; + } + + public boolean getIsUserAction(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).getIsUserAction(); + } + + public int getPackageUid(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).getPackageUid(); + } + + // Get the DNDPolicyProto (unmarshaled from bytes) associated with event i. + // Note that in creation of the log, we use a notification.proto mirror of DNDPolicyProto, + // but here we use the actual logging-side proto to make sure they continue to match. + public DNDPolicyProto getPolicyProto(int i) throws IllegalArgumentException { + checkInRange(i); + byte[] policyBytes = mChanges.get(i).getDNDPolicyProto(); + try { + return DNDPolicyProto.parseFrom(policyBytes); + } catch (InvalidProtocolBufferException e) { + return null; // couldn't turn it into proto + } + } + + public boolean getAreChannelsBypassing(int i) throws IllegalArgumentException { + checkInRange(i); + return mChanges.get(i).getAreChannelsBypassing(); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 2c95bdee3454..dedb8f170ee0 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -34,16 +34,21 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCRE import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; +import static android.service.notification.Condition.STATE_FALSE; import static android.service.notification.Condition.STATE_TRUE; import static android.util.StatsLog.ANNOTATION_ID_IS_UID; +import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; import static com.android.os.dnd.DNDModeProto.CHANNELS_BYPASSING_FIELD_NUMBER; import static com.android.os.dnd.DNDModeProto.ENABLED_FIELD_NUMBER; import static com.android.os.dnd.DNDModeProto.ID_FIELD_NUMBER; import static com.android.os.dnd.DNDModeProto.UID_FIELD_NUMBER; import static com.android.os.dnd.DNDModeProto.ZEN_MODE_FIELD_NUMBER; +import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED; import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG; +import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW; +import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW; import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE; import static junit.framework.Assert.assertEquals; @@ -104,9 +109,12 @@ import android.util.StatsEvent; import android.util.Xml; import com.android.internal.R; +import com.android.internal.config.sysui.TestableFlagResolver; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.os.dnd.DNDPolicyProto; +import com.android.os.dnd.DNDProtoEnums; import com.android.server.UiServiceTestCase; import com.android.server.notification.ManagedServices.UserProfiles; @@ -152,6 +160,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { private ContentResolver mContentResolver; @Mock AppOpsManager mAppOps; private WrappedSysUiStatsEvent.WrappedBuilderFactory mStatsEventBuilderFactory; + TestableFlagResolver mTestFlagResolver = new TestableFlagResolver(); + ZenModeEventLoggerFake mZenModeEventLogger; @Before public void setUp() throws PackageManager.NameNotFoundException { @@ -176,8 +186,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { AppGlobals.getPackageManager()); mConditionProviders.addSystemProvider(new CountdownConditionProvider()); mConditionProviders.addSystemProvider(new ScheduleConditionProvider()); + mZenModeEventLogger = new ZenModeEventLoggerFake(mPackageManager); mZenModeHelper = new ZenModeHelper(mContext, mTestableLooper.getLooper(), - mConditionProviders, mStatsEventBuilderFactory); + mConditionProviders, mStatsEventBuilderFactory, mTestFlagResolver, + mZenModeEventLogger); ResolveInfo ri = new ResolveInfo(); ri.activityInfo = new ActivityInfo(); @@ -188,6 +200,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( new String[] {pkg}); mZenModeHelper.mPm = mPackageManager; + + mZenModeEventLogger.reset(); } private XmlResourceParser getDefaultConfigParser() throws IOException, XmlPullParserException { @@ -224,7 +238,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.writeXml(serializer, false, version, UserHandle.USER_ALL); serializer.endDocument(); serializer.flush(); - mZenModeHelper.setConfig(new ZenModeConfig(), null, "writing xml"); + mZenModeHelper.setConfig(new ZenModeConfig(), null, "writing xml", Process.SYSTEM_UID, + true); return baos; } @@ -239,7 +254,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { serializer.flush(); ZenModeConfig newConfig = new ZenModeConfig(); newConfig.user = userId; - mZenModeHelper.setConfig(newConfig, null, "writing xml"); + mZenModeHelper.setConfig(newConfig, null, "writing xml", Process.SYSTEM_UID, true); return baos; } @@ -822,14 +837,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mAudioManager = mAudioManager; setupZenConfig(); - // Change the config a little bit, but enough that it would turn zen mode on - ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); - newConfig.manualRule = new ZenModeConfig.ZenRule(); - newConfig.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - newConfig.manualRule.enabled = true; - mZenModeHelper.setConfig(newConfig, null, "test"); + // Turn manual zen mode on + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null, + "test", CUSTOM_PKG_UID, false); - // audio manager shouldn't do anything until the handler processes its messagse + // audio manager shouldn't do anything until the handler processes its messages verify(mAudioManager, never()).updateRingerModeAffectedStreamsInternal(); // now process the looper's messages @@ -993,7 +1005,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { List<StatsEvent> events = new LinkedList<>(); mZenModeHelper.pullRules(events); - mZenModeHelper.removeAutomaticZenRule(CUSTOM_RULE_ID, "test"); + mZenModeHelper.removeAutomaticZenRule(CUSTOM_RULE_ID, "test", CUSTOM_PKG_UID, false); assertTrue(-1 == mZenModeHelper.mRulesUidCache.getOrDefault(CUSTOM_PKG_NAME + "|" + 0, -1)); } @@ -1044,12 +1056,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { config10.user = 10; config10.allowAlarms = true; config10.allowMedia = true; - mZenModeHelper.setConfig(config10, null, "writeXml"); + mZenModeHelper.setConfig(config10, null, "writeXml", Process.SYSTEM_UID, true); ZenModeConfig config11 = mZenModeHelper.mConfig.copy(); config11.user = 11; config11.allowAlarms = false; config11.allowMedia = false; - mZenModeHelper.setConfig(config11, null, "writeXml"); + mZenModeHelper.setConfig(config11, null, "writeXml", Process.SYSTEM_UID, true); // Backup user 10 and reset values. ByteArrayOutputStream baos = writeXmlAndPurgeForUser(null, 10); @@ -1552,7 +1564,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig config = new ZenModeConfig(); config.automaticRules = new ArrayMap<>(); mZenModeHelper.mConfig = config; - mZenModeHelper.updateDefaultZenRules(); // shouldn't throw null pointer + mZenModeHelper.updateDefaultZenRules( + Process.SYSTEM_UID, true); // shouldn't throw null pointer mZenModeHelper.pullRules(events); // shouldn't throw null pointer } @@ -1577,7 +1590,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { autoRules.put(SCHEDULE_DEFAULT_RULE_ID, updatedDefaultRule); mZenModeHelper.mConfig.automaticRules = autoRules; - mZenModeHelper.updateDefaultZenRules(); + mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID, true); assertEquals(updatedDefaultRule, mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID)); } @@ -1603,7 +1616,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { autoRules.put(SCHEDULE_DEFAULT_RULE_ID, updatedDefaultRule); mZenModeHelper.mConfig.automaticRules = autoRules; - mZenModeHelper.updateDefaultZenRules(); + mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID, true); assertEquals(updatedDefaultRule, mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID)); } @@ -1630,7 +1643,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { autoRules.put(SCHEDULE_DEFAULT_RULE_ID, customDefaultRule); mZenModeHelper.mConfig.automaticRules = autoRules; - mZenModeHelper.updateDefaultZenRules(); + mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID, true); ZenModeConfig.ZenRule ruleAfterUpdating = mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID); assertEquals(customDefaultRule.enabled, ruleAfterUpdating.enabled); @@ -1653,7 +1666,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); // We need the package name to be something that's not "android" so there aren't any // existing rules under that package. - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test", + CUSTOM_PKG_UID, false); assertNotNull(id); } try { @@ -1663,7 +1677,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test", + CUSTOM_PKG_UID, false); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { // yay @@ -1683,7 +1698,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(si), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test", + CUSTOM_PKG_UID, false); assertNotNull(id); } try { @@ -1693,7 +1709,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test", + CUSTOM_PKG_UID, false); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { // yay @@ -1713,7 +1730,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(si), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test", + CUSTOM_PKG_UID, false); assertNotNull(id); } try { @@ -1723,7 +1741,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test", + CUSTOM_PKG_UID, false); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { // yay @@ -1738,7 +1757,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test", + Process.SYSTEM_UID, true); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); @@ -1758,7 +1778,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ComponentName("android", "ScheduleConditionProvider"), ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test", + Process.SYSTEM_UID, true); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); @@ -1781,9 +1802,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test", + CUSTOM_PKG_UID, false); mZenModeHelper.setAutomaticZenRuleState(zenRule.getConditionId(), - new Condition(zenRule.getConditionId(), "", STATE_TRUE)); + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + CUSTOM_PKG_UID, false); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertEquals(STATE_TRUE, ruleInConfig.condition.state); @@ -1798,7 +1821,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test", + CUSTOM_PKG_UID, false); AutomaticZenRule zenRule2 = new AutomaticZenRule("NEW", null, @@ -1807,7 +1831,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - mZenModeHelper.updateAutomaticZenRule(id, zenRule2, ""); + mZenModeHelper.updateAutomaticZenRule(id, zenRule2, "", CUSTOM_PKG_UID, false); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertEquals("NEW", ruleInConfig.name); @@ -1822,14 +1846,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test", + CUSTOM_PKG_UID, false); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertTrue(ruleInConfig != null); assertEquals(zenRule.getName(), ruleInConfig.name); - mZenModeHelper.removeAutomaticZenRule(id, "test"); + mZenModeHelper.removeAutomaticZenRule(id, "test", CUSTOM_PKG_UID, false); assertNull(mZenModeHelper.mConfig.automaticRules.get(id)); } @@ -1841,14 +1866,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test", + CUSTOM_PKG_UID, false); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertTrue(ruleInConfig != null); assertEquals(zenRule.getName(), ruleInConfig.name); - mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), "test"); + mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), "test", + CUSTOM_PKG_UID, false); assertNull(mZenModeHelper.mConfig.automaticRules.get(id)); } @@ -1863,15 +1890,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ComponentName("android", "ScheduleConditionProvider"), sharedUri, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test"); + String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test", + Process.SYSTEM_UID, true); AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", new ComponentName("android", "ScheduleConditionProvider"), sharedUri, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2, "test"); + String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2, "test", + Process.SYSTEM_UID, true); Condition condition = new Condition(sharedUri, "", STATE_TRUE); - mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition); + mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition, Process.SYSTEM_UID, true); for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) { if (rule.id.equals(id)) { @@ -1884,17 +1913,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { } } - condition = new Condition(sharedUri, "", Condition.STATE_FALSE); - mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition); + condition = new Condition(sharedUri, "", STATE_FALSE); + mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition, Process.SYSTEM_UID, true); for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) { if (rule.id.equals(id)) { assertNotNull(rule.condition); - assertTrue(rule.condition.state == Condition.STATE_FALSE); + assertTrue(rule.condition.state == STATE_FALSE); } if (rule.id.equals(id2)) { assertNotNull(rule.condition); - assertTrue(rule.condition.state == Condition.STATE_FALSE); + assertTrue(rule.condition.state == STATE_FALSE); } } } @@ -1904,14 +1933,510 @@ public class ZenModeHelperTest extends UiServiceTestCase { setupZenConfig(); // note that caller=null because that's how it comes in from NMS.setZenMode - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null, ""); + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null, "", + Process.SYSTEM_UID, true); // confirm that setting zen mode via setManualZenMode changed the zen mode correctly assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode); // and also that it works to turn it back off again - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, ""); + mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "", + Process.SYSTEM_UID, true); + + assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode); + } + + @Test + public void testZenModeEventLog_setManualZenMode() throws IllegalArgumentException { + mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); + setupZenConfig(); + + // Turn zen mode on (to important_interruptions) + // Need to additionally call the looper in order to finish the post-apply-config process + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null, "", + Process.SYSTEM_UID, true); + + // Now turn zen mode off, but via a different package UID -- this should get registered as + // "not an action by the user" because some other app is changing zen mode + mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "", CUSTOM_PKG_UID, + false); + + // In total, this should be 2 loggable changes + assertEquals(2, mZenModeEventLogger.numLoggedChanges()); + + // we expect the following changes from turning zen mode on: + // - manual rule added + // - zen mode -> ZEN_MODE_IMPORTANT_INTERRUPTIONS + // This should combine to 1 log event (zen mode turns on) with the following properties: + // - event ID: DND_TURNED_ON + // - new zen mode = important interruptions; prev zen mode = off + // - changed rule type = manual + // - rules active = 1 + // - user action = true (system-based turning zen mode on) + // - package uid = system (as set above) + // - resulting DNDPolicyProto the same as the values in setupZenConfig() + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(0)); + assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); + assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); + assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0)); + assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); + assertTrue(mZenModeEventLogger.getFromSystemOrSystemUi(0)); + assertTrue(mZenModeEventLogger.getIsUserAction(0)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); + checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); + + // and from turning zen mode off: + // - event ID: DND_TURNED_OFF + // - new zen mode = off; previous = important interruptions + // - changed rule type = manual + // - rules active = 0 + // - user action = false + // - package uid = custom one passed in above + // - DNDPolicyProto still the same + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), + mZenModeEventLogger.getEventId(1)); + assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1)); + assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1)); + assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(1)); + assertEquals(0, mZenModeEventLogger.getNumRulesActive(1)); + assertFalse(mZenModeEventLogger.getIsUserAction(1)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1)); + checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); + } + + @Test + public void testZenModeEventLog_automaticRules() throws IllegalArgumentException { + mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); + setupZenConfig(); + + // Add a new automatic zen rule that's enabled + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, + "test", Process.SYSTEM_UID, true); + + // Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + Process.SYSTEM_UID, true); + + // Event 2: "User" turns off the automatic rule (sets it to not enabled) + zenRule.setEnabled(false); + mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID, true); + + // Add a new system rule + AutomaticZenRule systemRule = new AutomaticZenRule("systemRule", + null, + new ComponentName("android", "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String systemId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), systemRule, + "test", Process.SYSTEM_UID, true); + + // Event 3: turn on the system rule + mZenModeHelper.setAutomaticZenRuleState(systemId, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + Process.SYSTEM_UID, true); + + // Event 4: "User" deletes the rule + mZenModeHelper.removeAutomaticZenRule(systemId, "", Process.SYSTEM_UID, true); + + // In total, this represents 4 events + assertEquals(4, mZenModeEventLogger.numLoggedChanges()); + + // We should see an event from the automatic rule turning on; it should have the following + // properties: + // - event ID: DND_TURNED_ON + // - zen mode: OFF -> IMPORTANT_INTERRUPTIONS + // - automatic rule change + // - 1 rule (newly) active + // - automatic (is not a user action) + // - package UID is written to be the rule *owner* even though it "comes from system" + // - zen policy is the same as the set-up zen config + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(0)); + assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); + assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); + assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(0)); + assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); + assertFalse(mZenModeEventLogger.getIsUserAction(0)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); + checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); + + // When the automatic rule is disabled, this should turn off zen mode and also count as a + // user action. + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), + mZenModeEventLogger.getEventId(1)); + assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1)); + assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1)); + assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(1)); + assertEquals(0, mZenModeEventLogger.getNumRulesActive(1)); + assertTrue(mZenModeEventLogger.getIsUserAction(1)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); + checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); + + // When the system rule is enabled, this counts as an automatic action that comes from the + // system and turns on DND + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(2)); + assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(2)); + assertEquals(1, mZenModeEventLogger.getNumRulesActive(2)); + assertFalse(mZenModeEventLogger.getIsUserAction(2)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); + + // When the system rule is deleted, we consider this a user action that turns DND off + // (again) + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), + mZenModeEventLogger.getEventId(3)); + assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(3)); + assertEquals(0, mZenModeEventLogger.getNumRulesActive(3)); + assertTrue(mZenModeEventLogger.getIsUserAction(3)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(3)); + } + + @Test + public void testZenModeEventLog_policyChanges() throws IllegalArgumentException { + mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); + setupZenConfig(); + + // First just turn zen mode on + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null, "", + Process.SYSTEM_UID, true); + + // Now change the policy slightly; want to confirm that this'll be reflected in the logs + ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); + newConfig.allowAlarms = true; + newConfig.allowRepeatCallers = false; + mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), Process.SYSTEM_UID, + true); + + // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode + // is off. + mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "", + Process.SYSTEM_UID, true); + + // Change the policy again + newConfig.allowMessages = false; + newConfig.allowRepeatCallers = true; + mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), Process.SYSTEM_UID, + true); + + // Total events: we only expect ones for turning on, changing policy, and turning off + assertEquals(3, mZenModeEventLogger.numLoggedChanges()); + + // The first event is just turning DND on; make sure the policy is what we expect there + // before it changes in the next stage + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(0)); + checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); + + // Second message where we change the policy: + // - DND_POLICY_CHANGED (indicates only the policy changed and nothing else) + // - rule type: unknown (it's a policy change, not a rule change) + // - user action (because it comes from a "system" uid) + // - check the specific things changed above + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_POLICY_CHANGED.getId(), + mZenModeEventLogger.getEventId(1)); + assertEquals(DNDProtoEnums.UNKNOWN_RULE, mZenModeEventLogger.getChangedRuleType(1)); + assertTrue(mZenModeEventLogger.getIsUserAction(1)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); + DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(1); + assertEquals(STATE_ALLOW, dndProto.getAlarms().getNumber()); + assertEquals(STATE_DISALLOW, dndProto.getRepeatCallers().getNumber()); + + // The third and final event should turn DND off + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), + mZenModeEventLogger.getEventId(2)); + + // There should be no fourth event for changing the policy the second time. + } + + @Test + public void testZenModeEventLog_ruleCounts() throws IllegalArgumentException { + mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); + setupZenConfig(); + + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test", + Process.SYSTEM_UID, true); + + // Rule 2, same as rule 1 + AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, "test", + Process.SYSTEM_UID, true); + + // Rule 3, has stricter settings than the default settings + ZenModeConfig ruleConfig = mZenModeHelper.mConfig.copy(); + ruleConfig.allowReminders = false; + ruleConfig.allowCalls = false; + ruleConfig.allowMessages = false; + AutomaticZenRule zenRule3 = new AutomaticZenRule("name3", + null, + new ComponentName("android", "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + ruleConfig.toZenPolicy(), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id3 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule3, "test", + Process.SYSTEM_UID, true); + + // First: turn on rule 1 + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + Process.SYSTEM_UID, true); + + // Second: turn on rule 2 + mZenModeHelper.setAutomaticZenRuleState(id2, + new Condition(zenRule2.getConditionId(), "", STATE_TRUE), + Process.SYSTEM_UID, true); + + // Third: turn on rule 3 + mZenModeHelper.setAutomaticZenRuleState(id3, + new Condition(zenRule3.getConditionId(), "", STATE_TRUE), + Process.SYSTEM_UID, true); + + // Fourth: Turn *off* rule 2 + mZenModeHelper.setAutomaticZenRuleState(id2, + new Condition(zenRule2.getConditionId(), "", STATE_FALSE), + Process.SYSTEM_UID, true); + + // This should result in a total of four events + assertEquals(4, mZenModeEventLogger.numLoggedChanges()); + + // Event 1: rule 1 turns on. We expect this to turn on DND (zen mode) overall, so that's + // what the event should reflect. At this time, the policy is the same as initial setup. + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(0)); + assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); + assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); + assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); + assertFalse(mZenModeEventLogger.getIsUserAction(0)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); + checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); + + // Event 2: rule 2 turns on. This should not change anything about the policy, so the only + // change is that there are more rules active now. + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId(), + mZenModeEventLogger.getEventId(1)); + assertEquals(2, mZenModeEventLogger.getNumRulesActive(1)); + assertFalse(mZenModeEventLogger.getIsUserAction(1)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1)); + checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); + + // Event 3: rule 3 turns on. This should trigger a policy change, and be classified as such, + // but meanwhile also change the number of active rules. + // Rule 3 is also set up to be a "system"-owned rule, so the caller UID should remain system + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_POLICY_CHANGED.getId(), + mZenModeEventLogger.getEventId(2)); + assertEquals(3, mZenModeEventLogger.getNumRulesActive(2)); + assertFalse(mZenModeEventLogger.getIsUserAction(2)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); + DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(2); + assertEquals(STATE_DISALLOW, dndProto.getReminders().getNumber()); + assertEquals(STATE_DISALLOW, dndProto.getCalls().getNumber()); + assertEquals(STATE_DISALLOW, dndProto.getMessages().getNumber()); + + // Event 4: rule 2 turns off. Because rule 3 is still on and stricter than rule 1 (also + // still on), there should be no policy change as a result of rule 2 going away. Therefore + // this event should again only be an active rule change. + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId(), + mZenModeEventLogger.getEventId(3)); + assertEquals(2, mZenModeEventLogger.getNumRulesActive(3)); + assertFalse(mZenModeEventLogger.getIsUserAction(3)); + } + + @Test + public void testZenModeEventLog_noLogWithNoConfigChange() throws IllegalArgumentException { + // If evaluateZenMode is called independently of a config change, don't log. + mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); + setupZenConfig(); + + // Artificially turn zen mode "on". Re-evaluating zen mode should cause it to turn back off + // given that we don't have any zen rules active. + mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + mZenModeHelper.evaluateZenMode("test", true); + + // Check that the change actually took: zen mode should be off now assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode); + + // but still, nothing should've been logged + assertEquals(0, mZenModeEventLogger.numLoggedChanges()); + } + + @Test + public void testZenModeEventLog_reassignUid() throws IllegalArgumentException { + // Test that, only in specific cases, we reassign the calling UID to one associated with + // the automatic rule owner. + mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); + setupZenConfig(); + + // Rule 1, owned by a package + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test", + Process.SYSTEM_UID, true); + + // Rule 2, same as rule 1 but owned by the system + AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", + null, + new ComponentName("android", "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, "test", + Process.SYSTEM_UID, true); + + // Turn on rule 1; call looks like it's from the system. Because setting a condition is + // typically an automatic (non-user-initiated) action, expect the calling UID to be + // re-evaluated to the one associat.d with CUSTOM_PKG_NAME. + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + Process.SYSTEM_UID, true); + + // Second: turn on rule 2. This is a system-owned rule and the UID should not be modified + // (nor even looked up; the mock PackageManager won't handle "android" as input). + mZenModeHelper.setAutomaticZenRuleState(id2, + new Condition(zenRule2.getConditionId(), "", STATE_TRUE), + Process.SYSTEM_UID, true); + + // Disable rule 1. Because this looks like a user action, the UID should not be modified + // from the system-provided one. + zenRule.setEnabled(false); + mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID, true); + + // Add a manual rule. Any manual rule changes should not get calling uids reassigned. + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null, "", + CUSTOM_PKG_UID, false); + + // Change rule 2's condition, but from some other UID. Since it doesn't look like it's from + // the system, we keep the UID info. + mZenModeHelper.setAutomaticZenRuleState(id2, + new Condition(zenRule2.getConditionId(), "", STATE_FALSE), + 12345, false); + + // That was 5 events total + assertEquals(5, mZenModeEventLogger.numLoggedChanges()); + + // The first event (activating rule 1) should be of type "zen mode turns on", automatic, + // have a package UID of CUSTOM_PKG_UID + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(0)); + assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(0)); + assertFalse(mZenModeEventLogger.getIsUserAction(0)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); + + // The second event (activating rule 2) should have similar other properties but the UID + // should be system. + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId(), + mZenModeEventLogger.getEventId(1)); + assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(1)); + assertFalse(mZenModeEventLogger.getIsUserAction(1)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); + + // Third event: disable rule 1. This looks like a user action so UID should be left alone. + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId(), + mZenModeEventLogger.getEventId(2)); + assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(2)); + assertTrue(mZenModeEventLogger.getIsUserAction(2)); + assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); + + // Fourth event: turns on manual mode. Doesn't change effective policy so this is just a + // change in active rules. Confirm that the package UID is left unchanged. + // Because it's a manual mode change not from the system, isn't considered a user action. + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId(), + mZenModeEventLogger.getEventId(3)); + assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(3)); + assertFalse(mZenModeEventLogger.getIsUserAction(3)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(3)); + + // Fourth event: changed condition on rule 2 (turning it off via condition). + // This comes from a random different UID so we expect that to remain untouched. + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId(), + mZenModeEventLogger.getEventId(4)); + assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(4)); + assertFalse(mZenModeEventLogger.getIsUserAction(4)); + assertEquals(12345, mZenModeEventLogger.getPackageUid(4)); + } + + @Test + public void testZenModeEventLog_channelsBypassingChanges() { + // Verify that the right thing happens when the canBypassDnd value changes. + mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); + setupZenConfig(); + + // Turn on zen mode with a manual rule with an enabler set. This should *not* count + // as a user action, and *should* get its UID reassigned. + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + CUSTOM_PKG_NAME, "", Process.SYSTEM_UID, true); + + // Now change apps bypassing to true + ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); + newConfig.areChannelsBypassingDnd = true; + mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), Process.SYSTEM_UID, + true); + + // and then back to false, all without changing anything else + newConfig.areChannelsBypassingDnd = false; + mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), Process.SYSTEM_UID, + true); + + // Turn off manual mode, call from a package: don't reset UID even though enabler is set + mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, + CUSTOM_PKG_NAME, "", 12345, false); + + // And likewise when turning it back on again + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + CUSTOM_PKG_NAME, "", 12345, false); + + // These are 5 events in total. + assertEquals(5, mZenModeEventLogger.numLoggedChanges()); + + // First event: turns on, UID reassigned for manual mode + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(0)); + assertFalse(mZenModeEventLogger.getIsUserAction(0)); + assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); + + // Second event should be a policy-only change with are channels bypassing = true + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_POLICY_CHANGED.getId(), + mZenModeEventLogger.getEventId(1)); + assertTrue(mZenModeEventLogger.getAreChannelsBypassing(1)); + + // Third event also a policy-only change but with channels bypassing now false + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_POLICY_CHANGED.getId(), + mZenModeEventLogger.getEventId(2)); + assertFalse(mZenModeEventLogger.getAreChannelsBypassing(2)); + + // Fourth event: should turn DND off, not have UID reassigned + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), + mZenModeEventLogger.getEventId(3)); + assertFalse(mZenModeEventLogger.getIsUserAction(3)); + assertEquals(12345, mZenModeEventLogger.getPackageUid(3)); + + // Fifth event: turn DND back on, not have UID reassigned + assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), + mZenModeEventLogger.getEventId(4)); + assertFalse(mZenModeEventLogger.getIsUserAction(4)); + assertEquals(12345, mZenModeEventLogger.getPackageUid(4)); } private void setupZenConfig() { @@ -1921,7 +2446,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.allowSystem = false; mZenModeHelper.mConfig.allowReminders = true; mZenModeHelper.mConfig.allowCalls = true; + mZenModeHelper.mConfig.allowCallsFrom = PRIORITY_SENDERS_STARRED; mZenModeHelper.mConfig.allowMessages = true; + mZenModeHelper.mConfig.allowConversations = true; mZenModeHelper.mConfig.allowEvents = true; mZenModeHelper.mConfig.allowRepeatCallers = true; mZenModeHelper.mConfig.suppressedVisualEffects = SUPPRESSED_EFFECT_BADGE; @@ -1935,12 +2462,33 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertFalse(mZenModeHelper.mConfig.allowSystem); assertTrue(mZenModeHelper.mConfig.allowReminders); assertTrue(mZenModeHelper.mConfig.allowCalls); + assertEquals(PRIORITY_SENDERS_STARRED, mZenModeHelper.mConfig.allowCallsFrom); assertTrue(mZenModeHelper.mConfig.allowMessages); + assertTrue(mZenModeHelper.mConfig.allowConversations); assertTrue(mZenModeHelper.mConfig.allowEvents); assertTrue(mZenModeHelper.mConfig.allowRepeatCallers); assertEquals(SUPPRESSED_EFFECT_BADGE, mZenModeHelper.mConfig.suppressedVisualEffects); } + private void checkDndProtoMatchesSetupZenConfig(DNDPolicyProto dndProto) { + assertEquals(STATE_DISALLOW, dndProto.getAlarms().getNumber()); + assertEquals(STATE_DISALLOW, dndProto.getMedia().getNumber()); + assertEquals(STATE_DISALLOW, dndProto.getSystem().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getReminders().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getCalls().getNumber()); + assertEquals(PEOPLE_STARRED, dndProto.getAllowCallsFrom().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getMessages().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getEvents().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getRepeatCallers().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getFullscreen().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getLights().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getPeek().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getStatusBar().getNumber()); + assertEquals(STATE_DISALLOW, dndProto.getBadge().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getAmbient().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getNotificationList().getNumber()); + } + /** * Wrapper to use TypedXmlPullParser as XmlResourceParser for Resources.getXml() */ diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java index 37c4b3787835..5c3102d870d0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java @@ -23,12 +23,15 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -389,6 +392,19 @@ public class ActivityMetricsLaunchObserverTests extends WindowTestsBase { } @Test + public void testInTaskActivityStart() { + mTrampolineActivity.setVisible(true); + doReturn(true).when(mTrampolineActivity).isReportedDrawn(); + spyOn(mActivityMetricsLogger); + + onActivityLaunched(mTopActivity); + transitToDrawnAndVerifyOnLaunchFinished(mTopActivity); + + verify(mActivityMetricsLogger, timeout(TIMEOUT_MS)).logInTaskActivityStart( + any(), anyBoolean(), anyInt()); + } + + @Test public void testOnActivityLaunchFinishedTrampoline() { onActivityLaunchedTrampoline(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 95fc0faf50ba..2671e771aa59 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -645,7 +645,7 @@ public class ActivityStarterTests extends WindowTestsBase { runAndVerifyBackgroundActivityStartsSubtest("allowed_noStartsAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -659,7 +659,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_unsupportedUsecase_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -673,7 +673,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callingUidProcessStateTop_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -687,7 +687,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_realCallingUidProcessStateTop_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -701,7 +701,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_hasForegroundActivities_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - true, false, false, false, false, false, false); + true, false, false, false, false, false, false, false); } /** @@ -715,7 +715,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_pinned_singleinstance_aborted", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, true, false); + false, false, false, false, false, false, true, false); } /** @@ -729,7 +729,7 @@ public class ActivityStarterTests extends WindowTestsBase { runAndVerifyBackgroundActivityStartsSubtest("disallowed_rootUid_notAborted", false, Process.ROOT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -743,7 +743,7 @@ public class ActivityStarterTests extends WindowTestsBase { runAndVerifyBackgroundActivityStartsSubtest("disallowed_systemUid_notAborted", false, Process.SYSTEM_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -757,7 +757,7 @@ public class ActivityStarterTests extends WindowTestsBase { runAndVerifyBackgroundActivityStartsSubtest("disallowed_nfcUid_notAborted", false, Process.NFC_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -772,7 +772,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callingUidHasVisibleWindow_notAborted", false, UNIMPORTANT_UID, true, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -788,7 +788,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_realCallingUidHasVisibleWindow_abortedInU", true, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, true, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -803,7 +803,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callerIsRecents_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, true, false, false, false, false, false); + false, true, false, false, false, false, false, false); } /** @@ -818,7 +818,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callerIsAllowed_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, true, false, false, false, false); + false, false, true, false, false, false, false, false); } /** @@ -834,7 +834,7 @@ public class ActivityStarterTests extends WindowTestsBase { false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, true, false, false, false); + false, false, false, true, false, false, false, false); } /** @@ -850,7 +850,23 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callingPackageNameIsDeviceOwner_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, true, false, false); + false, false, false, false, true, false, false, false); + } + + /** + * This test ensures that supported usecases aren't aborted when background starts are + * disallowed. Each scenarios tests one condition that makes them supported in isolation. In + * this case the caller is a affiliated profile owner. + */ + @Test + public void + testBackgroundActivityStartsDisallowed_isAffiliatedProfileOwnerNotAborted() { + doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled(); + runAndVerifyBackgroundActivityStartsSubtest( + "disallowed_callingUidIsAffiliatedProfileOwner_notAborted", false, + UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, + UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, + false, false, false, false, false, true, false, false); } /** @@ -865,7 +881,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callerHasSystemExemptAppOpNotAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, true); + false, false, false, false, false, false, false, true); } /** @@ -881,7 +897,7 @@ public class ActivityStarterTests extends WindowTestsBase { "disallowed_callingPackageNameIsIme_notAborted", false, CURRENT_IME_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, false, false, false, false, false, false); + false, false, false, false, false, false, false, false); } /** @@ -902,7 +918,7 @@ public class ActivityStarterTests extends WindowTestsBase { "allowed_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP, - false, true, false, false, false, false, false); + false, true, false, false, false, false, false, false); verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, "", // activity name BackgroundActivityStartController.BAL_ALLOW_PERMISSION, @@ -933,7 +949,7 @@ public class ActivityStarterTests extends WindowTestsBase { "allowed_notAborted", false, UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP, Process.SYSTEM_UID, true, PROCESS_STATE_BOUND_TOP, - false, true, false, false, false, false, false); + false, true, false, false, false, false, false, false); verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED, DEFAULT_COMPONENT_PACKAGE_NAME + "/" + DEFAULT_COMPONENT_PACKAGE_NAME, BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT, @@ -949,6 +965,7 @@ public class ActivityStarterTests extends WindowTestsBase { boolean callerIsTempAllowed, boolean callerIsInstrumentingWithBackgroundActivityStartPrivileges, boolean isCallingUidDeviceOwner, + boolean isCallingUidAffiliatedProfileOwner, boolean isPinnedSingleInstance, boolean hasSystemExemptAppOp) { // window visibility @@ -982,6 +999,9 @@ public class ActivityStarterTests extends WindowTestsBase { callerIsInstrumentingWithBackgroundActivityStartPrivileges); // callingUid is the device owner doReturn(isCallingUidDeviceOwner).when(mAtm).isDeviceOwner(callingUid); + // callingUid is the affiliated profile owner + doReturn(isCallingUidAffiliatedProfileOwner).when(mAtm) + .isAffiliatedProfileOwner(callingUid); // caller has OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop doReturn(hasSystemExemptAppOp ? AppOpsManager.MODE_ALLOWED diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java index ad9f710fbbcc..bbec091b9924 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java @@ -79,6 +79,8 @@ public class ContentRecorderTests extends WindowTestsBase { private Task mTask; private final ContentRecordingSession mDisplaySession = ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY); + private final ContentRecordingSession mWaitingDisplaySession = + ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY); private ContentRecordingSession mTaskSession; private static Point sSurfaceSize; private ContentRecorder mContentRecorder; @@ -120,6 +122,10 @@ public class ContentRecorderTests extends WindowTestsBase { mTaskSession = ContentRecordingSession.createTaskSession(sTaskWindowContainerToken); mTaskSession.setVirtualDisplayId(displayId); + // GIVEN a session is waiting for the user to review consent. + mWaitingDisplaySession.setVirtualDisplayId(displayId); + mWaitingDisplaySession.setWaitingForConsent(true); + mConfigListener = new ConfigListener(); DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_WINDOW_MANAGER, mContext.getMainExecutor(), mConfigListener); @@ -221,6 +227,18 @@ public class ContentRecorderTests extends WindowTestsBase { } @Test + public void testUpdateRecording_waitingForConsent() { + mContentRecorder.setContentRecordingSession(mWaitingDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + + + mContentRecorder.setContentRecordingSession(mDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + } + + @Test public void testOnConfigurationChanged_neverRecording() { mContentRecorder.onConfigurationChanged(ORIENTATION_PORTRAIT); diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java index 6cda0381d245..52226c2be298 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java @@ -24,10 +24,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.view.ContentRecordingSession; @@ -36,6 +34,8 @@ import androidx.test.filters.SmallTest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Tests for the {@link ContentRecordingController} class. @@ -49,12 +49,20 @@ import org.junit.runner.RunWith; public class ContentRecordingControllerTests extends WindowTestsBase { private final ContentRecordingSession mDefaultSession = ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY); + private final ContentRecordingSession mWaitingDisplaySession = + ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY); private int mVirtualDisplayId; private DisplayContent mVirtualDisplayContent; + private WindowContainer.RemoteToken mRootTaskToken; + + @Mock + private WindowContainer mTaskWindowContainer; @Before public void setup() { + MockitoAnnotations.initMocks(this); + // GIVEN the VirtualDisplay associated with the session (so the display has state ON). mVirtualDisplayContent = new TestDisplayContent.Builder(mAtm, 500, 600).build(); mVirtualDisplayId = mVirtualDisplayContent.getDisplayId(); @@ -62,6 +70,11 @@ public class ContentRecordingControllerTests extends WindowTestsBase { spyOn(mVirtualDisplayContent); mDefaultSession.setVirtualDisplayId(mVirtualDisplayId); + mWaitingDisplaySession.setVirtualDisplayId(mVirtualDisplayId); + mWaitingDisplaySession.setWaitingForConsent(true); + + mRootTaskToken = new WindowContainer.RemoteToken(mTaskWindowContainer); + mTaskWindowContainer.mRemoteToken = mRootTaskToken; } @Test @@ -92,7 +105,7 @@ public class ContentRecordingControllerTests extends WindowTestsBase { } @Test - public void testSetContentRecordingSessionLocked_newDisplaySession_accepted() { + public void testSetContentRecordingSessionLocked_newSession_accepted() { ContentRecordingController controller = new ContentRecordingController(); // GIVEN a valid display session. // WHEN updating the session. @@ -104,15 +117,37 @@ public class ContentRecordingControllerTests extends WindowTestsBase { } @Test - public void testSetContentRecordingSessionLocked_updateCurrentDisplaySession_notAccepted() { + public void testSetContentRecordingSessionLocked_updateSession_noLongerWaiting_accepted() { + ContentRecordingController controller = new ContentRecordingController(); + // GIVEN a valid display session already in place. + controller.setContentRecordingSessionLocked(mWaitingDisplaySession, mWm); + verify(mVirtualDisplayContent, atLeastOnce()).setContentRecordingSession( + mWaitingDisplaySession); + + // WHEN updating the session on the same display, so no longer waiting to record. + ContentRecordingSession sessionUpdate = ContentRecordingSession.createTaskSession( + mRootTaskToken.toWindowContainerToken().asBinder()); + sessionUpdate.setVirtualDisplayId(mVirtualDisplayId); + sessionUpdate.setWaitingForConsent(false); + controller.setContentRecordingSessionLocked(sessionUpdate, mWm); + + ContentRecordingSession resultingSession = controller.getContentRecordingSessionLocked(); + // THEN the session was accepted. + assertThat(resultingSession).isEqualTo(sessionUpdate); + verify(mVirtualDisplayContent, atLeastOnce()).setContentRecordingSession(sessionUpdate); + verify(mVirtualDisplayContent).updateRecording(); + } + + @Test + public void testSetContentRecordingSessionLocked_invalidUpdateSession_notWaiting_notAccepted() { ContentRecordingController controller = new ContentRecordingController(); // GIVEN a valid display session already in place. controller.setContentRecordingSessionLocked(mDefaultSession, mWm); verify(mVirtualDisplayContent, atLeastOnce()).setContentRecordingSession(mDefaultSession); // WHEN updating the session on the same display. - ContentRecordingSession sessionUpdate = - ContentRecordingSession.createTaskSession(mock(IBinder.class)); + ContentRecordingSession sessionUpdate = ContentRecordingSession.createTaskSession( + mRootTaskToken.toWindowContainerToken().asBinder()); sessionUpdate.setVirtualDisplayId(mVirtualDisplayId); controller.setContentRecordingSessionLocked(sessionUpdate, mWm); @@ -123,7 +158,7 @@ public class ContentRecordingControllerTests extends WindowTestsBase { } @Test - public void testSetContentRecordingSessionLocked_disableCurrentDisplaySession_accepted() { + public void testSetContentRecordingSessionLocked_disableCurrentSession_accepted() { ContentRecordingController controller = new ContentRecordingController(); // GIVEN a valid display session already in place. controller.setContentRecordingSessionLocked(mDefaultSession, mWm); @@ -141,7 +176,7 @@ public class ContentRecordingControllerTests extends WindowTestsBase { } @Test - public void testSetContentRecordingSessionLocked_takeOverCurrentDisplaySession_accepted() { + public void testSetContentRecordingSessionLocked_takeOverCurrentSession_accepted() { ContentRecordingController controller = new ContentRecordingController(); // GIVEN a valid display session already in place. controller.setContentRecordingSessionLocked(mDefaultSession, mWm); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index 8ac6b0f65b72..5ec36048234b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -301,6 +301,33 @@ public class DisplayPolicyTests extends WindowTestsBase { } @Test + public void testSwitchDecorInsets() { + createNavBarWithProvidedInsets(mDisplayContent); + final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy(); + final DisplayInfo info = mDisplayContent.getDisplayInfo(); + final int w = info.logicalWidth; + final int h = info.logicalHeight; + displayPolicy.updateDecorInsetsInfo(); + final Rect prevConfigFrame = new Rect(displayPolicy.getDecorInsetsInfo(info.rotation, + info.logicalWidth, info.logicalHeight).mConfigFrame); + + displayPolicy.updateCachedDecorInsets(); + mDisplayContent.updateBaseDisplayMetrics(w / 2, h / 2, + info.logicalDensityDpi, info.physicalXDpi, info.physicalYDpi); + // There is no previous cache. But the current state will be cached. + assertFalse(displayPolicy.shouldKeepCurrentDecorInsets()); + + // Switch to original state. + displayPolicy.updateCachedDecorInsets(); + mDisplayContent.updateBaseDisplayMetrics(w, h, + info.logicalDensityDpi, info.physicalXDpi, info.physicalYDpi); + assertTrue(displayPolicy.shouldKeepCurrentDecorInsets()); + // The current insets are restored from cache directly. + assertEquals(prevConfigFrame, displayPolicy.getDecorInsetsInfo(info.rotation, + info.logicalWidth, info.logicalHeight).mConfigFrame); + } + + @Test public void testUpdateDisplayConfigurationByDecor() { doReturn(NO_CUTOUT).when(mDisplayContent).calculateDisplayCutoutForRotation(anyInt()); final WindowState navbar = createNavBarWithProvidedInsets(mDisplayContent); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index a3117269eb01..3ca35ef7cc26 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -479,6 +479,8 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { public void testOnActivityConfigurationChanging_displayRotationNotChanging_noRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); + doReturn(false).when(mActivity.mLetterboxUiController) + .isCameraCompatSplitScreenAspectRatioAllowed(); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false); @@ -487,6 +489,19 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test + public void testOnActivityConfigurationChanging_splitScreenAspectRatioAllowed_refresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + doReturn(true).when(mActivity.mLetterboxUiController) + .isCameraCompatSplitScreenAspectRatioAllowed(); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false); + + assertActivityRefreshRequested(/* refreshRequested */ true); + } + + @Test public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(false); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 03c93e976059..7d507e9150e8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -16,6 +16,8 @@ package com.android.server.wm; +import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP; +import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP; import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; @@ -24,6 +26,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE; +import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR; import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT; @@ -40,7 +43,9 @@ import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; @@ -453,6 +458,29 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test + public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() { + final InsetsSource taskbar = new InsetsSource(/*id=*/ 0, + WindowInsets.Type.navigationBars()); + taskbar.setInsetsRoundedCornerFrame(true); + final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar); + final Rect opaqueBounds = new Rect(0, 0, 500, 300); + doReturn(opaqueBounds).when(mActivity).getBounds(); + // Activity is translucent + spyOn(mActivity.mLetterboxUiController); + doReturn(true).when(mActivity.mLetterboxUiController).hasInheritedLetterboxBehavior(); + + // Makes requested sizes different + mainWindow.mRequestedWidth = opaqueBounds.width() - 1; + mainWindow.mRequestedHeight = opaqueBounds.height() - 1; + assertNull(mActivity.mLetterboxUiController.getCropBoundsIfNeeded(mainWindow)); + + // Makes requested sizes equals + mainWindow.mRequestedWidth = opaqueBounds.width(); + mainWindow.mRequestedHeight = opaqueBounds.height(); + assertNotNull(mActivity.mLetterboxUiController.getCropBoundsIfNeeded(mainWindow)); + } + + @Test public void testGetCropBoundsIfNeeded_noCrop() { final InsetsSource taskbar = new InsetsSource(/*id=*/ 0, WindowInsets.Type.navigationBars()); @@ -861,6 +889,174 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_overrideEnabled_returnsTrue() { + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyTrue_overrideEnabled_returnsTrue() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyTrue_overrideDisabled_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_overrideDisabled_returnsFalse() { + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyFalse_overrideEnabled_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO}) + public void testshouldOverrideMinAspectRatio_propertyFalse_noOverride_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideMinAspectRatio()); + } + + @Test + @EnableCompatChanges({FORCE_RESIZE_APP}) + public void testshouldOverrideForceResizeApp_overrideEnabled_returnsTrue() { + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideForceResizeApp()); + } + + @Test + @EnableCompatChanges({FORCE_RESIZE_APP}) + public void testshouldOverrideForceResizeApp_propertyTrue_overrideEnabled_returnsTrue() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideForceResizeApp()); + } + + @Test + @DisableCompatChanges({FORCE_RESIZE_APP}) + public void testshouldOverrideForceResizeApp_propertyTrue_overrideDisabled_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideForceResizeApp()); + } + + @Test + @DisableCompatChanges({FORCE_RESIZE_APP}) + public void testshouldOverrideForceResizeApp_overrideDisabled_returnsFalse() { + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideForceResizeApp()); + } + + @Test + @EnableCompatChanges({FORCE_RESIZE_APP}) + public void testshouldOverrideForceResizeApp_propertyFalse_overrideEnabled_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, /* value */ false); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideForceResizeApp()); + } + + @Test + @DisableCompatChanges({FORCE_RESIZE_APP}) + public void testshouldOverrideForceResizeApp_propertyFalse_noOverride_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, /* value */ false); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideForceResizeApp()); + } + + @Test + @EnableCompatChanges({FORCE_NON_RESIZE_APP}) + public void testshouldOverrideForceNonResizeApp_overrideEnabled_returnsTrue() { + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideForceNonResizeApp()); + } + + @Test + @EnableCompatChanges({FORCE_NON_RESIZE_APP}) + public void testshouldOverrideForceNonResizeApp_propertyTrue_overrideEnabled_returnsTrue() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldOverrideForceNonResizeApp()); + } + + @Test + @DisableCompatChanges({FORCE_NON_RESIZE_APP}) + public void testshouldOverrideForceNonResizeApp_propertyTrue_overrideDisabled_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideForceNonResizeApp()); + } + + @Test + @DisableCompatChanges({FORCE_NON_RESIZE_APP}) + public void testshouldOverrideForceNonResizeApp_overrideDisabled_returnsFalse() { + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideForceNonResizeApp()); + } + + @Test + @EnableCompatChanges({FORCE_NON_RESIZE_APP}) + public void testshouldOverrideForceNonResizeApp_propertyFalse_overrideEnabled_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, /* value */ false); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideForceNonResizeApp()); + } + + @Test + @DisableCompatChanges({FORCE_NON_RESIZE_APP}) + public void testshouldOverrideForceNonResizeApp_propertyFalse_noOverride_returnsFalse() + throws Exception { + mockThatProperty(PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, /* value */ false); + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldOverrideForceNonResizeApp()); + } + + @Test public void testgetFixedOrientationLetterboxAspectRatio_splitScreenAspectEnabled() { doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration) .isCameraCompatTreatmentEnabled(anyBoolean()); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 06bcbf34e042..2dd34eb5ac4d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -3511,6 +3511,51 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testLetterboxAlignedToBottom_NotOverlappingNavbar() { + assertLandscapeActivityAlignedToBottomWithNavbar(false /* immersive */); + } + + @Test + public void testImmersiveLetterboxAlignedToBottom_OverlappingNavbar() { + assertLandscapeActivityAlignedToBottomWithNavbar(true /* immersive */); + } + + private void assertLandscapeActivityAlignedToBottomWithNavbar(boolean immersive) { + final int screenHeight = 2800; + final int screenWidth = 1400; + final int taskbarHeight = 200; + setUpDisplaySizeWithApp(screenWidth, screenHeight); + + mActivity.mDisplayContent.setIgnoreOrientationRequest(true); + mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(1.0f); + + final InsetsSource navSource = new InsetsSource( + InsetsSource.createId(null, 0, navigationBars()), navigationBars()); + navSource.setInsetsRoundedCornerFrame(true); + // Immersive activity has transient navbar + navSource.setVisible(!immersive); + navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight)); + mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15); + + final WindowState w1 = addWindowToActivity(mActivity); + w1.mAboveInsetsState.addSource(navSource); + + // Prepare unresizable landscape activity + prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); + final DisplayPolicy displayPolicy = mActivity.mDisplayContent.getDisplayPolicy(); + doReturn(immersive).when(displayPolicy).isImmersiveMode(); + + mActivity.mRootWindowContainer.performSurfacePlacement(); + + LetterboxDetails letterboxDetails = mActivity.mLetterboxUiController.getLetterboxDetails(); + + // Letterboxed activity at bottom + assertEquals(new Rect(0, 2100, 1400, 2800), mActivity.getBounds()); + final int expectedHeight = immersive ? screenHeight : screenHeight - taskbarHeight; + assertEquals(expectedHeight, letterboxDetails.getLetterboxInnerBounds().bottom); + } + + @Test public void testSplitScreenLetterboxDetailsForStatusBar_twoLetterboxedApps() { mAtm.mDevEnableNonResizableMultiWindow = true; setUpDisplaySizeWithApp(2800, 1000); @@ -3938,7 +3983,7 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mWm.mLetterboxConfiguration.setIsAutomaticReachabilityInBookModeEnabled(true); mWm.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier( - 1.0f /*letterboxVerticalPositionMultiplier*/); + 1.0f /*letterboxHorizontalPositionMultiplier*/); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); Rect letterboxNoFold = new Rect(2100, 0, 2800, 1400); diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index a98429ad4902..cf236ddb100d 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -2539,10 +2539,19 @@ public class UsageStatsService extends SystemService implements } @Override - public void reportChooserSelection(String packageName, int userId, String contentType, - String[] annotations, String action) { + public void reportChooserSelection(@NonNull String packageName, int userId, + @NonNull String contentType, String[] annotations, @NonNull String action) { if (packageName == null) { - Slog.w(TAG, "Event report user selecting a null package"); + throw new IllegalArgumentException("Package selection must not be null."); + } + // A valid contentType and action must be provided for chooser selection events. + if (contentType == null || contentType.isBlank() + || action == null || action.isBlank()) { + return; + } + // Verify if this package exists before reporting an event for it. + if (mPackageManagerInternal.getPackageUid(packageName, 0, userId) < 0) { + Slog.w(TAG, "Event report user selecting an invalid package"); return; } diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java index f1e1a5adc2c3..dc5f6e960a2b 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java @@ -1252,6 +1252,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { mEventLogger.enqueue(new SessionEvent(Type.RESUME_FAILED, modelData.getModelId(), String.valueOf(status)) .printLog(ALOGW, TAG)); + modelData.setRequested(false); callback.onResumeFailed(status); } catch (RemoteException e) { mEventLogger.enqueue(new SessionEvent(Type.RESUME_FAILED, @@ -1300,6 +1301,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { mEventLogger.enqueue(new SessionEvent(Type.PAUSE_FAILED, modelData.getModelId(), String.valueOf(status)) .printLog(ALOGW, TAG)); + modelData.setRequested(false); callback.onPauseFailed(status); } catch (RemoteException e) { mEventLogger.enqueue(new SessionEvent(Type.PAUSE_FAILED, @@ -1453,6 +1455,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } synchronized void setNotLoaded() { + mRecognitionToken = null; mModelState = MODEL_NOTLOADED; } @@ -1462,6 +1465,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { synchronized void clearState() { mModelState = MODEL_NOTLOADED; + mRecognitionToken = null; mRecognitionConfig = null; mRequested = false; mCallback = null; diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java index 9fb5509141a7..13945a119e6f 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java @@ -366,10 +366,10 @@ public class SoundTriggerService extends SystemService { try { int uid = mPackageManager.getPackageUid(mOriginatorIdentity.packageName, PackageManager.PackageInfoFlags.of(0)); - if (uid != mOriginatorIdentity.uid) { - throw new SecurityException("Package name: " + - mOriginatorIdentity.packageName + "with uid: " + uid - + "attempted to spoof as: " + mOriginatorIdentity.uid); + if (!UserHandle.isSameApp(uid, mOriginatorIdentity.uid)) { + throw new SecurityException("Uid " + mOriginatorIdentity.uid + + " attempted to spoof package name " + + mOriginatorIdentity.packageName + " with uid: " + uid); } } catch (PackageManager.NameNotFoundException e) { throw new SecurityException("Package name not found: " diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java index f70268e1848a..e793f317d41f 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java @@ -168,9 +168,18 @@ class SoundTriggerModule implements IBinder.DeathRecipient, ISoundTriggerHal.Glo * Attached to the HAL service via factory. */ private void attachToHal() { - mHalService = new SoundTriggerHalEnforcer( - new SoundTriggerHalWatchdog( - new SoundTriggerDuplicateModelHandler(mHalFactory.create()))); + mHalService = null; + while (mHalService == null) { + try { + mHalService = new SoundTriggerHalEnforcer( + new SoundTriggerHalWatchdog( + new SoundTriggerDuplicateModelHandler(mHalFactory.create()))); + } catch (RuntimeException e) { + if (!(e.getCause() instanceof RemoteException)) { + throw e; + } + } + } mHalService.linkToDeath(this); mHalService.registerCallback(this); mProperties = mHalService.getProperties(); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java index cd29dace2263..3a651042ea71 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java @@ -47,7 +47,6 @@ import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPH import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_UNEXPECTED_CALLBACK; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECT_UNEXPECTED_CALLBACK; import static com.android.server.voiceinteraction.HotwordDetectionConnection.ENFORCE_HOTWORD_PHRASE_ID; -import static com.android.server.voiceinteraction.SoundTriggerSessionPermissionsDecorator.enforcePermissionForPreflight; import android.annotation.NonNull; import android.annotation.Nullable; @@ -743,7 +742,14 @@ abstract class DetectorSession { void enforcePermissionsForDataDelivery() { Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - enforcePermissionForPreflight(mContext, mVoiceInteractorIdentity, RECORD_AUDIO); + int result = PermissionChecker.checkPermissionForPreflight( + mContext, RECORD_AUDIO, /* pid */ -1, mVoiceInteractorIdentity.uid, + mVoiceInteractorIdentity.packageName); + if (result != PermissionChecker.PERMISSION_GRANTED) { + throw new SecurityException( + "Failed to obtain permission RECORD_AUDIO for identity " + + mVoiceInteractorIdentity); + } int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD); mAppOpsManager.noteOpNoThrow(hotwordOp, mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName, @@ -770,7 +776,7 @@ abstract class DetectorSession { throw new SecurityException( TextUtils.formatSimple("Failed to obtain permission %s for identity %s", permission, - SoundTriggerSessionPermissionsDecorator.toString(identity))); + identity)); } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index edaaf3fbbccc..248cc26ce656 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -962,7 +962,7 @@ final class HotwordDetectionConnection { final DetectorSession session = mDetectorSessions.get( HotwordDetector.DETECTOR_TYPE_VISUAL_QUERY_DETECTOR); if (session == null || session.isDestroyed()) { - Slog.v(TAG, "Not found the look and talk perceiver"); + Slog.v(TAG, "Not found the visual query detector"); return null; } return (VisualQueryDetectorSession) session; diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java deleted file mode 100644 index 0ef2f06b6684..000000000000 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.voiceinteraction; - -import android.hardware.soundtrigger.SoundTrigger; -import android.os.RemoteException; - -import com.android.internal.app.IHotwordRecognitionStatusCallback; -import com.android.internal.app.IVoiceInteractionSoundTriggerSession; - -/** - * A remote object that simply proxies calls to a real {@link IVoiceInteractionSoundTriggerSession} - * implementation. This design pattern allows us to add decorators to the core implementation - * (simply wrapping a binder object does not work). - */ -final class SoundTriggerSessionBinderProxy extends IVoiceInteractionSoundTriggerSession.Stub { - - private final IVoiceInteractionSoundTriggerSession mDelegate; - - SoundTriggerSessionBinderProxy(IVoiceInteractionSoundTriggerSession delegate) { - mDelegate = delegate; - } - - @Override - public SoundTrigger.ModuleProperties getDspModuleProperties() throws RemoteException { - return mDelegate.getDspModuleProperties(); - } - - @Override - public int startRecognition(int i, String s, - IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback, - SoundTrigger.RecognitionConfig recognitionConfig, boolean b) throws RemoteException { - return mDelegate.startRecognition(i, s, iHotwordRecognitionStatusCallback, - recognitionConfig, b); - } - - @Override - public int stopRecognition(int i, - IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback) - throws RemoteException { - return mDelegate.stopRecognition(i, iHotwordRecognitionStatusCallback); - } - - @Override - public int setParameter(int i, int i1, int i2) throws RemoteException { - return mDelegate.setParameter(i, i1, i2); - } - - @Override - public int getParameter(int i, int i1) throws RemoteException { - return mDelegate.getParameter(i, i1); - } - - @Override - public SoundTrigger.ModelParamRange queryParameter(int i, int i1) throws RemoteException { - return mDelegate.queryParameter(i, i1); - } - - @Override - public void detach() throws RemoteException { - mDelegate.detach(); - } -} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java deleted file mode 100644 index 0f8a945ec461..000000000000 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.voiceinteraction; - -import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; -import static android.Manifest.permission.RECORD_AUDIO; - -import static com.android.server.voiceinteraction.HotwordDetectionConnection.DEBUG; - -import android.annotation.NonNull; -import android.content.Context; -import android.content.PermissionChecker; -import android.hardware.soundtrigger.SoundTrigger; -import android.media.permission.Identity; -import android.media.permission.PermissionUtil; -import android.os.IBinder; -import android.os.RemoteException; -import android.text.TextUtils; -import android.util.Slog; - -import com.android.internal.app.IHotwordRecognitionStatusCallback; -import com.android.internal.app.IVoiceInteractionSoundTriggerSession; - -/** - * Decorates {@link IVoiceInteractionSoundTriggerSession} with permission checks for {@link - * android.Manifest.permission#RECORD_AUDIO} and - * {@link android.Manifest.permission#CAPTURE_AUDIO_HOTWORD}. - * <p> - * Does not implement {@link #asBinder()} as it's intended to be wrapped by an - * {@link IVoiceInteractionSoundTriggerSession.Stub} object. - */ -final class SoundTriggerSessionPermissionsDecorator implements - IVoiceInteractionSoundTriggerSession { - static final String TAG = "SoundTriggerSessionPermissionsDecorator"; - - private final IVoiceInteractionSoundTriggerSession mDelegate; - private final Context mContext; - private final Identity mOriginatorIdentity; - - SoundTriggerSessionPermissionsDecorator(IVoiceInteractionSoundTriggerSession delegate, - Context context, Identity originatorIdentity) { - mDelegate = delegate; - mContext = context; - mOriginatorIdentity = originatorIdentity; - } - - @Override - public SoundTrigger.ModuleProperties getDspModuleProperties() throws RemoteException { - // No permission needed here (the app must have the Assistant Role to retrieve the session). - return mDelegate.getDspModuleProperties(); - } - - @Override - public int startRecognition(int i, String s, - IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback, - SoundTrigger.RecognitionConfig recognitionConfig, boolean b) throws RemoteException { - if (DEBUG) { - Slog.d(TAG, "startRecognition"); - } - if (!isHoldingPermissions()) { - return SoundTrigger.STATUS_PERMISSION_DENIED; - } - return mDelegate.startRecognition(i, s, iHotwordRecognitionStatusCallback, - recognitionConfig, b); - } - - @Override - public int stopRecognition(int i, - IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback) - throws RemoteException { - // Stopping a model does not require special permissions. Having a handle to the session is - // sufficient. - return mDelegate.stopRecognition(i, iHotwordRecognitionStatusCallback); - } - - @Override - public int setParameter(int i, int i1, int i2) throws RemoteException { - if (!isHoldingPermissions()) { - return SoundTrigger.STATUS_PERMISSION_DENIED; - } - return mDelegate.setParameter(i, i1, i2); - } - - @Override - public int getParameter(int i, int i1) throws RemoteException { - // No permission needed here (the app must have the Assistant Role to retrieve the session). - return mDelegate.getParameter(i, i1); - } - - @Override - public SoundTrigger.ModelParamRange queryParameter(int i, int i1) throws RemoteException { - // No permission needed here (the app must have the Assistant Role to retrieve the session). - return mDelegate.queryParameter(i, i1); - } - - @Override - public IBinder asBinder() { - throw new UnsupportedOperationException( - "This object isn't intended to be used as a Binder."); - } - - @Override - public void detach() { - try { - mDelegate.detach(); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - } - - // TODO: Share this code with SoundTriggerMiddlewarePermission. - private boolean isHoldingPermissions() { - try { - enforcePermissionForPreflight(mContext, mOriginatorIdentity, RECORD_AUDIO); - enforcePermissionForPreflight(mContext, mOriginatorIdentity, CAPTURE_AUDIO_HOTWORD); - return true; - } catch (SecurityException e) { - Slog.e(TAG, e.toString()); - return false; - } - } - - /** - * Throws a {@link SecurityException} if originator permanently doesn't have the given - * permission. - * Soft (temporary) denials are considered OK for preflight purposes. - * - * @param context A {@link Context}, used for permission checks. - * @param identity The identity to check. - * @param permission The identifier of the permission we want to check. - */ - static void enforcePermissionForPreflight(@NonNull Context context, - @NonNull Identity identity, @NonNull String permission) { - final int status = PermissionUtil.checkPermissionForPreflight(context, identity, - permission); - switch (status) { - case PermissionChecker.PERMISSION_GRANTED: - case PermissionChecker.PERMISSION_SOFT_DENIED: - return; - case PermissionChecker.PERMISSION_HARD_DENIED: - throw new SecurityException( - TextUtils.formatSimple("Failed to obtain permission %s for identity %s", - permission, toString(identity))); - default: - throw new RuntimeException("Unexpected permission check result."); - } - } - - static String toString(Identity identity) { - return "{uid=" + identity.uid - + " pid=" + identity.pid - + " packageName=" + identity.packageName - + " attributionTag=" + identity.attributionTag - + "}"; - } - - // Temporary hack for using the same status code as SoundTrigger, so we don't change behavior. - // TODO: Reuse SoundTrigger code so we don't need to do this. - private static final int TEMPORARY_PERMISSION_DENIED = 3; -} diff --git a/telecomm/java/android/telecom/CallAttributes.java b/telecomm/java/android/telecom/CallAttributes.java index 52ff90f38113..b1a7d819cd17 100644 --- a/telecomm/java/android/telecom/CallAttributes.java +++ b/telecomm/java/android/telecom/CallAttributes.java @@ -59,6 +59,9 @@ public final class CallAttributes implements Parcelable { public static final String CALL_CAPABILITIES_KEY = "TelecomCapabilities"; /** @hide **/ + public static final String DISPLAY_NAME_KEY = "DisplayName"; + + /** @hide **/ public static final String CALLER_PID_KEY = "CallerPid"; /** @hide **/ diff --git a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java index f9b76f4907cf..9a8c9655375d 100644 --- a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java +++ b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java @@ -32,7 +32,6 @@ import android.os.RemoteException; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; -import android.provider.DeviceConfig; import android.telephony.SubscriptionManager; import android.telephony.TelephonyFrameworkInitializer; import android.telephony.TelephonyManager; @@ -262,9 +261,6 @@ public final class TelephonyUtils { */ public static void showSwitchToManagedProfileDialogIfAppropriate(Context context, int subId, int callingUid, String callingPackage) { - if (!isSwitchToManagedProfileDialogFlagEnabled()) { - return; - } final long token = Binder.clearCallingIdentity(); try { UserHandle callingUserHandle = UserHandle.getUserHandleForUid(callingUid); @@ -302,11 +298,6 @@ public final class TelephonyUtils { } } - public static boolean isSwitchToManagedProfileDialogFlagEnabled() { - return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER, - "enable_switch_to_managed_profile_dialog", false); - } - private static boolean isUidForeground(Context context, int uid) { ActivityManager am = context.getSystemService(ActivityManager.class); boolean result = am != null && am.getUidImportance(uid) diff --git a/tests/Internal/src/android/app/WallpaperColorsTest.java b/tests/Internal/src/android/app/WallpaperColorsTest.java index 9ffb236d3f59..70660a0a117e 100644 --- a/tests/Internal/src/android/app/WallpaperColorsTest.java +++ b/tests/Internal/src/android/app/WallpaperColorsTest.java @@ -48,10 +48,10 @@ public class WallpaperColorsTest { } /** - * Check that white supports dark text and black doesn't + * Check that white surface supports dark text */ @Test - public void colorHintsTest() { + public void whiteSurfaceColorHintsTest() { Bitmap image = Bitmap.createBitmap(30, 30, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(image); @@ -59,28 +59,68 @@ public class WallpaperColorsTest { int hints = WallpaperColors.fromBitmap(image).getColorHints(); boolean supportsDarkText = (hints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0; boolean supportsDarkTheme = (hints & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0; - boolean fromBitmap = (hints & WallpaperColors.HINT_FROM_BITMAP) != 0; Assert.assertTrue("White surface should support dark text.", supportsDarkText); Assert.assertFalse("White surface shouldn't support dark theme.", supportsDarkTheme); - Assert.assertTrue("From bitmap should be true if object was created " - + "using WallpaperColors#fromBitmap.", fromBitmap); - - canvas.drawColor(Color.BLACK); - hints = WallpaperColors.fromBitmap(image).getColorHints(); - supportsDarkText = (hints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0; - supportsDarkTheme = (hints & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0; - Assert.assertFalse("Black surface shouldn't support dark text.", supportsDarkText); - Assert.assertTrue("Black surface should support dark theme.", supportsDarkTheme); Paint paint = new Paint(); paint.setStyle(Paint.Style.FILL); paint.setColor(Color.BLACK); - canvas.drawColor(Color.WHITE); canvas.drawRect(0, 0, 8, 8, paint); supportsDarkText = (WallpaperColors.fromBitmap(image) .getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0; Assert.assertFalse("Light surface shouldn't support dark text " + "when it contains dark pixels.", supportsDarkText); + } + + /** + * Check that x-small white region supports dark text when max number of dark pixels = 0 + */ + @Test + public void xSmallWhiteSurfaceColorHintsTest() { + Bitmap xsmall_image = Bitmap.createBitmap(1, 5, Bitmap.Config.ARGB_8888); + Canvas xsmall_canvas = new Canvas(xsmall_image); + + xsmall_canvas.drawColor(Color.WHITE); + int hints = WallpaperColors.fromBitmap(xsmall_image).getColorHints(); + boolean supportsDarkText = (hints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0; + boolean supportsDarkTheme = (hints & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0; + Assert.assertTrue("X-small white surface should support dark text.", + supportsDarkText); + Assert.assertFalse("X-small white surface shouldn't support dark theme.", + supportsDarkTheme); + } + + /** + * Check that black surface doesn't support dark text + */ + @Test + public void blackSurfaceColorHintsTest() { + Bitmap image = Bitmap.createBitmap(30, 30, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(image); + + canvas.drawColor(Color.BLACK); + int hints = WallpaperColors.fromBitmap(image).getColorHints(); + boolean supportsDarkText = (hints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0; + boolean supportsDarkTheme = (hints & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0; + Assert.assertFalse("Black surface shouldn't support dark text.", supportsDarkText); + Assert.assertTrue("Black surface should support dark theme.", supportsDarkTheme); + } + + /** + * Check that bitmap hint properly indicates when object created via WallpaperColors#fromBitmap + * versus WallpaperColors() public constructor + */ + @Test + public void bitmapHintsTest() { + Bitmap image = Bitmap.createBitmap(30, 30, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(image); + + canvas.drawColor(Color.WHITE); + int hints = WallpaperColors.fromBitmap(image).getColorHints(); + + boolean fromBitmap = (hints & WallpaperColors.HINT_FROM_BITMAP) != 0; + Assert.assertTrue("From bitmap should be true if object was created " + + "using WallpaperColors#fromBitmap.", fromBitmap); WallpaperColors colors = new WallpaperColors(Color.valueOf(Color.GREEN), null, null); fromBitmap = (colors.getColorHints() & WallpaperColors.HINT_FROM_BITMAP) != 0; diff --git a/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java index 7d9a6a56262c..d16e90e26aaa 100644 --- a/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java +++ b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java @@ -37,9 +37,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Locale; import java.util.Set; @@ -54,7 +54,7 @@ public class AppLocaleCollectorTest { private LocaleStore.LocaleInfo mAppCurrentLocale; private Set<LocaleInfo> mAllAppActiveLocales; private Set<LocaleInfo> mImeLocales; - private List<LocaleInfo> mSystemCurrentLocales; + private Set<LocaleInfo> mSystemCurrentLocales; private Set<LocaleInfo> mSystemSupportedLocales; private AppLocaleStore.AppLocaleResult mResult; private static final String PKG1 = "pkg1"; @@ -73,14 +73,6 @@ public class AppLocaleCollectorTest { public void setUp() throws Exception { mAppLocaleCollector = spy( new AppLocaleCollector(InstrumentationRegistry.getContext(), PKG1)); - - mAppCurrentLocale = createLocaleInfo("en-US", CURRENT); - mAllAppActiveLocales = initAllAppActivatedLocales(); - mImeLocales = initImeLocales(); - mSystemSupportedLocales = initSystemSupportedLocales(); - mSystemCurrentLocales = initSystemCurrentLocales(); - mResult = new AppLocaleStore.AppLocaleResult(GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG, - initAppSupportedLocale()); } @Test @@ -88,7 +80,7 @@ public class AppLocaleCollectorTest { LocaleList.setDefault( LocaleList.forLanguageTags("en-US-u-mu-fahrenhe,ar-JO-u-mu-fahrenhe-nu-latn")); - List<LocaleStore.LocaleInfo> list = + Set<LocaleStore.LocaleInfo> list = mAppLocaleCollector.getSystemCurrentLocales(); LocaleList expected = LocaleList.forLanguageTags("en-US,ar-JO-u-nu-latn"); @@ -99,7 +91,69 @@ public class AppLocaleCollectorTest { } @Test - public void testGetSupportedLocaleList() { + public void testGetSupportedLocaleList_filterNonAppsupportedSystemLanguage() { + mAppCurrentLocale = createLocaleInfo("en-US", CURRENT); + + // App supports five locales + HashSet<Locale> appSupported = + getAppSupportedLocales(new String[] { + "en-US", + "fr", + "ar", + "es", + "bn" + }); + // There are six locales in system current locales. + mSystemCurrentLocales = getSystemCurrentLocales(new String[] { + "en-US", + "fr-FR", + "ar-JO", + "ca-AD", + "da-DK", + "es-US" + }); + mAllAppActiveLocales = Collections.emptySet(); + mImeLocales = Collections.emptySet(); + mSystemSupportedLocales = Collections.emptySet(); + mResult = new AppLocaleStore.AppLocaleResult(GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG, + appSupported); + + doReturn(mAppCurrentLocale).when(mAppLocaleCollector).getAppCurrentLocale(); + doReturn(mResult).when(mAppLocaleCollector).getAppSupportedLocales(); + doReturn(mAllAppActiveLocales).when(mAppLocaleCollector).getAllAppActiveLocales(); + doReturn(mImeLocales).when(mAppLocaleCollector).getActiveImeLocales(); + doReturn(mSystemSupportedLocales).when(mAppLocaleCollector).getSystemSupportedLocale( + anyObject(), eq(null), eq(true)); + doReturn(mSystemCurrentLocales).when( + mAppLocaleCollector).getSystemCurrentLocales(); + + Set<LocaleInfo> result = mAppLocaleCollector.getSupportedLocaleList(null, true, false); + + // The result would show four rather than six locales in the suggested region. + HashMap<String, Integer> expectedResult = new HashMap<>(); + expectedResult.put("en-US", CURRENT); // The locale current App activates. + expectedResult.put("ar-JO", SYSTEM_AVAILABLE); + expectedResult.put("fr-FR", SYSTEM_AVAILABLE); + expectedResult.put("es-US", SYSTEM_AVAILABLE); + expectedResult.put(createLocaleInfo("", SYSTEM).getId(), SYSTEM); // System language title + + assertEquals(result.size(), expectedResult.size()); + for (LocaleStore.LocaleInfo info: result) { + int suggestionFlags = expectedResult.getOrDefault(info.getId(), -1); + assertEquals(info.mSuggestionFlags, suggestionFlags); + } + } + + @Test + public void testGetSupportedLocaleList_withActiveLocalesFromOtherAppAndIme() { + mAppCurrentLocale = createLocaleInfo("en-US", CURRENT); + mAllAppActiveLocales = initAllAppActivatedLocales(); + mImeLocales = initImeLocales(); + mSystemSupportedLocales = initSystemSupportedLocales(); + mSystemCurrentLocales = initSystemCurrentLocales(); + mResult = new AppLocaleStore.AppLocaleResult(GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG, + initAppSupportedLocale()); + doReturn(mAppCurrentLocale).when(mAppLocaleCollector).getAppCurrentLocale(); doReturn(mResult).when(mAppLocaleCollector).getAppSupportedLocales(); doReturn(mAllAppActiveLocales).when(mAppLocaleCollector).getAllAppActiveLocales(); @@ -147,8 +201,8 @@ public class AppLocaleCollectorTest { ); } - private List<LocaleInfo> initSystemCurrentLocales() { - return List.of(createLocaleInfo("zh-Hant-TW", SYSTEM_AVAILABLE), + private Set<LocaleInfo> initSystemCurrentLocales() { + return Set.of(createLocaleInfo("zh-Hant-TW", SYSTEM_AVAILABLE), createLocaleInfo("ja-JP", SYSTEM_AVAILABLE), // will be filtered because current App activates this locale. createLocaleInfo("en-US", SYSTEM_AVAILABLE)); @@ -188,6 +242,22 @@ public class AppLocaleCollectorTest { return hs; } + private Set<LocaleStore.LocaleInfo> getSystemCurrentLocales(String []languageTags) { + HashSet<LocaleStore.LocaleInfo> hs = new HashSet<>(languageTags.length); + for (String tag:languageTags) { + hs.add(createLocaleInfo(tag, SYSTEM_AVAILABLE)); + } + return hs; + } + + private HashSet<Locale> getAppSupportedLocales(String []languageTags) { + HashSet<Locale> hs = new HashSet<>(languageTags.length); + for (String language:languageTags) { + hs.add(Locale.forLanguageTag(language)); + } + return hs; + } + private LocaleInfo createLocaleInfo(String languageTag, int suggestionFlag) { LocaleInfo localeInfo = LocaleStore.fromLocale(Locale.forLanguageTag(languageTag)); localeInfo.mSuggestionFlags = suggestionFlag; |