diff options
456 files changed, 16649 insertions, 4537 deletions
diff --git a/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java b/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java index 0802072ae144..0cce6ad73eda 100644 --- a/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java +++ b/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java @@ -18,11 +18,11 @@ package android.os; import android.content.Context; import android.hardware.display.DisplayManager; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; import android.provider.Settings; import android.view.Display; -import androidx.benchmark.BenchmarkState; -import androidx.benchmark.junit4.BenchmarkRule; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -38,7 +38,7 @@ public final class DisplayPerfTest { private static final float DELTA = 0.001f; @Rule - public final BenchmarkRule mBenchmarkRule = new BenchmarkRule(); + public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); private DisplayManager mDisplayManager; private Context mContext; @@ -51,7 +51,7 @@ public final class DisplayPerfTest { @Test public void testBrightnessChanges() throws Exception { - final BenchmarkState state = mBenchmarkRule.getState(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); Settings.System.putInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL); 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 53f5c6e64129..f9dd0b379cb2 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -327,6 +327,7 @@ class JobConcurrencyManager { private final ArraySet<ContextAssignment> mRecycledIdle = new ArraySet<>(); private final ArrayList<ContextAssignment> mRecycledPreferredUidOnly = new ArrayList<>(); private final ArrayList<ContextAssignment> mRecycledStoppable = new ArrayList<>(); + private final AssignmentInfo mRecycledAssignmentInfo = new AssignmentInfo(); private final Pools.Pool<ContextAssignment> mContextAssignmentPool = new Pools.SimplePool<>(MAX_RETAINED_OBJECTS); @@ -414,7 +415,7 @@ class JobConcurrencyManager { @VisibleForTesting JobConcurrencyManager(JobSchedulerService service, Injector injector) { mService = service; - mLock = mService.mLock; + mLock = mService.getLock(); mContext = service.getTestableContext(); mInjector = injector; @@ -693,8 +694,9 @@ class JobConcurrencyManager { return; } - final long minPreferredUidOnlyWaitingTimeMs = prepareForAssignmentDeterminationLocked( - mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable); + prepareForAssignmentDeterminationLocked( + mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable, + mRecycledAssignmentInfo); if (DEBUG) { Slog.d(TAG, printAssignments("running jobs initial", @@ -703,7 +705,7 @@ class JobConcurrencyManager { determineAssignmentsLocked( mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable, - minPreferredUidOnlyWaitingTimeMs); + mRecycledAssignmentInfo); if (DEBUG) { Slog.d(TAG, printAssignments("running jobs final", @@ -715,17 +717,18 @@ class JobConcurrencyManager { carryOutAssignmentChangesLocked(mRecycledChanged); cleanUpAfterAssignmentChangesLocked( - mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable); + mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable, + mRecycledAssignmentInfo); noteConcurrency(); } - /** @return the minimum remaining execution time for preferred UID only JobServiceContexts. */ @VisibleForTesting @GuardedBy("mLock") - long prepareForAssignmentDeterminationLocked(final ArraySet<ContextAssignment> idle, + void prepareForAssignmentDeterminationLocked(final ArraySet<ContextAssignment> idle, final List<ContextAssignment> preferredUidOnly, - final List<ContextAssignment> stoppable) { + final List<ContextAssignment> stoppable, + final AssignmentInfo info) { final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue(); final List<JobServiceContext> activeServices = mActiveServices; @@ -755,6 +758,9 @@ class JobConcurrencyManager { if (js != null) { mWorkCountTracker.incrementRunningJobCount(jsc.getRunningJobWorkType()); assignment.workType = jsc.getRunningJobWorkType(); + if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) { + info.numRunningTopEj++; + } } assignment.preferredUid = jsc.getPreferredUid(); @@ -789,10 +795,11 @@ class JobConcurrencyManager { } mWorkCountTracker.onCountDone(); - // Return 0 if there were no preferred UID only contexts to indicate no waiting time due + // Set 0 if there were no preferred UID only contexts to indicate no waiting time due // to such jobs. - return minPreferredUidOnlyWaitingTimeMs == Long.MAX_VALUE - ? 0 : minPreferredUidOnlyWaitingTimeMs; + info.minPreferredUidOnlyWaitingTimeMs = + minPreferredUidOnlyWaitingTimeMs == Long.MAX_VALUE + ? 0 : minPreferredUidOnlyWaitingTimeMs; } @VisibleForTesting @@ -801,7 +808,7 @@ class JobConcurrencyManager { final ArraySet<ContextAssignment> idle, final List<ContextAssignment> preferredUidOnly, final List<ContextAssignment> stoppable, - long minPreferredUidOnlyWaitingTimeMs) { + @NonNull AssignmentInfo info) { final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue(); final List<JobServiceContext> activeServices = mActiveServices; pendingJobQueue.resetIterator(); @@ -832,7 +839,7 @@ class JobConcurrencyManager { // pending jobs that could be designated as waiting too long, and those other jobs // would only have to wait for the new slots to become available. final long minWaitingTimeMs = - Math.min(minPreferredUidOnlyWaitingTimeMs, minChangedWaitingTimeMs); + Math.min(info.minPreferredUidOnlyWaitingTimeMs, minChangedWaitingTimeMs); // Find an available slot for nextPending. The context should be one of the following: // 1. Unused @@ -861,13 +868,6 @@ class JobConcurrencyManager { } } if (selectedContext == null && stoppable.size() > 0) { - int topEjCount = 0; - for (int r = mRunningJobs.size() - 1; r >= 0; --r) { - JobStatus js = mRunningJobs.valueAt(r); - if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) { - topEjCount++; - } - } for (int s = stoppable.size() - 1; s >= 0; --s) { final ContextAssignment assignment = stoppable.get(s); final JobStatus runningJob = assignment.context.getRunningJobLocked(); @@ -888,7 +888,8 @@ class JobConcurrencyManager { final int currentJobBias = mService.evaluateJobBiasLocked(runningJob); canReplace = runningJob.lastEvaluatedBias < JobInfo.BIAS_TOP_APP // Case 2 || currentJobBias < JobInfo.BIAS_TOP_APP // Case 3 - || topEjCount > .5 * mWorkTypeConfig.getMaxTotal(); // Case 4 + // Case 4 + || info.numRunningTopEj > .5 * mWorkTypeConfig.getMaxTotal(); } if (!canReplace && mMaxWaitTimeBypassEnabled) { // Case 5 if (nextPending.shouldTreatAsExpeditedJob()) { @@ -955,7 +956,7 @@ class JobConcurrencyManager { if (selectedContext != null) { selectedContext.newJob = nextPending; preferredUidOnly.remove(selectedContext); - minPreferredUidOnlyWaitingTimeMs = newMinPreferredUidOnlyWaitingTimeMs; + info.minPreferredUidOnlyWaitingTimeMs = newMinPreferredUidOnlyWaitingTimeMs; } } // Make sure to run EJs for the TOP app immediately. @@ -1072,7 +1073,8 @@ class JobConcurrencyManager { private void cleanUpAfterAssignmentChangesLocked(final ArraySet<ContextAssignment> changed, final ArraySet<ContextAssignment> idle, final List<ContextAssignment> preferredUidOnly, - final List<ContextAssignment> stoppable) { + final List<ContextAssignment> stoppable, + final AssignmentInfo assignmentInfo) { for (int s = stoppable.size() - 1; s >= 0; --s) { final ContextAssignment assignment = stoppable.get(s); assignment.clear(); @@ -1093,6 +1095,7 @@ class JobConcurrencyManager { idle.clear(); stoppable.clear(); preferredUidOnly.clear(); + assignmentInfo.clear(); mWorkCountTracker.resetStagingCount(); mActivePkgStats.forEach(mPackageStatsStagingCountClearer); } @@ -2540,6 +2543,17 @@ class JobConcurrencyManager { } } + @VisibleForTesting + static final class AssignmentInfo { + public long minPreferredUidOnlyWaitingTimeMs; + public int numRunningTopEj; + + void clear() { + minPreferredUidOnlyWaitingTimeMs = 0; + numRunningTopEj = 0; + } + } + // TESTING HELPERS @VisibleForTesting diff --git a/api/Android.bp b/api/Android.bp index a3e64a565422..37b5d4c5f47e 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -113,6 +113,7 @@ combined_apis { "framework-sdksandbox", "framework-tethering", "framework-uwb", + "framework-virtualization", "framework-wifi", "i18n.module.public.api", ], diff --git a/api/api.go b/api/api.go index 6a6c493e041a..ba0fdc18d23e 100644 --- a/api/api.go +++ b/api/api.go @@ -27,8 +27,16 @@ import ( const art = "art.module.public.api" const conscrypt = "conscrypt.module.public.api" const i18n = "i18n.module.public.api" +const virtualization = "framework-virtualization" var core_libraries_modules = []string{art, conscrypt, i18n} +// List of modules that are not yet updatable, and hence they can still compile +// against hidden APIs. These modules are filtered out when building the +// updatable-framework-module-impl (because updatable-framework-module-impl is +// built against module_current SDK). Instead they are directly statically +// linked into the all-framework-module-lib, which is building against hidden +// APIs. +var non_updatable_modules = []string{virtualization} // The intention behind this soong plugin is to generate a number of "merged" // API-related modules that would otherwise require a large amount of very @@ -249,12 +257,31 @@ func createMergedSystemStubs(ctx android.LoadHookContext, modules []string) { func createMergedFrameworkImpl(ctx android.LoadHookContext, modules []string) { // This module is for the "framework-all" module, which should not include the core libraries. modules = removeAll(modules, core_libraries_modules) - props := libraryProps{} - props.Name = proptools.StringPtr("all-framework-module-impl") - props.Static_libs = transformArray(modules, "", ".impl") - props.Sdk_version = proptools.StringPtr("module_current") - props.Visibility = []string{"//frameworks/base"} - ctx.CreateModule(java.LibraryFactory, &props) + // Remove the modules that belong to non-updatable APEXes since those are allowed to compile + // against unstable APIs. + modules = removeAll(modules, non_updatable_modules) + // First create updatable-framework-module-impl, which contains all updatable modules. + // This module compiles against module_lib SDK. + { + props := libraryProps{} + props.Name = proptools.StringPtr("updatable-framework-module-impl") + props.Static_libs = transformArray(modules, "", ".impl") + props.Sdk_version = proptools.StringPtr("module_current") + props.Visibility = []string{"//frameworks/base"} + ctx.CreateModule(java.LibraryFactory, &props) + } + + // Now create all-framework-module-impl, which contains updatable-framework-module-impl + // and all non-updatable modules. This module compiles against hidden APIs. + { + props := libraryProps{} + props.Name = proptools.StringPtr("all-framework-module-impl") + props.Static_libs = transformArray(non_updatable_modules, "", ".impl") + props.Static_libs = append(props.Static_libs, "updatable-framework-module-impl") + props.Sdk_version = proptools.StringPtr("core_platform") + props.Visibility = []string{"//frameworks/base"} + ctx.CreateModule(java.LibraryFactory, &props) + } } func createMergedFrameworkModuleLibStubs(ctx android.LoadHookContext, modules []string) { diff --git a/boot/Android.bp b/boot/Android.bp index 7839918d6a54..6e5291408ffa 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -136,6 +136,10 @@ platform_bootclasspath { apex: "com.android.car.framework", module: "com.android.car.framework-bootclasspath-fragment", }, + { + apex: "com.android.virt", + module: "com.android.virt-bootclasspath-fragment", + }, ], // Additional information needed by hidden api processing. diff --git a/core/api/current.txt b/core/api/current.txt index 6a875b83674c..f80c09596fb1 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -17651,6 +17651,7 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Rational> CONTROL_AE_COMPENSATION_STEP; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Boolean> CONTROL_AE_LOCK_AVAILABLE; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AF_AVAILABLE_MODES; + field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Boolean> CONTROL_AUTOFRAMING_AVAILABLE; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AVAILABLE_EFFECTS; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.Capability[]> CONTROL_AVAILABLE_EXTENDED_SCENE_MODE_CAPABILITIES; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AVAILABLE_MODES; @@ -17955,6 +17956,12 @@ package android.hardware.camera2 { field public static final int CONTROL_AF_TRIGGER_CANCEL = 2; // 0x2 field public static final int CONTROL_AF_TRIGGER_IDLE = 0; // 0x0 field public static final int CONTROL_AF_TRIGGER_START = 1; // 0x1 + field public static final int CONTROL_AUTOFRAMING_AUTO = 2; // 0x2 + field public static final int CONTROL_AUTOFRAMING_OFF = 0; // 0x0 + field public static final int CONTROL_AUTOFRAMING_ON = 1; // 0x1 + field public static final int CONTROL_AUTOFRAMING_STATE_CONVERGED = 2; // 0x2 + field public static final int CONTROL_AUTOFRAMING_STATE_FRAMING = 1; // 0x1 + field public static final int CONTROL_AUTOFRAMING_STATE_INACTIVE = 0; // 0x0 field public static final int CONTROL_AWB_MODE_AUTO = 1; // 0x1 field public static final int CONTROL_AWB_MODE_CLOUDY_DAYLIGHT = 6; // 0x6 field public static final int CONTROL_AWB_MODE_DAYLIGHT = 5; // 0x5 @@ -18202,6 +18209,7 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AF_MODE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AF_REGIONS; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AF_TRIGGER; + field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AUTOFRAMING; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Boolean> CONTROL_AWB_LOCK; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AWB_MODE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AWB_REGIONS; @@ -18292,6 +18300,8 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AF_SCENE_CHANGE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AF_STATE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AF_TRIGGER; + field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AUTOFRAMING; + field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AUTOFRAMING_STATE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Boolean> CONTROL_AWB_LOCK; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AWB_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AWB_REGIONS; @@ -32315,6 +32325,7 @@ package android.os { method public static final boolean is64Bit(); method public static boolean isApplicationUid(int); method public static final boolean isIsolated(); + method public static final boolean isIsolatedUid(int); method public static final boolean isSdkSandbox(); method public static final void killProcess(int); method public static final int myPid(); @@ -44029,6 +44040,8 @@ package android.telephony { field public static final int APPTYPE_USIM = 2; // 0x2 field public static final int AUTHTYPE_EAP_AKA = 129; // 0x81 field public static final int AUTHTYPE_EAP_SIM = 128; // 0x80 + field public static final int AUTHTYPE_GBA_BOOTSTRAP = 132; // 0x84 + field public static final int AUTHTYPE_GBA_NAF_KEY_EXTERNAL = 133; // 0x85 field public static final int CALL_COMPOSER_STATUS_OFF = 0; // 0x0 field public static final int CALL_COMPOSER_STATUS_ON = 1; // 0x1 field public static final int CALL_STATE_IDLE = 0; // 0x0 @@ -54030,23 +54043,22 @@ package android.view.inputmethod { } public final class TextAppearanceInfo implements android.os.Parcelable { - ctor public TextAppearanceInfo(@NonNull android.widget.TextView); method public int describeContents(); - method @Nullable public String getFontFamilyName(); method @Nullable public String getFontFeatureSettings(); method @Nullable public String getFontVariationSettings(); + method @ColorInt public int getHighlightTextColor(); + method @ColorInt public int getHintTextColor(); method public float getLetterSpacing(); method public int getLineBreakStyle(); method public int getLineBreakWordStyle(); - method public int getMaxLength(); + method @ColorInt public int getLinkTextColor(); + method @ColorInt public int getShadowColor(); method @Px public float getShadowDx(); method @Px public float getShadowDy(); method @Px public float getShadowRadius(); + method @Nullable public String getSystemFontFamilyName(); method @ColorInt public int getTextColor(); - method @ColorInt public int getTextColorHighlight(); - method @ColorInt public int getTextColorHint(); - method @Nullable public android.content.res.ColorStateList getTextColorLink(); - method @IntRange(from=0xffffffff, to=android.graphics.fonts.FontStyle.FONT_WEIGHT_MAX) public int getTextFontWeight(); + method @IntRange(from=android.graphics.fonts.FontStyle.FONT_WEIGHT_UNSPECIFIED, to=android.graphics.fonts.FontStyle.FONT_WEIGHT_MAX) public int getTextFontWeight(); method @NonNull public android.os.LocaleList getTextLocales(); method public float getTextScaleX(); method @Px public float getTextSize(); @@ -54058,6 +54070,33 @@ package android.view.inputmethod { field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.TextAppearanceInfo> CREATOR; } + public static final class TextAppearanceInfo.Builder { + ctor public TextAppearanceInfo.Builder(); + method @NonNull public android.view.inputmethod.TextAppearanceInfo build(); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setAllCaps(boolean); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setElegantTextHeight(boolean); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setFallbackLineSpacing(boolean); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setFontFeatureSettings(@Nullable String); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setFontVariationSettings(@Nullable String); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setHighlightTextColor(@ColorInt int); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setHintTextColor(@ColorInt int); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setLetterSpacing(float); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setLineBreakStyle(int); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setLineBreakWordStyle(int); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setLinkTextColor(@ColorInt int); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setShadowColor(@ColorInt int); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setShadowDx(@Px float); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setShadowDy(@Px float); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setShadowRadius(@Px float); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setSystemFontFamilyName(@Nullable String); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextColor(@ColorInt int); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextFontWeight(@IntRange(from=android.graphics.fonts.FontStyle.FONT_WEIGHT_UNSPECIFIED, to=android.graphics.fonts.FontStyle.FONT_WEIGHT_MAX) int); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextLocales(@NonNull android.os.LocaleList); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextScaleX(float); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextSize(@Px float); + method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextStyle(int); + } + public final class TextAttribute implements android.os.Parcelable { method public int describeContents(); method @NonNull public android.os.PersistableBundle getExtras(); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index e928eb5f7731..e6ddf9f6daf0 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -413,6 +413,7 @@ package android.provider { public final class DeviceConfig { field public static final String NAMESPACE_ALARM_MANAGER = "alarm_manager"; + field public static final String NAMESPACE_APP_CLONING = "app_cloning"; field public static final String NAMESPACE_APP_STANDBY = "app_standby"; field public static final String NAMESPACE_DEVICE_IDLE = "device_idle"; } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ce1eff143185..cb9b76dd5c22 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -1109,6 +1109,7 @@ package android.app.admin { method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public android.os.UserHandle createAndProvisionManagedProfile(@NonNull android.app.admin.ManagedProfileProvisioningParams) throws android.app.admin.ProvisioningException; method @Nullable public android.content.Intent createProvisioningIntentFromNfcIntent(@NonNull android.content.Intent); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void finalizeWorkProfileProvisioning(@NonNull android.os.UserHandle, @Nullable android.accounts.Account); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) public java.util.Set<java.lang.Integer> getApplicationExemptions(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException; method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public boolean getBluetoothContactSharingDisabled(@NonNull android.os.UserHandle); method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public String getDeviceOwner(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS}) public android.content.ComponentName getDeviceOwnerComponentOnAnyUser(); @@ -1134,6 +1135,7 @@ package android.app.admin { method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, android.Manifest.permission.PROVISION_DEMO_DEVICE}) public void provisionFullyManagedDevice(@NonNull android.app.admin.FullyManagedDeviceProvisioningParams) throws android.app.admin.ProvisioningException; method @RequiresPermission(android.Manifest.permission.TRIGGER_LOST_MODE) public void sendLostModeLocationUpdate(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS) public boolean setActiveProfileOwner(@NonNull android.content.ComponentName, String) throws java.lang.IllegalArgumentException; + method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) public void setApplicationExemptions(@NonNull String, @NonNull java.util.Set<java.lang.Integer>) throws android.content.pm.PackageManager.NameNotFoundException; method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void setDeviceProvisioningConfigApplied(); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setDpcDownloaded(boolean); method @Deprecated @RequiresPermission(value=android.Manifest.permission.GRANT_PROFILE_OWNER_DEVICE_IDS_ACCESS, conditional=true) public void setProfileOwnerCanAccessDeviceIds(@NonNull android.content.ComponentName); @@ -1155,6 +1157,7 @@ package android.app.admin { field public static final String ACTION_SET_PROFILE_OWNER = "android.app.action.SET_PROFILE_OWNER"; field @Deprecated public static final String ACTION_STATE_USER_SETUP_COMPLETE = "android.app.action.STATE_USER_SETUP_COMPLETE"; field @RequiresPermission(android.Manifest.permission.LAUNCH_DEVICE_MANAGER_SETUP) public static final String ACTION_UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER = "android.app.action.UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER"; + field public static final int EXEMPT_FROM_APP_STANDBY = 0; // 0x0 field public static final String EXTRA_FORCE_UPDATE_ROLE_HOLDER = "android.app.extra.FORCE_UPDATE_ROLE_HOLDER"; field public static final String EXTRA_LOST_MODE_LOCATION = "android.app.extra.LOST_MODE_LOCATION"; field public static final String EXTRA_PROFILE_OWNER_NAME = "android.app.extra.PROFILE_OWNER_NAME"; @@ -2909,6 +2912,7 @@ package android.companion.virtual { method @NonNull public java.util.Set<android.content.ComponentName> getBlockedCrossTaskNavigations(); method public int getDefaultActivityPolicy(); method public int getDefaultNavigationPolicy(); + method public int getDevicePolicy(int); method public int getLockState(); method @Nullable public String getName(); method @NonNull public java.util.Set<android.os.UserHandle> getUsersWithMatchingAccounts(); @@ -2916,14 +2920,18 @@ package android.companion.virtual { field public static final int ACTIVITY_POLICY_DEFAULT_ALLOWED = 0; // 0x0 field public static final int ACTIVITY_POLICY_DEFAULT_BLOCKED = 1; // 0x1 field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.VirtualDeviceParams> CREATOR; + field public static final int DEVICE_POLICY_CUSTOM = 1; // 0x1 + field public static final int DEVICE_POLICY_DEFAULT = 0; // 0x0 field public static final int LOCK_STATE_ALWAYS_UNLOCKED = 1; // 0x1 field public static final int LOCK_STATE_DEFAULT = 0; // 0x0 field public static final int NAVIGATION_POLICY_DEFAULT_ALLOWED = 0; // 0x0 field public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1 + field public static final int POLICY_TYPE_SENSORS = 0; // 0x0 } public static final class VirtualDeviceParams.Builder { ctor public VirtualDeviceParams.Builder(); + method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder addDevicePolicy(int, int); method @NonNull public android.companion.virtual.VirtualDeviceParams build(); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedActivities(@NonNull java.util.Set<android.content.ComponentName>); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>); @@ -3502,6 +3510,7 @@ package android.content.pm { field public static final String FEATURE_REBOOT_ESCROW = "android.hardware.reboot_escrow"; field public static final String FEATURE_TELEPHONY_CARRIERLOCK = "android.hardware.telephony.carrierlock"; field public static final String FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION = "android.hardware.telephony.ims.singlereg"; + field public static final String FEATURE_VIRTUALIZATION_FRAMEWORK = "android.software.virtualization_framework"; field public static final int FLAGS_PERMISSION_RESERVED_PERMISSION_CONTROLLER = -268435456; // 0xf0000000 field public static final int FLAG_PERMISSION_APPLY_RESTRICTION = 16384; // 0x4000 field public static final int FLAG_PERMISSION_AUTO_REVOKED = 131072; // 0x20000 diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 501b136726d3..30ff05284f3a 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1685,7 +1685,7 @@ public class Activity extends ContextThemeWrapper .isOnBackInvokedCallbackEnabled(this); if (aheadOfTimeBack) { // Add onBackPressed as default back behavior. - mDefaultBackCallback = this::navigateBack; + mDefaultBackCallback = this::onBackInvoked; getOnBackInvokedDispatcher().registerSystemOnBackInvokedCallback(mDefaultBackCallback); } } @@ -4003,22 +4003,19 @@ public class Activity extends ContextThemeWrapper if (!fragmentManager.isStateSaved() && fragmentManager.popBackStackImmediate()) { return; } - navigateBack(); + onBackInvoked(); } - private void navigateBack() { - if (!isTaskRoot()) { - // If the activity is not the root of the task, allow finish to proceed normally. - finishAfterTransition(); - return; - } - // Inform activity task manager that the activity received a back press while at the - // root of the task. This call allows ActivityTaskManager to intercept or move the task - // to the back. - ActivityClient.getInstance().onBackPressedOnTaskRoot(mToken, + private void onBackInvoked() { + // Inform activity task manager that the activity received a back press. + // This call allows ActivityTaskManager to intercept or move the task + // to the back when needed. + ActivityClient.getInstance().onBackPressed(mToken, new RequestFinishCallback(new WeakReference<>(this))); - getAutofillClientController().onActivityBackPressed(mIntent); + if (isTaskRoot()) { + getAutofillClientController().onActivityBackPressed(mIntent); + } } /** diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index d1e6780e3618..4cf48abc2ed3 100644 --- a/core/java/android/app/ActivityClient.java +++ b/core/java/android/app/ActivityClient.java @@ -525,9 +525,9 @@ public class ActivityClient { } } - void onBackPressedOnTaskRoot(IBinder token, IRequestFinishCallback callback) { + void onBackPressed(IBinder token, IRequestFinishCallback callback) { try { - getActivityClientController().onBackPressedOnTaskRoot(token, callback); + getActivityClientController().onBackPressed(token, callback); } catch (RemoteException e) { e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 1b3282e752f4..d5879fb523ce 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1407,9 +1407,41 @@ public class AppOpsManager { public static final int OP_FOREGROUND_SERVICE_SPECIAL_USE = AppProtoEnums.APP_OP_FOREGROUND_SERVICE_SPECIAL_USE; + /** + * Exempt from start foreground service from background restriction. + * + * Only to be used by the system. + * + * @hide + */ + public static final int OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION = + AppProtoEnums.APP_OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION; + + /** + * Exempt from start foreground service from background with while in user permission + * restriction. + * + * Only to be used by the system. + * + * @hide + */ + public static final int OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION = + AppProtoEnums + .APP_OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION; + + /** + * Hide foreground service stop button in quick settings. + * + * Only to be used by the system. + * + * @hide + */ + public static final int OP_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON = + AppProtoEnums.APP_OP_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 128; + public static final int _NUM_OP = 131; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -1947,6 +1979,38 @@ public class AppOpsManager { public static final String OPSTR_FOREGROUND_SERVICE_SPECIAL_USE = "android:foreground_service_special_use"; + /** + * Exempt from start foreground service from background restriction. + * + * Only to be used by the system. + * + * @hide + */ + public static final String OPSTR_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION = + "android:system_exempt_from_fgs_bg_start_restriction"; + + /** + * Exempt from start foreground service from background with while in user permission + * restriction. + * + * Only to be used by the system. + * + * @hide + */ + public static final String + OPSTR_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION = + "android:system_exempt_from_fgs_bg_start_while_in_use_permission_restriction"; + + /** + * Hide foreground service stop button in quick settings. + * + * Only to be used by the system. + * + * @hide + */ + public static final String OPSTR_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON = + "android:system_exempt_from_fgs_stop_button"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2441,6 +2505,17 @@ public class AppOpsManager { new AppOpInfo.Builder(OP_FOREGROUND_SERVICE_SPECIAL_USE, OPSTR_FOREGROUND_SERVICE_SPECIAL_USE, "FOREGROUND_SERVICE_SPECIAL_USE") .setPermission(Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE).build(), + new AppOpInfo.Builder(OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION, + OPSTR_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION, + "SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION").build(), + new AppOpInfo.Builder( + OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION, + OPSTR_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION, + "SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION") + .build(), + new AppOpInfo.Builder(OP_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON, + OPSTR_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON, + "SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON").build() }; // The number of longs needed to form a full bitmask of app ops @@ -2498,12 +2573,6 @@ public class AppOpsManager { sPermToOp.put(sAppOpInfos[op].permission, op); } } - - if ((_NUM_OP + Long.SIZE - 1) / Long.SIZE != 2) { - // The code currently assumes that the length of sAppOpsNotedInThisBinderTransaction is - // two longs - throw new IllegalStateException("notedAppOps collection code assumes < 128 appops"); - } } /** Config used to control app ops access messages sampling */ @@ -2603,8 +2672,8 @@ public class AppOpsManager { if (boxedOpCode != null) { return boxedOpCode; } - if (HealthConnectManager.isHealthPermission(ActivityThread.currentApplication(), - permission)) { + if (permission != null && HealthConnectManager.isHealthPermission( + ActivityThread.currentApplication(), permission)) { return OP_READ_WRITE_HEALTH_DATA; } return OP_NONE; diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index 45d44589b2d8..1777f37202c2 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -63,7 +63,6 @@ public class BroadcastOptions extends ComponentOptions { private long mRequireCompatChangeId = CHANGE_INVALID; private boolean mRequireCompatChangeEnabled = true; private boolean mIsAlarmBroadcast = false; - private boolean mIsInteractiveBroadcast = false; private long mIdForResponseEvent; private @Nullable IntentFilter mRemoveMatchingFilter; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; @@ -171,13 +170,6 @@ public class BroadcastOptions extends ComponentOptions { "android:broadcast.is_alarm"; /** - * Corresponds to {@link #setInteractiveBroadcast(boolean)} - * @hide - */ - public static final String KEY_INTERACTIVE_BROADCAST = - "android:broadcast.is_interactive"; - - /** * @hide * @deprecated Use {@link android.os.PowerExemptionManager# * TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED} instead. @@ -308,7 +300,6 @@ public class BroadcastOptions extends ComponentOptions { mRequireCompatChangeEnabled = opts.getBoolean(KEY_REQUIRE_COMPAT_CHANGE_ENABLED, true); mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT); mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false); - mIsInteractiveBroadcast = opts.getBoolean(KEY_INTERACTIVE_BROADCAST, false); mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER, IntentFilter.class); mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, @@ -629,28 +620,6 @@ public class BroadcastOptions extends ComponentOptions { } /** - * When set, this broadcast will be understood as having originated from - * some direct interaction by the user such as a notification tap or button - * press. Only the OS itself may use this option. - * @hide - * @param broadcastIsInteractive - * @see #isInteractiveBroadcast() - */ - @RequiresPermission(android.Manifest.permission.BROADCAST_OPTION_INTERACTIVE) - public void setInteractiveBroadcast(boolean broadcastIsInteractive) { - mIsInteractiveBroadcast = broadcastIsInteractive; - } - - /** - * Did this broadcast originate with a direct user interaction? - * @return true if this broadcast is the result of an interaction, false otherwise - * @hide - */ - public boolean isInteractiveBroadcast() { - return mIsInteractiveBroadcast; - } - - /** * Did this broadcast originate from a push message from the server? * * @return true if this broadcast is a push message, false otherwise. @@ -837,9 +806,6 @@ public class BroadcastOptions extends ComponentOptions { if (mIsAlarmBroadcast) { b.putBoolean(KEY_ALARM_BROADCAST, true); } - if (mIsInteractiveBroadcast) { - b.putBoolean(KEY_INTERACTIVE_BROADCAST, true); - } if (mMinManifestReceiverApiLevel != 0) { b.putInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, mMinManifestReceiverApiLevel); } diff --git a/core/java/android/app/ComponentOptions.java b/core/java/android/app/ComponentOptions.java index 4e5e384a2798..74db39f63830 100644 --- a/core/java/android/app/ComponentOptions.java +++ b/core/java/android/app/ComponentOptions.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.RequiresPermission; import android.os.Bundle; /** @@ -45,8 +46,15 @@ public class ComponentOptions { public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION = "android.pendingIntent.backgroundActivityAllowedByPermission"; + /** + * Corresponds to {@link #setInteractive(boolean)} + * @hide + */ + public static final String KEY_INTERACTIVE = "android:component.isInteractive"; + private boolean mPendingIntentBalAllowed = PENDING_INTENT_BAL_ALLOWED_DEFAULT; private boolean mPendingIntentBalAllowedByPermission = false; + private boolean mIsInteractive = false; ComponentOptions() { } @@ -61,6 +69,29 @@ public class ComponentOptions { setPendingIntentBackgroundActivityLaunchAllowedByPermission( opts.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, false)); + mIsInteractive = opts.getBoolean(KEY_INTERACTIVE, false); + } + + /** + * When set, a broadcast will be understood as having originated from + * some direct interaction by the user such as a notification tap or button + * press. Only the OS itself may use this option. + * @hide + * @param interactive + * @see #isInteractive() + */ + @RequiresPermission(android.Manifest.permission.COMPONENT_OPTION_INTERACTIVE) + public void setInteractive(boolean interactive) { + mIsInteractive = interactive; + } + + /** + * Did this PendingIntent send originate with a direct user interaction? + * @return true if this is the result of an interaction, false otherwise + * @hide + */ + public boolean isInteractive() { + return mIsInteractive; } /** @@ -103,6 +134,9 @@ public class ComponentOptions { b.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, mPendingIntentBalAllowedByPermission); } + if (mIsInteractive) { + b.putBoolean(KEY_INTERACTIVE, true); + } return b; } } diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 9aa67bc51182..62481ba8f251 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -145,10 +145,9 @@ interface IActivityClientController { void unregisterRemoteAnimations(in IBinder token); /** - * Reports that an Activity received a back key press when there were no additional activities - * on the back stack. + * Reports that an Activity received a back key press. */ - oneway void onBackPressedOnTaskRoot(in IBinder activityToken, + oneway void onBackPressed(in IBinder activityToken, in IRequestFinishCallback callback); /** Reports that the splash screen view has attached to activity. */ diff --git a/core/java/android/app/IBackupAgent.aidl b/core/java/android/app/IBackupAgent.aidl index 37c5cabc2376..811118479ef8 100644 --- a/core/java/android/app/IBackupAgent.aidl +++ b/core/java/android/app/IBackupAgent.aidl @@ -16,9 +16,12 @@ package android.app; +import android.app.backup.BackupRestoreEventLogger; import android.app.backup.IBackupCallback; import android.app.backup.IBackupManager; import android.os.ParcelFileDescriptor; + +import com.android.internal.infra.AndroidFuture; /** * Interface presented by applications being asked to participate in the @@ -193,4 +196,14 @@ oneway interface IBackupAgent { * @param message The message to be passed to the agent's application in an exception. */ void fail(String message); + + /** + * Provides the logging results that were accumulated in the BackupAgent during a backup or + * restore operation. This method should be called after the agent completes its backup or + * restore. + * + * @param resultsFuture a future that is completed with the logging results. + */ + void getLoggerResults( + in AndroidFuture<List<BackupRestoreEventLogger.DataTypeResult>> resultsFuture); } diff --git a/core/java/android/app/IRequestFinishCallback.aidl b/core/java/android/app/IRequestFinishCallback.aidl index 22c20c840e99..72426df84b75 100644 --- a/core/java/android/app/IRequestFinishCallback.aidl +++ b/core/java/android/app/IRequestFinishCallback.aidl @@ -18,7 +18,7 @@ package android.app; /** * This callback allows ActivityTaskManager to ask the calling Activity - * to finish in response to a call to onBackPressedOnTaskRoot. + * to finish in response to a call to onBackPressed. * * {@hide} */ diff --git a/core/java/android/app/IntentService.java b/core/java/android/app/IntentService.java index 2e833084641c..99f864c32803 100644 --- a/core/java/android/app/IntentService.java +++ b/core/java/android/app/IntentService.java @@ -57,8 +57,7 @@ import android.os.Message; * @deprecated IntentService is subject to all the * <a href="{@docRoot}about/versions/oreo/background.html">background execution limits</a> * imposed with Android 8.0 (API level 26). Consider using {@link androidx.work.WorkManager} - * or {@link androidx.core.app.JobIntentService}, which uses jobs - * instead of services when running on Android 8.0 or higher. + * instead. */ @Deprecated public abstract class IntentService extends Service { diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 01e4b150fd7c..7167d4f17a6d 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -17,6 +17,8 @@ package android.app; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST; +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; +import static android.text.TextUtils.formatSimple; import android.annotation.IntDef; import android.annotation.NonNull; @@ -34,6 +36,7 @@ import android.content.res.Configuration; import android.os.Build; import android.os.IBinder; import android.os.RemoteException; +import android.os.Trace; import android.util.ArrayMap; import android.util.Log; import android.view.contentcapture.ContentCaptureManager; @@ -763,10 +766,12 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac */ public final void startForeground(int id, Notification notification) { try { + final ComponentName comp = new ComponentName(this, mClassName); mActivityManager.setServiceForeground( - new ComponentName(this, mClassName), mToken, id, + comp, mToken, id, notification, 0, FOREGROUND_SERVICE_TYPE_MANIFEST); clearStartForegroundServiceStackTrace(); + logForegroundServiceStart(comp, FOREGROUND_SERVICE_TYPE_MANIFEST); } catch (RemoteException ex) { } } @@ -843,10 +848,12 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac public final void startForeground(int id, @NonNull Notification notification, @RequiresPermission @ForegroundServiceType int foregroundServiceType) { try { + final ComponentName comp = new ComponentName(this, mClassName); mActivityManager.setServiceForeground( - new ComponentName(this, mClassName), mToken, id, + comp, mToken, id, notification, 0, foregroundServiceType); clearStartForegroundServiceStackTrace(); + logForegroundServiceStart(comp, foregroundServiceType); } catch (RemoteException ex) { } } @@ -897,6 +904,7 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac mActivityManager.setServiceForeground( new ComponentName(this, mClassName), mToken, 0, null, notificationBehavior, 0); + logForegroundServiceStopIfNecessary(); } catch (RemoteException ex) { } } @@ -992,6 +1000,7 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac */ public final void detachAndCleanUp() { mToken = null; + logForegroundServiceStopIfNecessary(); } final String getClassName() { @@ -1025,6 +1034,50 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac private boolean mStartCompatibility = false; /** + * This will be set to the title of the system trace when this service is started as + * a foreground service, and will be set to null when it's no longer in foreground + * service state. + */ + @GuardedBy("mForegroundServiceTraceTitleLock") + private @Nullable String mForegroundServiceTraceTitle = null; + + private final Object mForegroundServiceTraceTitleLock = new Object(); + + private static final String TRACE_TRACK_NAME_FOREGROUND_SERVICE = "FGS"; + + private void logForegroundServiceStart(ComponentName comp, + @ForegroundServiceType int foregroundServiceType) { + synchronized (mForegroundServiceTraceTitleLock) { + if (mForegroundServiceTraceTitle == null) { + mForegroundServiceTraceTitle = formatSimple("comp=%s type=%s", + comp.toShortString(), Integer.toHexString(foregroundServiceType)); + // The service is not in foreground state, emit a start event. + Trace.asyncTraceForTrackBegin(TRACE_TAG_ACTIVITY_MANAGER, + TRACE_TRACK_NAME_FOREGROUND_SERVICE, + mForegroundServiceTraceTitle, + System.identityHashCode(this)); + } else { + // The service is already in foreground state, emit an one-off event. + Trace.instantForTrack(TRACE_TAG_ACTIVITY_MANAGER, + TRACE_TRACK_NAME_FOREGROUND_SERVICE, + mForegroundServiceTraceTitle); + } + } + } + + private void logForegroundServiceStopIfNecessary() { + synchronized (mForegroundServiceTraceTitleLock) { + if (mForegroundServiceTraceTitle != null) { + Trace.asyncTraceForTrackEnd(TRACE_TAG_ACTIVITY_MANAGER, + TRACE_TRACK_NAME_FOREGROUND_SERVICE, + mForegroundServiceTraceTitle, + System.identityHashCode(this)); + mForegroundServiceTraceTitle = null; + } + } + } + + /** * This keeps track of the stacktrace where Context.startForegroundService() was called * for each service class. We use that when we crash the app for not calling * {@link #startForeground} in time, in {@link ActivityThread#throwRemoteServiceException}. diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 6d289726baf5..a035375f2f82 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -229,6 +229,8 @@ public class StatusBarManager { public static final int CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP = 1; /** @hide */ public static final int CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = 2; + /** @hide */ + public static final int CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE = 3; /** * Session flag for {@link #registerSessionListener} indicating the listener diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 6fedb41884ec..be4df9d24c25 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -103,6 +103,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.infra.AndroidFuture; import com.android.internal.net.NetworkUtilsInternal; import com.android.internal.os.BackgroundThread; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import com.android.org.conscrypt.TrustedCertificateStore; @@ -3823,6 +3824,27 @@ public class DevicePolicyManager { public static final int OPERATION_SAFETY_REASON_DRIVING_DISTRACTION = 1; /** + * Prevent an app from being placed into app standby buckets, such that it will not be subject + * to device resources restrictions as a result of app standby buckets. + * + * @hide + */ + @SystemApi + public static final int EXEMPT_FROM_APP_STANDBY = 0; + + /** + * Exemptions to platform restrictions, given to an application through + * {@link #setApplicationExemptions(String, Set)}. + * + * @hide + */ + @IntDef(prefix = { "EXEMPT_FROM_"}, value = { + EXEMPT_FROM_APP_STANDBY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ApplicationExemptionConstants {} + + /** * Broadcast action: notify system apps (e.g. settings, SysUI, etc) that the device management * resources with IDs {@link #EXTRA_RESOURCE_IDS} has been updated, the updated resources can be * retrieved using {@link DevicePolicyResourcesManager#getDrawable} and @@ -14727,6 +14749,95 @@ public class DevicePolicyManager { } /** + * Service-specific error code used in {@link #setApplicationExemptions(String, Set)} and + * {@link #getApplicationExemptions(String)}. + * @hide + */ + public static final int ERROR_PACKAGE_NAME_NOT_FOUND = 1; + + /** + * Called by an application with the + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS} permission, to + * grant platform restriction exemptions to a given application. + * + * @param packageName The package name of the application to be exempt. + * @param exemptions The set of exemptions to be applied. + * @throws SecurityException If the caller does not have + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS} + * @throws NameNotFoundException If either the package is not installed or the package is not + * visible to the caller. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) + public void setApplicationExemptions(@NonNull String packageName, + @NonNull @ApplicationExemptionConstants Set<Integer> exemptions) + throws NameNotFoundException { + throwIfParentInstance("setApplicationExemptions"); + if (mService != null) { + try { + mService.setApplicationExemptions(packageName, + ArrayUtils.convertToIntArray(new ArraySet<>(exemptions))); + } catch (ServiceSpecificException e) { + switch (e.errorCode) { + case ERROR_PACKAGE_NAME_NOT_FOUND: + throw new NameNotFoundException(e.getMessage()); + default: + throw new RuntimeException( + "Unknown error setting application exemptions: " + e.errorCode, e); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Returns all the platform restriction exemptions currently applied to an application. Called + * by an application with the + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS} permission. + * + * @param packageName The package name to check. + * @return A set of platform restrictions an application is exempt from. + * @throws SecurityException If the caller does not have + * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS} + * @throws NameNotFoundException If either the package is not installed or the package is not + * visible to the caller. + * @hide + */ + @NonNull + @SystemApi + @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) + public Set<Integer> getApplicationExemptions(@NonNull String packageName) + throws NameNotFoundException { + throwIfParentInstance("getApplicationExemptions"); + if (mService == null) { + return Collections.emptySet(); + } + try { + return intArrayToSet(mService.getApplicationExemptions(packageName)); + } catch (ServiceSpecificException e) { + switch (e.errorCode) { + case ERROR_PACKAGE_NAME_NOT_FOUND: + throw new NameNotFoundException(e.getMessage()); + default: + throw new RuntimeException( + "Unknown error getting application exemptions: " + e.errorCode, e); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private Set<Integer> intArrayToSet(int[] array) { + Set<Integer> set = new ArraySet<>(); + for (int item : array) { + set.add(item); + } + return set; + } + + /** * Called by a device owner or a profile owner to disable user 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. Starting from Android 13, packages with user control disabled are diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 6c27dd7b771b..8a4026539267 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -568,4 +568,7 @@ interface IDevicePolicyManager { boolean shouldAllowBypassingDevicePolicyManagementRoleQualification(); List<UserHandle> getPolicyManagedProfiles(in UserHandle userHandle); + + void setApplicationExemptions(String packageName, in int[]exemptions); + int[] getApplicationExemptions(String packageName); } diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index b1b59b0e39b1..a4f612d7faee 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -41,6 +41,7 @@ import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.infra.AndroidFuture; import libcore.io.IoUtils; @@ -202,6 +203,7 @@ public abstract class BackupAgent extends ContextWrapper { Handler mHandler = null; + @Nullable private volatile BackupRestoreEventLogger mLogger = null; @Nullable private UserHandle mUser; // This field is written from the main thread (in onCreate), and read in a Binder thread (in // onFullBackup that is called from system_server via Binder). @@ -234,6 +236,20 @@ public abstract class BackupAgent extends ContextWrapper { } catch (InterruptedException e) { /* ignored */ } } + /** + * Get a logger to record app-specific backup and restore events that are happening during a + * backup or restore operation. + * + * <p>The logger instance had been created by the system with the correct {@link + * BackupRestoreEventLogger.OperationType} that corresponds to the operation the {@code + * BackupAgent} is currently handling. + * + * @hide + */ + @Nullable + public BackupRestoreEventLogger getBackupRestoreEventLogger() { + return mLogger; + } public BackupAgent() { super(null); @@ -264,6 +280,9 @@ public abstract class BackupAgent extends ContextWrapper { * @hide */ public void onCreate(UserHandle user, @OperationType int operationType) { + // TODO: Instantiate with the correct type using a parameter. + mLogger = new BackupRestoreEventLogger(BackupRestoreEventLogger.OperationType.BACKUP); + onCreate(); mUser = user; @@ -1305,6 +1324,16 @@ public abstract class BackupAgent extends ContextWrapper { } } } + + @Override + public void getLoggerResults( + AndroidFuture<List<BackupRestoreEventLogger.DataTypeResult>> in) { + if (mLogger != null) { + in.complete(mLogger.getLoggingResults()); + } else { + in.complete(Collections.emptyList()); + } + } } static class FailRunnable implements Runnable { diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.aidl b/core/java/android/app/backup/BackupRestoreEventLogger.aidl new file mode 100644 index 000000000000..d6ef4e64258d --- /dev/null +++ b/core/java/android/app/backup/BackupRestoreEventLogger.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.app.backup; + +parcelable BackupRestoreEventLogger.DataTypeResult;
\ No newline at end of file diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java index 6f62c8a03078..68740cb3c086 100644 --- a/core/java/android/app/backup/BackupRestoreEventLogger.java +++ b/core/java/android/app/backup/BackupRestoreEventLogger.java @@ -19,6 +19,10 @@ package android.app.backup; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; import android.util.Slog; import java.lang.annotation.Retention; @@ -312,7 +316,7 @@ public class BackupRestoreEventLogger { /** * Encapsulate logging results for a single data type. */ - public static class DataTypeResult { + public static class DataTypeResult implements Parcelable { @BackupRestoreDataType private final String mDataType; private int mSuccessCount; @@ -362,5 +366,57 @@ public class BackupRestoreEventLogger { public byte[] getMetadataHash() { return mMetadataHash; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mDataType); + + dest.writeInt(mSuccessCount); + + dest.writeInt(mFailCount); + + Bundle errorsBundle = new Bundle(); + for (Map.Entry<String, Integer> e : mErrors.entrySet()) { + errorsBundle.putInt(e.getKey(), e.getValue()); + } + dest.writeBundle(errorsBundle); + + dest.writeByteArray(mMetadataHash); + } + + public static final Parcelable.Creator<DataTypeResult> CREATOR = + new Parcelable.Creator<>() { + public DataTypeResult createFromParcel(Parcel in) { + String dataType = in.readString(); + + int successCount = in.readInt(); + + int failCount = in.readInt(); + + Map<String, Integer> errors = new ArrayMap<>(); + Bundle errorsBundle = in.readBundle(getClass().getClassLoader()); + for (String key : errorsBundle.keySet()) { + errors.put(key, errorsBundle.getInt(key)); + } + + byte[] metadataHash = in.createByteArray(); + + DataTypeResult result = new DataTypeResult(dataType); + result.mSuccessCount = successCount; + result.mFailCount = failCount; + result.mErrors.putAll(errors); + result.mMetadataHash = metadataHash; + return result; + } + + public DataTypeResult[] newArray(int size) { + return new DataTypeResult[size]; + } + }; } } diff --git a/core/java/android/app/prediction/AppPredictor.java b/core/java/android/app/prediction/AppPredictor.java index 2581daa2f68b..d628b7f92c6c 100644 --- a/core/java/android/app/prediction/AppPredictor.java +++ b/core/java/android/app/prediction/AppPredictor.java @@ -30,6 +30,8 @@ import android.os.ServiceManager; import android.util.ArrayMap; import android.util.Log; +import com.android.internal.annotations.GuardedBy; + import dalvik.system.CloseGuard; import java.util.List; @@ -79,6 +81,7 @@ public final class AppPredictor { private final AtomicBoolean mIsClosed = new AtomicBoolean(false); private final AppPredictionSessionId mSessionId; + @GuardedBy("itself") private final ArrayMap<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>(); /** @@ -94,7 +97,7 @@ public final class AppPredictor { IBinder b = ServiceManager.getService(Context.APP_PREDICTION_SERVICE); mPredictionManager = IPredictionManager.Stub.asInterface(b); mSessionId = new AppPredictionSessionId( - context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUserId()); + context.getPackageName() + ":" + UUID.randomUUID(), context.getUserId()); try { mPredictionManager.createPredictionSession(predictionContext, mSessionId, getToken()); } catch (RemoteException e) { @@ -155,6 +158,15 @@ public final class AppPredictor { */ public void registerPredictionUpdates(@NonNull @CallbackExecutor Executor callbackExecutor, @NonNull AppPredictor.Callback callback) { + synchronized (mRegisteredCallbacks) { + registerPredictionUpdatesLocked(callbackExecutor, callback); + } + } + + @GuardedBy("mRegisteredCallbacks") + private void registerPredictionUpdatesLocked( + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull AppPredictor.Callback callback) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } @@ -183,6 +195,13 @@ public final class AppPredictor { * @param callback The callback to be unregistered. */ public void unregisterPredictionUpdates(@NonNull AppPredictor.Callback callback) { + synchronized (mRegisteredCallbacks) { + unregisterPredictionUpdatesLocked(callback); + } + } + + @GuardedBy("mRegisteredCallbacks") + private void unregisterPredictionUpdatesLocked(@NonNull AppPredictor.Callback callback) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } @@ -235,7 +254,7 @@ public final class AppPredictor { } try { - mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice(targets), + mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice<>(targets), new CallbackWrapper(callbackExecutor, callback)); } catch (RemoteException e) { Log.e(TAG, "Failed to sort targets", e); @@ -251,19 +270,25 @@ public final class AppPredictor { if (!mIsClosed.getAndSet(true)) { mCloseGuard.close(); - // Do destroy; - try { - mPredictionManager.onDestroyPredictionSession(mSessionId); - } catch (RemoteException e) { - Log.e(TAG, "Failed to notify app target event", e); - e.rethrowAsRuntimeException(); + synchronized (mRegisteredCallbacks) { + destroySessionLocked(); } - mRegisteredCallbacks.clear(); } else { throw new IllegalStateException("This client has already been destroyed."); } } + @GuardedBy("mRegisteredCallbacks") + private void destroySessionLocked() { + try { + mPredictionManager.onDestroyPredictionSession(mSessionId); + } catch (RemoteException e) { + Log.e(TAG, "Failed to notify app target event", e); + e.rethrowAsRuntimeException(); + } + mRegisteredCallbacks.clear(); + } + @Override protected void finalize() throws Throwable { try { diff --git a/core/java/android/app/search/SearchSession.java b/core/java/android/app/search/SearchSession.java index 2cd1d96190b0..10db3376adc5 100644 --- a/core/java/android/app/search/SearchSession.java +++ b/core/java/android/app/search/SearchSession.java @@ -23,9 +23,11 @@ import android.app.search.ISearchCallback.Stub; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.Binder; +import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.util.Log; import dalvik.system.CloseGuard; @@ -70,7 +72,7 @@ import java.util.function.Consumer; * @hide */ @SystemApi -public final class SearchSession implements AutoCloseable{ +public final class SearchSession implements AutoCloseable { private static final String TAG = SearchSession.class.getSimpleName(); private static final boolean DEBUG = false; @@ -229,7 +231,14 @@ public final class SearchSession implements AutoCloseable{ if (DEBUG) { Log.d(TAG, "CallbackWrapper.onResult result=" + result.getList()); } - mExecutor.execute(() -> mCallback.accept(result.getList())); + List<SearchTarget> list = result.getList(); + if (list.size() > 0) { + Bundle bundle = list.get(0).getExtras(); + if (bundle != null) { + bundle.putLong("key_ipc_start", SystemClock.elapsedRealtime()); + } + } + mExecutor.execute(() -> mCallback.accept(list)); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/core/java/android/app/time/DetectorStatusTypes.java b/core/java/android/app/time/DetectorStatusTypes.java new file mode 100644 index 000000000000..3643fc9a7d86 --- /dev/null +++ b/core/java/android/app/time/DetectorStatusTypes.java @@ -0,0 +1,227 @@ +/* + * 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 android.app.time; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A set of constants that can relate to time or time zone detector status. + * + * <ul> + * <li>Detector status - the status of the overall detector.</li> + * <li>Detection algorithm status - the status of an algorithm that a detector can use. + * Each detector is expected to have one or more known algorithms to detect its chosen property, + * e.g. for time zone devices can have a "location" detection algorithm, where the device's + * location is used to detect the time zone.</li> + * </ul> + * + * @hide + */ +public final class DetectorStatusTypes { + + /** A status code for a detector. */ + @IntDef(prefix = "DETECTOR_STATUS_", value = { + DETECTOR_STATUS_UNKNOWN, + DETECTOR_STATUS_NOT_SUPPORTED, + DETECTOR_STATUS_NOT_RUNNING, + DETECTOR_STATUS_RUNNING, + }) + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.SOURCE) + public @interface DetectorStatus {} + + /** + * The detector status is unknown. Expected only for use as a placeholder before the actual + * status is known. + */ + public static final @DetectorStatus int DETECTOR_STATUS_UNKNOWN = 0; + + /** The detector is not supported on this device. */ + public static final @DetectorStatus int DETECTOR_STATUS_NOT_SUPPORTED = 1; + + /** The detector is supported but is not running. */ + public static final @DetectorStatus int DETECTOR_STATUS_NOT_RUNNING = 2; + + /** The detector is supported and is running. */ + public static final @DetectorStatus int DETECTOR_STATUS_RUNNING = 3; + + private DetectorStatusTypes() {} + + /** + * A status code for a detection algorithm. + */ + @IntDef(prefix = "DETECTION_ALGORITHM_STATUS_", value = { + DETECTION_ALGORITHM_STATUS_UNKNOWN, + DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED, + DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + DETECTION_ALGORITHM_STATUS_RUNNING, + }) + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.SOURCE) + public @interface DetectionAlgorithmStatus {} + + /** + * The detection algorithm status is unknown. Expected only for use as a placeholder before the + * actual status is known. + */ + public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_UNKNOWN = 0; + + /** The detection algorithm is not supported on this device. */ + public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED = 1; + + /** The detection algorithm supported but is not running. */ + public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_NOT_RUNNING = 2; + + /** The detection algorithm supported and is running. */ + public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_RUNNING = 3; + + /** + * Validates the supplied value is one of the known {@code DETECTOR_STATUS_} constants and + * returns it if it is valid. {@link #DETECTOR_STATUS_UNKNOWN} is considered valid. + * + * @throws IllegalArgumentException if the value is not recognized + */ + public static @DetectorStatus int requireValidDetectorStatus( + @DetectorStatus int detectorStatus) { + if (detectorStatus < DETECTOR_STATUS_UNKNOWN || detectorStatus > DETECTOR_STATUS_RUNNING) { + throw new IllegalArgumentException("Invalid detector status: " + detectorStatus); + } + return detectorStatus; + } + + /** + * Returns a string for each {@code DETECTOR_STATUS_} constant. See also + * {@link #detectorStatusFromString(String)}. + * + * @throws IllegalArgumentException if the value is not recognized + */ + @NonNull + public static String detectorStatusToString(@DetectorStatus int detectorStatus) { + switch (detectorStatus) { + case DETECTOR_STATUS_UNKNOWN: + return "UNKNOWN"; + case DETECTOR_STATUS_NOT_SUPPORTED: + return "NOT_SUPPORTED"; + case DETECTOR_STATUS_NOT_RUNNING: + return "NOT_RUNNING"; + case DETECTOR_STATUS_RUNNING: + return "RUNNING"; + default: + throw new IllegalArgumentException("Unknown status: " + detectorStatus); + } + } + + /** + * Returns {@code DETECTOR_STATUS_} constant value from a string. See also + * {@link #detectorStatusToString(int)}. + * + * @throws IllegalArgumentException if the value is not recognized or is invalid + */ + public static @DetectorStatus int detectorStatusFromString( + @Nullable String detectorStatusString) { + if (TextUtils.isEmpty(detectorStatusString)) { + throw new IllegalArgumentException("Empty status: " + detectorStatusString); + } + + switch (detectorStatusString) { + case "UNKNOWN": + return DETECTOR_STATUS_UNKNOWN; + case "NOT_SUPPORTED": + return DETECTOR_STATUS_NOT_SUPPORTED; + case "NOT_RUNNING": + return DETECTOR_STATUS_NOT_RUNNING; + case "RUNNING": + return DETECTOR_STATUS_RUNNING; + default: + throw new IllegalArgumentException("Unknown status: " + detectorStatusString); + } + } + + /** + * Validates the supplied value is one of the known {@code DETECTION_ALGORITHM_} constants and + * returns it if it is valid. {@link #DETECTION_ALGORITHM_STATUS_UNKNOWN} is considered valid. + * + * @throws IllegalArgumentException if the value is not recognized + */ + public static @DetectionAlgorithmStatus int requireValidDetectionAlgorithmStatus( + @DetectionAlgorithmStatus int detectionAlgorithmStatus) { + if (detectionAlgorithmStatus < DETECTION_ALGORITHM_STATUS_UNKNOWN + || detectionAlgorithmStatus > DETECTION_ALGORITHM_STATUS_RUNNING) { + throw new IllegalArgumentException( + "Invalid detection algorithm: " + detectionAlgorithmStatus); + } + return detectionAlgorithmStatus; + } + + /** + * Returns a string for each {@code DETECTION_ALGORITHM_} constant. See also + * {@link #detectionAlgorithmStatusFromString(String)} + * + * @throws IllegalArgumentException if the value is not recognized + */ + @NonNull + public static String detectionAlgorithmStatusToString( + @DetectionAlgorithmStatus int detectorAlgorithmStatus) { + switch (detectorAlgorithmStatus) { + case DETECTION_ALGORITHM_STATUS_UNKNOWN: + return "UNKNOWN"; + case DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED: + return "NOT_SUPPORTED"; + case DETECTION_ALGORITHM_STATUS_NOT_RUNNING: + return "NOT_RUNNING"; + case DETECTION_ALGORITHM_STATUS_RUNNING: + return "RUNNING"; + default: + throw new IllegalArgumentException("Unknown status: " + detectorAlgorithmStatus); + } + } + + /** + * Returns {@code DETECTION_ALGORITHM_} constant value from a string. See also + * {@link #detectionAlgorithmStatusToString(int)} (String)} + * + * @throws IllegalArgumentException if the value is not recognized or is invalid + */ + public static @DetectionAlgorithmStatus int detectionAlgorithmStatusFromString( + @Nullable String detectorAlgorithmStatusString) { + + if (TextUtils.isEmpty(detectorAlgorithmStatusString)) { + throw new IllegalArgumentException("Empty status: " + detectorAlgorithmStatusString); + } + + switch (detectorAlgorithmStatusString) { + case "UNKNOWN": + return DETECTION_ALGORITHM_STATUS_UNKNOWN; + case "NOT_SUPPORTED": + return DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED; + case "NOT_RUNNING": + return DETECTION_ALGORITHM_STATUS_NOT_RUNNING; + case "RUNNING": + return DETECTION_ALGORITHM_STATUS_RUNNING; + default: + throw new IllegalArgumentException( + "Unknown status: " + detectorAlgorithmStatusString); + } + } +} diff --git a/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.aidl b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.aidl new file mode 100644 index 000000000000..7184b123af1c --- /dev/null +++ b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.app.time; + +parcelable LocationTimeZoneAlgorithmStatus; diff --git a/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java new file mode 100644 index 000000000000..710b8c40cefe --- /dev/null +++ b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java @@ -0,0 +1,363 @@ +/* + * 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_UNKNOWN; +import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusFromString; +import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusToString; +import static android.app.time.DetectorStatusTypes.requireValidDetectionAlgorithmStatus; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus; +import android.os.Parcel; +import android.os.Parcelable; +import android.service.timezone.TimeZoneProviderStatus; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Information about the status of the location-based time zone detection algorithm. + * + * @hide + */ +public final class LocationTimeZoneAlgorithmStatus implements Parcelable { + + /** + * An enum that describes a location time zone provider's status. + * + * @hide + */ + @IntDef(prefix = "PROVIDER_STATUS_", value = { + PROVIDER_STATUS_NOT_PRESENT, + PROVIDER_STATUS_NOT_READY, + PROVIDER_STATUS_IS_CERTAIN, + PROVIDER_STATUS_IS_UNCERTAIN, + }) + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.SOURCE) + public @interface ProviderStatus {} + + /** + * Indicates a provider is not present because it has not been configured, the configuration + * is bad, or the provider has reported a permanent failure. + */ + public static final @ProviderStatus int PROVIDER_STATUS_NOT_PRESENT = 1; + + /** + * Indicates a provider has not reported it is certain or uncertain. This may be because it has + * just started running, or it has been stopped. + */ + public static final @ProviderStatus int PROVIDER_STATUS_NOT_READY = 2; + + /** + * Indicates a provider last reported it is certain. + */ + public static final @ProviderStatus int PROVIDER_STATUS_IS_CERTAIN = 3; + + /** + * Indicates a provider last reported it is uncertain. + */ + public static final @ProviderStatus int PROVIDER_STATUS_IS_UNCERTAIN = 4; + + /** + * An instance that provides no information about algorithm status because the algorithm has not + * yet reported. Effectively a "null" status placeholder. + */ + @NonNull + public static final LocationTimeZoneAlgorithmStatus UNKNOWN = + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_UNKNOWN, + PROVIDER_STATUS_NOT_READY, null, PROVIDER_STATUS_NOT_READY, null); + + private final @DetectionAlgorithmStatus int mStatus; + private final @ProviderStatus int mPrimaryProviderStatus; + // May be populated when mPrimaryProviderReportedStatus == PROVIDER_STATUS_IS_CERTAIN + // or PROVIDER_STATUS_IS_UNCERTAIN + @Nullable private final TimeZoneProviderStatus mPrimaryProviderReportedStatus; + + private final @ProviderStatus int mSecondaryProviderStatus; + // May be populated when mSecondaryProviderReportedStatus == PROVIDER_STATUS_IS_CERTAIN + // or PROVIDER_STATUS_IS_UNCERTAIN + @Nullable private final TimeZoneProviderStatus mSecondaryProviderReportedStatus; + + public LocationTimeZoneAlgorithmStatus( + @DetectionAlgorithmStatus int status, + @ProviderStatus int primaryProviderStatus, + @Nullable TimeZoneProviderStatus primaryProviderReportedStatus, + @ProviderStatus int secondaryProviderStatus, + @Nullable TimeZoneProviderStatus secondaryProviderReportedStatus) { + + mStatus = requireValidDetectionAlgorithmStatus(status); + mPrimaryProviderStatus = requireValidProviderStatus(primaryProviderStatus); + mPrimaryProviderReportedStatus = primaryProviderReportedStatus; + mSecondaryProviderStatus = requireValidProviderStatus(secondaryProviderStatus); + mSecondaryProviderReportedStatus = secondaryProviderReportedStatus; + + boolean primaryProviderHasReported = hasProviderReported(primaryProviderStatus); + boolean primaryProviderReportedStatusPresent = primaryProviderReportedStatus != null; + if (!primaryProviderHasReported && primaryProviderReportedStatusPresent) { + throw new IllegalArgumentException( + "primaryProviderReportedStatus=" + primaryProviderReportedStatus + + ", primaryProviderStatus=" + + providerStatusToString(primaryProviderStatus)); + } + + boolean secondaryProviderHasReported = hasProviderReported(secondaryProviderStatus); + boolean secondaryProviderReportedStatusPresent = secondaryProviderReportedStatus != null; + if (!secondaryProviderHasReported && secondaryProviderReportedStatusPresent) { + throw new IllegalArgumentException( + "secondaryProviderReportedStatus=" + secondaryProviderReportedStatus + + ", secondaryProviderStatus=" + + providerStatusToString(secondaryProviderStatus)); + } + + // If the algorithm isn't running, providers can't report. + if (status != DETECTION_ALGORITHM_STATUS_RUNNING + && (primaryProviderHasReported || secondaryProviderHasReported)) { + throw new IllegalArgumentException( + "algorithmStatus=" + detectionAlgorithmStatusToString(status) + + ", primaryProviderReportedStatus=" + primaryProviderReportedStatus + + ", secondaryProviderReportedStatus=" + + secondaryProviderReportedStatus); + } + } + + /** + * Returns the status value of the detection algorithm. + */ + public @DetectionAlgorithmStatus int getStatus() { + return mStatus; + } + + /** + * Returns the status of the primary location time zone provider as categorized by the detection + * algorithm. + */ + public @ProviderStatus int getPrimaryProviderStatus() { + return mPrimaryProviderStatus; + } + + /** + * Returns the status of the primary location time zone provider as reported by the provider + * itself. Can be {@code null} when the provider hasn't reported, or omitted when it has. + */ + @Nullable + public TimeZoneProviderStatus getPrimaryProviderReportedStatus() { + return mPrimaryProviderReportedStatus; + } + + /** + * Returns the status of the secondary location time zone provider as categorized by the + * detection algorithm. + */ + public @ProviderStatus int getSecondaryProviderStatus() { + return mSecondaryProviderStatus; + } + + /** + * Returns the status of the secondary location time zone provider as reported by the provider + * itself. Can be {@code null} when the provider hasn't reported, or omitted when it has. + */ + @Nullable + public TimeZoneProviderStatus getSecondaryProviderReportedStatus() { + return mSecondaryProviderReportedStatus; + } + + @Override + public String toString() { + return "LocationTimeZoneAlgorithmStatus{" + + "mAlgorithmStatus=" + detectionAlgorithmStatusToString(mStatus) + + ", mPrimaryProviderStatus=" + providerStatusToString(mPrimaryProviderStatus) + + ", mPrimaryProviderReportedStatus=" + mPrimaryProviderReportedStatus + + ", mSecondaryProviderStatus=" + providerStatusToString(mSecondaryProviderStatus) + + ", mSecondaryProviderReportedStatus=" + mSecondaryProviderReportedStatus + + '}'; + } + + /** + * Parses a {@link LocationTimeZoneAlgorithmStatus} from a toString() string for manual + * command-line testing. + */ + @NonNull + public static LocationTimeZoneAlgorithmStatus parseCommandlineArg(@NonNull String arg) { + // Note: "}" has to be escaped on Android with "\\}" because the regexp library is not based + // on OpenJDK code. + Pattern pattern = Pattern.compile("LocationTimeZoneAlgorithmStatus\\{" + + "mAlgorithmStatus=(.+)" + + ", mPrimaryProviderStatus=([^,]+)" + + ", mPrimaryProviderReportedStatus=(null|TimeZoneProviderStatus\\{[^}]+\\})" + + ", mSecondaryProviderStatus=([^,]+)" + + ", mSecondaryProviderReportedStatus=(null|TimeZoneProviderStatus\\{[^}]+\\})" + + "\\}" + ); + Matcher matcher = pattern.matcher(arg); + if (!matcher.matches()) { + throw new IllegalArgumentException("Unable to parse algorithm status arg: " + arg); + } + @DetectionAlgorithmStatus int algorithmStatus = + detectionAlgorithmStatusFromString(matcher.group(1)); + @ProviderStatus int primaryProviderStatus = providerStatusFromString(matcher.group(2)); + TimeZoneProviderStatus primaryProviderReportedStatus = + parseTimeZoneProviderStatusOrNull(matcher.group(3)); + @ProviderStatus int secondaryProviderStatus = providerStatusFromString(matcher.group(4)); + TimeZoneProviderStatus secondaryProviderReportedStatus = + parseTimeZoneProviderStatusOrNull(matcher.group(5)); + return new LocationTimeZoneAlgorithmStatus( + algorithmStatus, primaryProviderStatus, primaryProviderReportedStatus, + secondaryProviderStatus, secondaryProviderReportedStatus); + } + + @Nullable + private static TimeZoneProviderStatus parseTimeZoneProviderStatusOrNull( + String providerReportedStatusString) { + TimeZoneProviderStatus providerReportedStatus; + if ("null".equals(providerReportedStatusString)) { + providerReportedStatus = null; + } else { + providerReportedStatus = + TimeZoneProviderStatus.parseProviderStatus(providerReportedStatusString); + } + return providerReportedStatus; + } + + @NonNull + public static final Creator<LocationTimeZoneAlgorithmStatus> CREATOR = new Creator<>() { + @Override + public LocationTimeZoneAlgorithmStatus createFromParcel(Parcel in) { + @DetectionAlgorithmStatus int algorithmStatus = in.readInt(); + @ProviderStatus int primaryProviderStatus = in.readInt(); + TimeZoneProviderStatus primaryProviderReportedStatus = + in.readParcelable(getClass().getClassLoader(), TimeZoneProviderStatus.class); + @ProviderStatus int secondaryProviderStatus = in.readInt(); + TimeZoneProviderStatus secondaryProviderReportedStatus = + in.readParcelable(getClass().getClassLoader(), TimeZoneProviderStatus.class); + return new LocationTimeZoneAlgorithmStatus( + algorithmStatus, primaryProviderStatus, primaryProviderReportedStatus, + secondaryProviderStatus, secondaryProviderReportedStatus); + } + + @Override + public LocationTimeZoneAlgorithmStatus[] newArray(int size) { + return new LocationTimeZoneAlgorithmStatus[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeInt(mStatus); + parcel.writeInt(mPrimaryProviderStatus); + parcel.writeParcelable(mPrimaryProviderReportedStatus, flags); + parcel.writeInt(mSecondaryProviderStatus); + parcel.writeParcelable(mSecondaryProviderReportedStatus, flags); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LocationTimeZoneAlgorithmStatus that = (LocationTimeZoneAlgorithmStatus) o; + return mStatus == that.mStatus + && mPrimaryProviderStatus == that.mPrimaryProviderStatus + && Objects.equals( + mPrimaryProviderReportedStatus, that.mPrimaryProviderReportedStatus) + && mSecondaryProviderStatus == that.mSecondaryProviderStatus + && Objects.equals( + mSecondaryProviderReportedStatus, that.mSecondaryProviderReportedStatus); + } + + @Override + public int hashCode() { + return Objects.hash(mStatus, + mPrimaryProviderStatus, mPrimaryProviderReportedStatus, + mSecondaryProviderStatus, mSecondaryProviderReportedStatus); + } + + /** @hide */ + @VisibleForTesting + @NonNull + public static String providerStatusToString(@ProviderStatus int providerStatus) { + switch (providerStatus) { + case PROVIDER_STATUS_NOT_PRESENT: + return "NOT_PRESENT"; + case PROVIDER_STATUS_NOT_READY: + return "NOT_READY"; + case PROVIDER_STATUS_IS_CERTAIN: + return "IS_CERTAIN"; + case PROVIDER_STATUS_IS_UNCERTAIN: + return "IS_UNCERTAIN"; + default: + throw new IllegalArgumentException("Unknown status: " + providerStatus); + } + } + + /** @hide */ + @VisibleForTesting public static @ProviderStatus int providerStatusFromString( + @Nullable String providerStatusString) { + if (TextUtils.isEmpty(providerStatusString)) { + throw new IllegalArgumentException("Empty status: " + providerStatusString); + } + + switch (providerStatusString) { + case "NOT_PRESENT": + return PROVIDER_STATUS_NOT_PRESENT; + case "NOT_READY": + return PROVIDER_STATUS_NOT_READY; + case "IS_CERTAIN": + return PROVIDER_STATUS_IS_CERTAIN; + case "IS_UNCERTAIN": + return PROVIDER_STATUS_IS_UNCERTAIN; + default: + throw new IllegalArgumentException("Unknown status: " + providerStatusString); + } + } + + private static boolean hasProviderReported(@ProviderStatus int providerStatus) { + return providerStatus == PROVIDER_STATUS_IS_CERTAIN + || providerStatus == PROVIDER_STATUS_IS_UNCERTAIN; + } + + /** @hide */ + @VisibleForTesting public static @ProviderStatus int requireValidProviderStatus( + @ProviderStatus int providerStatus) { + if (providerStatus < PROVIDER_STATUS_NOT_PRESENT + || providerStatus > PROVIDER_STATUS_IS_UNCERTAIN) { + throw new IllegalArgumentException( + "Invalid provider status: " + providerStatus); + } + return providerStatus; + } +} diff --git a/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.aidl b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.aidl new file mode 100644 index 000000000000..0eb5b63b7ffb --- /dev/null +++ b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.app.time; + +parcelable TelephonyTimeZoneAlgorithmStatus; diff --git a/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.java b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.java new file mode 100644 index 000000000000..95240c00fa3f --- /dev/null +++ b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.java @@ -0,0 +1,96 @@ +/* + * 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusToString; +import static android.app.time.DetectorStatusTypes.requireValidDetectionAlgorithmStatus; + +import android.annotation.NonNull; +import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Information about the status of the telephony-based time zone detection algorithm. + * + * @hide + */ +public final class TelephonyTimeZoneAlgorithmStatus implements Parcelable { + + private final @DetectionAlgorithmStatus int mAlgorithmStatus; + + public TelephonyTimeZoneAlgorithmStatus(@DetectionAlgorithmStatus int algorithmStatus) { + mAlgorithmStatus = requireValidDetectionAlgorithmStatus(algorithmStatus); + } + + /** + * Returns the status of the detection algorithm. + */ + public @DetectionAlgorithmStatus int getAlgorithmStatus() { + return mAlgorithmStatus; + } + + @Override + public String toString() { + return "TelephonyTimeZoneAlgorithmStatus{" + + "mAlgorithmStatus=" + detectionAlgorithmStatusToString(mAlgorithmStatus) + + '}'; + } + + @NonNull + public static final Creator<TelephonyTimeZoneAlgorithmStatus> CREATOR = new Creator<>() { + @Override + public TelephonyTimeZoneAlgorithmStatus createFromParcel(Parcel in) { + @DetectionAlgorithmStatus int algorithmStatus = in.readInt(); + return new TelephonyTimeZoneAlgorithmStatus(algorithmStatus); + } + + @Override + public TelephonyTimeZoneAlgorithmStatus[] newArray(int size) { + return new TelephonyTimeZoneAlgorithmStatus[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeInt(mAlgorithmStatus); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TelephonyTimeZoneAlgorithmStatus that = (TelephonyTimeZoneAlgorithmStatus) o; + return mAlgorithmStatus == that.mAlgorithmStatus; + } + + @Override + public int hashCode() { + return Objects.hash(mAlgorithmStatus); + } +} diff --git a/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java b/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java index cd91b0431b28..4684c6ad811c 100644 --- a/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java +++ b/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java @@ -23,27 +23,40 @@ import android.os.Parcel; import android.os.Parcelable; import java.util.Objects; +import java.util.concurrent.Executor; /** - * A pair containing a user's {@link TimeZoneCapabilities} and {@link TimeZoneConfiguration}. + * An object containing a user's {@link TimeZoneCapabilities} and {@link TimeZoneConfiguration}. * * @hide */ @SystemApi public final class TimeZoneCapabilitiesAndConfig implements Parcelable { - public static final @NonNull Creator<TimeZoneCapabilitiesAndConfig> CREATOR = - new Creator<TimeZoneCapabilitiesAndConfig>() { - public TimeZoneCapabilitiesAndConfig createFromParcel(Parcel in) { - return TimeZoneCapabilitiesAndConfig.createFromParcel(in); - } - - public TimeZoneCapabilitiesAndConfig[] newArray(int size) { - return new TimeZoneCapabilitiesAndConfig[size]; - } - }; + public static final @NonNull Creator<TimeZoneCapabilitiesAndConfig> CREATOR = new Creator<>() { + public TimeZoneCapabilitiesAndConfig createFromParcel(Parcel in) { + return TimeZoneCapabilitiesAndConfig.createFromParcel(in); + } + public TimeZoneCapabilitiesAndConfig[] newArray(int size) { + return new TimeZoneCapabilitiesAndConfig[size]; + } + }; + /** + * The time zone detector status. + * + * Implementation note for future platform engineers: This field is only needed by SettingsUI + * initially and so it has not been added to the SDK API. {@link TimeZoneDetectorStatus} + * contains details about the internals of the time zone detector so thought should be given to + * abstraction / exposing a lightweight version if something unbundled needs access to detector + * details. Also, that could be good time to add separate APIs for bundled components, or add + * new APIs that return something more extensible and generic like a Bundle or a less + * constraining name. See also {@link + * TimeManager#addTimeZoneDetectorListener(Executor, TimeManager.TimeZoneDetectorListener)}, + * which notified of changes to any fields in this class, including the detector status. + */ + @NonNull private final TimeZoneDetectorStatus mDetectorStatus; @NonNull private final TimeZoneCapabilities mCapabilities; @NonNull private final TimeZoneConfiguration mConfiguration; @@ -53,26 +66,41 @@ public final class TimeZoneCapabilitiesAndConfig implements Parcelable { * @hide */ public TimeZoneCapabilitiesAndConfig( + @NonNull TimeZoneDetectorStatus detectorStatus, @NonNull TimeZoneCapabilities capabilities, @NonNull TimeZoneConfiguration configuration) { - this.mCapabilities = Objects.requireNonNull(capabilities); - this.mConfiguration = Objects.requireNonNull(configuration); + mDetectorStatus = Objects.requireNonNull(detectorStatus); + mCapabilities = Objects.requireNonNull(capabilities); + mConfiguration = Objects.requireNonNull(configuration); } @NonNull private static TimeZoneCapabilitiesAndConfig createFromParcel(Parcel in) { - TimeZoneCapabilities capabilities = in.readParcelable(null, android.app.time.TimeZoneCapabilities.class); - TimeZoneConfiguration configuration = in.readParcelable(null, android.app.time.TimeZoneConfiguration.class); - return new TimeZoneCapabilitiesAndConfig(capabilities, configuration); + TimeZoneDetectorStatus detectorStatus = + in.readParcelable(null, TimeZoneDetectorStatus.class); + TimeZoneCapabilities capabilities = in.readParcelable(null, TimeZoneCapabilities.class); + TimeZoneConfiguration configuration = in.readParcelable(null, TimeZoneConfiguration.class); + return new TimeZoneCapabilitiesAndConfig(detectorStatus, capabilities, configuration); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mDetectorStatus, flags); dest.writeParcelable(mCapabilities, flags); dest.writeParcelable(mConfiguration, flags); } /** + * Returns the time zone detector's status. + * + * @hide + */ + @NonNull + public TimeZoneDetectorStatus getDetectorStatus() { + return mDetectorStatus; + } + + /** * Returns the user's time zone behavior capabilities. */ @NonNull @@ -102,7 +130,8 @@ public final class TimeZoneCapabilitiesAndConfig implements Parcelable { return false; } TimeZoneCapabilitiesAndConfig that = (TimeZoneCapabilitiesAndConfig) o; - return mCapabilities.equals(that.mCapabilities) + return mDetectorStatus.equals(that.mDetectorStatus) + && mCapabilities.equals(that.mCapabilities) && mConfiguration.equals(that.mConfiguration); } @@ -114,7 +143,8 @@ public final class TimeZoneCapabilitiesAndConfig implements Parcelable { @Override public String toString() { return "TimeZoneCapabilitiesAndConfig{" - + "mCapabilities=" + mCapabilities + + "mDetectorStatus=" + mDetectorStatus + + ", mCapabilities=" + mCapabilities + ", mConfiguration=" + mConfiguration + '}'; } diff --git a/core/java/android/app/time/TimeZoneDetectorStatus.aidl b/core/java/android/app/time/TimeZoneDetectorStatus.aidl new file mode 100644 index 000000000000..32204df6d698 --- /dev/null +++ b/core/java/android/app/time/TimeZoneDetectorStatus.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.app.time; + +parcelable TimeZoneDetectorStatus; diff --git a/core/java/android/app/time/TimeZoneDetectorStatus.java b/core/java/android/app/time/TimeZoneDetectorStatus.java new file mode 100644 index 000000000000..16374639b77c --- /dev/null +++ b/core/java/android/app/time/TimeZoneDetectorStatus.java @@ -0,0 +1,124 @@ +/* + * 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DetectorStatus; +import static android.app.time.DetectorStatusTypes.requireValidDetectorStatus; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Information about the status of the automatic time zone detector. Used by SettingsUI to display + * status information to the user. + * + * @hide + */ +public final class TimeZoneDetectorStatus implements Parcelable { + + private final @DetectorStatus int mDetectorStatus; + @NonNull private final TelephonyTimeZoneAlgorithmStatus mTelephonyTimeZoneAlgorithmStatus; + @NonNull private final LocationTimeZoneAlgorithmStatus mLocationTimeZoneAlgorithmStatus; + + public TimeZoneDetectorStatus( + @DetectorStatus int detectorStatus, + @NonNull TelephonyTimeZoneAlgorithmStatus telephonyTimeZoneAlgorithmStatus, + @NonNull LocationTimeZoneAlgorithmStatus locationTimeZoneAlgorithmStatus) { + mDetectorStatus = requireValidDetectorStatus(detectorStatus); + mTelephonyTimeZoneAlgorithmStatus = + Objects.requireNonNull(telephonyTimeZoneAlgorithmStatus); + mLocationTimeZoneAlgorithmStatus = Objects.requireNonNull(locationTimeZoneAlgorithmStatus); + } + + public @DetectorStatus int getDetectorStatus() { + return mDetectorStatus; + } + + @NonNull + public TelephonyTimeZoneAlgorithmStatus getTelephonyTimeZoneAlgorithmStatus() { + return mTelephonyTimeZoneAlgorithmStatus; + } + + @NonNull + public LocationTimeZoneAlgorithmStatus getLocationTimeZoneAlgorithmStatus() { + return mLocationTimeZoneAlgorithmStatus; + } + + @Override + public String toString() { + return "TimeZoneDetectorStatus{" + + "mDetectorStatus=" + DetectorStatusTypes.detectorStatusToString(mDetectorStatus) + + ", mTelephonyTimeZoneAlgorithmStatus=" + mTelephonyTimeZoneAlgorithmStatus + + ", mLocationTimeZoneAlgorithmStatus=" + mLocationTimeZoneAlgorithmStatus + + '}'; + } + + public static final @NonNull Creator<TimeZoneDetectorStatus> CREATOR = new Creator<>() { + @Override + public TimeZoneDetectorStatus createFromParcel(Parcel in) { + @DetectorStatus int detectorStatus = in.readInt(); + TelephonyTimeZoneAlgorithmStatus telephonyTimeZoneAlgorithmStatus = + in.readParcelable(getClass().getClassLoader(), + TelephonyTimeZoneAlgorithmStatus.class); + LocationTimeZoneAlgorithmStatus locationTimeZoneAlgorithmStatus = + in.readParcelable(getClass().getClassLoader(), + LocationTimeZoneAlgorithmStatus.class); + return new TimeZoneDetectorStatus(detectorStatus, + telephonyTimeZoneAlgorithmStatus, locationTimeZoneAlgorithmStatus); + } + + @Override + public TimeZoneDetectorStatus[] newArray(int size) { + return new TimeZoneDetectorStatus[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeInt(mDetectorStatus); + parcel.writeParcelable(mTelephonyTimeZoneAlgorithmStatus, flags); + parcel.writeParcelable(mLocationTimeZoneAlgorithmStatus, flags); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TimeZoneDetectorStatus that = (TimeZoneDetectorStatus) o; + return mDetectorStatus == that.mDetectorStatus + && mTelephonyTimeZoneAlgorithmStatus.equals(that.mTelephonyTimeZoneAlgorithmStatus) + && mLocationTimeZoneAlgorithmStatus.equals(that.mLocationTimeZoneAlgorithmStatus); + } + + @Override + public int hashCode() { + return Objects.hash(mDetectorStatus, mTelephonyTimeZoneAlgorithmStatus, + mLocationTimeZoneAlgorithmStatus); + } +} diff --git a/core/java/android/app/timezonedetector/TimeZoneDetector.java b/core/java/android/app/timezonedetector/TimeZoneDetector.java index 0e9e28be8818..f357fb243fe1 100644 --- a/core/java/android/app/timezonedetector/TimeZoneDetector.java +++ b/core/java/android/app/timezonedetector/TimeZoneDetector.java @@ -81,11 +81,11 @@ public interface TimeZoneDetector { String SHELL_COMMAND_SET_GEO_DETECTION_ENABLED = "set_geo_detection_enabled"; /** - * A shell command that injects a geolocation time zone suggestion (as if from the + * A shell command that injects a location algorithm event (as if from the * location_time_zone_manager). * @hide */ - String SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE = "suggest_geo_location_time_zone"; + String SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT = "handle_location_algorithm_event"; /** * A shell command that injects a manual time zone suggestion (as if from the SettingsUI or diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl index 82d7534c84d9..7d6336a225bd 100644 --- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl +++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl @@ -52,6 +52,11 @@ interface IVirtualDeviceManager { List<VirtualDevice> getVirtualDevices(); /** + * Returns the device policy for the given virtual device and policy type. + */ + int getDevicePolicy(int deviceId, int policyType); + + /** * Creates a virtual display owned by a particular virtual device. * * @param virtualDisplayConfig The configuration used in creating the display diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 0bb86fbf00f8..c14bb1beb025 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -182,6 +182,28 @@ public final class VirtualDeviceManager { } /** + * Returns the device policy for the given virtual device and policy type. + * + * <p>In case the virtual device identifier is not valid, or there's no explicitly specified + * policy for that device and policy type, then + * {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT} is returned. + * + * @hide + */ + public @VirtualDeviceParams.DevicePolicy int getDevicePolicy( + int deviceId, @VirtualDeviceParams.PolicyType int policyType) { + if (mService == null) { + Log.w(TAG, "Failed to retrieve device policy; no virtual device manager service."); + return VirtualDeviceParams.DEVICE_POLICY_DEFAULT; + } + try { + return mService.getDevicePolicy(deviceId, policyType); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * A virtual device has its own virtual display, audio output, microphone, and camera etc. The * creator of a virtual device can take the output from the virtual display and stream it over * to another device, and inject input events that are received from the remote device. diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java index d40c9d63039d..c6e6f8324cff 100644 --- a/core/java/android/companion/virtual/VirtualDeviceParams.java +++ b/core/java/android/companion/virtual/VirtualDeviceParams.java @@ -28,6 +28,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; import android.util.ArraySet; +import android.util.SparseIntArray; import com.android.internal.util.Preconditions; @@ -103,6 +104,47 @@ public final class VirtualDeviceParams implements Parcelable { */ public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; + /** @hide */ + @IntDef(prefix = "DEVICE_POLICY_", value = {DEVICE_POLICY_DEFAULT, DEVICE_POLICY_CUSTOM}) + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + public @interface DevicePolicy {} + + /** + * Indicates that there is no special logic for this virtual device and it should be treated + * the same way as the default device, keeping the default behavior unchanged. + */ + public static final int DEVICE_POLICY_DEFAULT = 0; + + /** + * Indicates that there is custom logic, specific to this virtual device, which should be + * triggered instead of the default behavior. + */ + public static final int DEVICE_POLICY_CUSTOM = 1; + + /** + * Any relevant component must be able to interpret the correct meaning of a custom policy for + * a given policy type. + * @hide + */ + @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS}) + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + public @interface PolicyType {} + + /** + * Tells the sensor framework how to handle sensor requests from contexts associated with this + * virtual device, namely the sensors returned by + * {@link android.hardware.SensorManager#getSensorList}: + * + * <ul> + * <li>{@link #DEVICE_POLICY_DEFAULT}: Return the sensors of the default device. + * <li>{@link #DEVICE_POLICY_CUSTOM}: Return the sensors of the virtual device. Note that if + * the virtual device did not create any virtual sensors, then an empty list is returned. + * </ul> + */ + public static final int POLICY_TYPE_SENSORS = 0; + private final int mLockState; @NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts; @NonNull private final ArraySet<ComponentName> mAllowedCrossTaskNavigations; @@ -114,6 +156,8 @@ public final class VirtualDeviceParams implements Parcelable { @ActivityPolicy private final int mDefaultActivityPolicy; @Nullable private final String mName; + // Mapping of @PolicyType to @DevicePolicy + @NonNull private final SparseIntArray mDevicePolicies; private VirtualDeviceParams( @LockState int lockState, @@ -124,12 +168,14 @@ public final class VirtualDeviceParams implements Parcelable { @NonNull Set<ComponentName> allowedActivities, @NonNull Set<ComponentName> blockedActivities, @ActivityPolicy int defaultActivityPolicy, - @Nullable String name) { + @Nullable String name, + @NonNull SparseIntArray devicePolicies) { Preconditions.checkNotNull(usersWithMatchingAccounts); Preconditions.checkNotNull(allowedCrossTaskNavigations); Preconditions.checkNotNull(blockedCrossTaskNavigations); Preconditions.checkNotNull(allowedActivities); Preconditions.checkNotNull(blockedActivities); + Preconditions.checkNotNull(devicePolicies); mLockState = lockState; mUsersWithMatchingAccounts = new ArraySet<>(usersWithMatchingAccounts); @@ -140,6 +186,7 @@ public final class VirtualDeviceParams implements Parcelable { mBlockedActivities = new ArraySet<>(blockedActivities); mDefaultActivityPolicy = defaultActivityPolicy; mName = name; + mDevicePolicies = devicePolicies; } @SuppressWarnings("unchecked") @@ -153,6 +200,7 @@ public final class VirtualDeviceParams implements Parcelable { mBlockedActivities = (ArraySet<ComponentName>) parcel.readArraySet(null); mDefaultActivityPolicy = parcel.readInt(); mName = parcel.readString8(); + mDevicePolicies = parcel.readSparseIntArray(); } /** @@ -258,6 +306,16 @@ public final class VirtualDeviceParams implements Parcelable { return mName; } + /** + * Returns the policy specified for this policy type, or {@link #DEVICE_POLICY_DEFAULT} if no + * policy for this type has been explicitly specified. + * + * @see Builder#addDevicePolicy + */ + public @DevicePolicy int getDevicePolicy(@PolicyType int policyType) { + return mDevicePolicies.get(policyType, DEVICE_POLICY_DEFAULT); + } + @Override public int describeContents() { return 0; @@ -274,6 +332,7 @@ public final class VirtualDeviceParams implements Parcelable { dest.writeArraySet(mBlockedActivities); dest.writeInt(mDefaultActivityPolicy); dest.writeString8(mName); + dest.writeSparseIntArray(mDevicePolicies); } @Override @@ -285,6 +344,18 @@ public final class VirtualDeviceParams implements Parcelable { return false; } VirtualDeviceParams that = (VirtualDeviceParams) o; + final int devicePoliciesCount = mDevicePolicies.size(); + if (devicePoliciesCount != that.mDevicePolicies.size()) { + return false; + } + for (int i = 0; i < devicePoliciesCount; i++) { + if (mDevicePolicies.keyAt(i) != that.mDevicePolicies.keyAt(i)) { + return false; + } + if (mDevicePolicies.valueAt(i) != that.mDevicePolicies.valueAt(i)) { + return false; + } + } return mLockState == that.mLockState && mUsersWithMatchingAccounts.equals(that.mUsersWithMatchingAccounts) && Objects.equals(mAllowedCrossTaskNavigations, that.mAllowedCrossTaskNavigations) @@ -298,10 +369,15 @@ public final class VirtualDeviceParams implements Parcelable { @Override public int hashCode() { - return Objects.hash( + int hashCode = Objects.hash( mLockState, mUsersWithMatchingAccounts, mAllowedCrossTaskNavigations, mBlockedCrossTaskNavigations, mDefaultNavigationPolicy, mAllowedActivities, - mBlockedActivities, mDefaultActivityPolicy, mName); + mBlockedActivities, mDefaultActivityPolicy, mName, mDevicePolicies); + for (int i = 0; i < mDevicePolicies.size(); i++) { + hashCode = 31 * hashCode + mDevicePolicies.keyAt(i); + hashCode = 31 * hashCode + mDevicePolicies.valueAt(i); + } + return hashCode; } @Override @@ -317,6 +393,7 @@ public final class VirtualDeviceParams implements Parcelable { + " mBlockedActivities=" + mBlockedActivities + " mDefaultActivityPolicy=" + mDefaultActivityPolicy + " mName=" + mName + + " mDevicePolicies=" + mDevicePolicies + ")"; } @@ -350,6 +427,7 @@ public final class VirtualDeviceParams implements Parcelable { private int mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_ALLOWED; private boolean mDefaultActivityPolicyConfigured = false; @Nullable private String mName; + @NonNull private SparseIntArray mDevicePolicies = new SparseIntArray(); /** * Sets the lock state of the device. The permission {@code ADD_ALWAYS_UNLOCKED_DISPLAY} @@ -528,6 +606,18 @@ public final class VirtualDeviceParams implements Parcelable { } /** + * Specifies a policy for this virtual device. + * + * @param policyType the type of policy, i.e. which behavior to specify a policy for. + * @param devicePolicy the value of the policy, i.e. how to interpret the device behavior. + */ + @NonNull + public Builder addDevicePolicy(@PolicyType int policyType, @DevicePolicy int devicePolicy) { + mDevicePolicies.put(policyType, devicePolicy); + return this; + } + + /** * Builds the {@link VirtualDeviceParams} instance. */ @NonNull @@ -541,7 +631,8 @@ public final class VirtualDeviceParams implements Parcelable { mAllowedActivities, mBlockedActivities, mDefaultActivityPolicy, - mName); + mName, + mDevicePolicies); } } } diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java index dbefa65f5c68..cc7977a267a5 100644 --- a/core/java/android/content/om/FabricatedOverlay.java +++ b/core/java/android/content/om/FabricatedOverlay.java @@ -26,6 +26,7 @@ import android.text.TextUtils; import com.android.internal.util.Preconditions; import java.util.ArrayList; +import java.util.Objects; /** * Fabricated Runtime Resource Overlays (FRROs) are overlays generated ar runtime. @@ -89,6 +90,27 @@ public class FabricatedOverlay { } /** + * Ensure the resource name is in the form [package]:type/entry. + * + * @param name name of the target resource to overlay (in the form [package]:type/entry) + * @return the valid name + */ + private static String ensureValidResourceName(@NonNull String name) { + Objects.requireNonNull(name); + final int slashIndex = name.indexOf('/'); /* must contain '/' */ + final int colonIndex = name.indexOf(':'); /* ':' should before '/' if ':' exist */ + + // The minimum length of resource type is "id". + Preconditions.checkArgument( + slashIndex >= 0 /* It must contain the type name */ + && colonIndex != 0 /* 0 means the package name is empty */ + && (slashIndex - colonIndex) > 2 /* The shortest length of type is "id" */, + "\"%s\" is invalid resource name", + name); + return name; + } + + /** * Sets the value of the fabricated overlay * * @param resourceName name of the target resource to overlay (in the form @@ -99,6 +121,8 @@ public class FabricatedOverlay { * @see android.util.TypedValue#type */ public Builder setResourceValue(@NonNull String resourceName, int dataType, int value) { + ensureValidResourceName(resourceName); + final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry(); entry.resourceName = resourceName; entry.dataType = dataType; @@ -120,6 +144,8 @@ public class FabricatedOverlay { */ public Builder setResourceValue(@NonNull String resourceName, int dataType, int value, String configuration) { + ensureValidResourceName(resourceName); + final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry(); entry.resourceName = resourceName; entry.dataType = dataType; @@ -140,6 +166,8 @@ public class FabricatedOverlay { * @see android.util.TypedValue#type */ public Builder setResourceValue(@NonNull String resourceName, int dataType, String value) { + ensureValidResourceName(resourceName); + final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry(); entry.resourceName = resourceName; entry.dataType = dataType; @@ -161,6 +189,8 @@ public class FabricatedOverlay { */ public Builder setResourceValue(@NonNull String resourceName, int dataType, String value, String configuration) { + ensureValidResourceName(resourceName); + final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry(); entry.resourceName = resourceName; entry.dataType = dataType; @@ -180,6 +210,8 @@ public class FabricatedOverlay { */ public Builder setResourceValue(@NonNull String resourceName, ParcelFileDescriptor value, String configuration) { + ensureValidResourceName(resourceName); + final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry(); entry.resourceName = resourceName; entry.binaryData = value; diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 6c1d84be6c9f..485d04db82d5 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -2959,6 +2959,18 @@ public abstract class PackageManager { public static final String FEATURE_OPENGLES_EXTENSION_PACK = "android.hardware.opengles.aep"; /** + * Feature for {@link #getSystemAvailableFeatures()} and {@link #hasSystemFeature(String)}. + * This feature indicates whether device supports + * <a href="https://source.android.com/docs/core/virtualization">Android Virtualization Framework</a>. + * + * @hide + */ + @SystemApi + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_VIRTUALIZATION_FRAMEWORK = + "android.software.virtualization_framework"; + + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature(String, int)}: If this feature is supported, the Vulkan * implementation on this device is hardware accelerated, and the Vulkan native API will diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java index 6fa331bbdc54..5c07e6e94131 100644 --- a/core/java/android/credentials/ui/Entry.java +++ b/core/java/android/credentials/ui/Entry.java @@ -17,7 +17,10 @@ package android.credentials.ui; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PendingIntent; import android.app.slice.Slice; +import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; @@ -85,6 +88,8 @@ public class Entry implements Parcelable { @NonNull private final String mKey; @NonNull private final String mSubkey; + @Nullable private PendingIntent mPendingIntent; + @Nullable private Intent mFrameworkExtrasIntent; @NonNull private final Slice mSlice; @@ -100,14 +105,29 @@ public class Entry implements Parcelable { AnnotationValidations.validate(NonNull.class, null, mSubkey); mSlice = slice; AnnotationValidations.validate(NonNull.class, null, mSlice); + mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); + mFrameworkExtrasIntent = in.readTypedObject(Intent.CREATOR); } + /** Constructor to be used for an entry that does not require further activities + * to be invoked when selected. + */ public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice) { mKey = key; mSubkey = subkey; mSlice = slice; } + /** Constructor to be used for an entry that requires a pending intent to be invoked + * when clicked. + */ + public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice, + @NonNull PendingIntent pendingIntent, @Nullable Intent intent) { + this(key, subkey, slice); + mPendingIntent = pendingIntent; + mFrameworkExtrasIntent = intent; + } + /** * Returns the identifier of this entry that's unique within the context of the CredentialManager * request. @@ -133,11 +153,23 @@ public class Entry implements Parcelable { return mSlice; } + @Nullable + public PendingIntent getPendingIntent() { + return mPendingIntent; + } + + @Nullable + public Intent getFrameworkExtrasIntent() { + return mFrameworkExtrasIntent; + } + @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mKey); dest.writeString8(mSubkey); mSlice.writeToParcel(dest, flags); + mPendingIntent.writeToParcel(dest, flags); + mFrameworkExtrasIntent.writeToParcel(dest, flags); } @Override diff --git a/core/java/android/credentials/ui/ProviderPendingIntentResponse.java b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java new file mode 100644 index 000000000000..420956f69b7f --- /dev/null +++ b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java @@ -0,0 +1,79 @@ +/* + * 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 android.credentials.ui; + +import android.annotation.Nullable; +import android.content.Intent; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +/** + * Response from a provider's pending intent + * + * @hide + */ +public final class ProviderPendingIntentResponse implements Parcelable { + private final int mResultCode; + @Nullable + private final Intent mResultData; + + public ProviderPendingIntentResponse(int resultCode, @Nullable Intent resultData) { + mResultCode = resultCode; + mResultData = resultData; + } + + protected ProviderPendingIntentResponse(Parcel in) { + mResultCode = in.readInt(); + mResultData = in.readTypedObject(Intent.CREATOR); + } + + public static final Creator<ProviderPendingIntentResponse> CREATOR = + new Creator<ProviderPendingIntentResponse>() { + @Override + public ProviderPendingIntentResponse createFromParcel(Parcel in) { + return new ProviderPendingIntentResponse(in); + } + + @Override + public ProviderPendingIntentResponse[] newArray(int size) { + return new ProviderPendingIntentResponse[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeSerializable(mResultCode); + dest.writeTypedObject(mResultData, flags); + } + + /** Returns the result code associated with this pending intent activity result. */ + public int getResultCode() { + return mResultCode; + } + + /** Returns the result data associated with this pending intent activity result. */ + @NonNull public Intent getResultData() { + return mResultData; + } +} diff --git a/core/java/android/credentials/ui/UserSelectionDialogResult.java b/core/java/android/credentials/ui/UserSelectionDialogResult.java index 6025d78d1bae..0e8e7b6f06a5 100644 --- a/core/java/android/credentials/ui/UserSelectionDialogResult.java +++ b/core/java/android/credentials/ui/UserSelectionDialogResult.java @@ -57,6 +57,7 @@ public class UserSelectionDialogResult extends BaseDialogResult implements Parce @NonNull private final String mProviderId; @NonNull private final String mEntryKey; @NonNull private final String mEntrySubkey; + @Nullable private ProviderPendingIntentResponse mProviderPendingIntentResponse; public UserSelectionDialogResult( @NonNull IBinder requestToken, @NonNull String providerId, @@ -67,6 +68,17 @@ public class UserSelectionDialogResult extends BaseDialogResult implements Parce mEntrySubkey = entrySubkey; } + public UserSelectionDialogResult( + @NonNull IBinder requestToken, @NonNull String providerId, + @NonNull String entryKey, @NonNull String entrySubkey, + @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) { + super(requestToken); + mProviderId = providerId; + mEntryKey = entryKey; + mEntrySubkey = entrySubkey; + mProviderPendingIntentResponse = providerPendingIntentResponse; + } + /** Returns provider package name whose entry was selected by the user. */ @NonNull public String getProviderId() { @@ -85,6 +97,12 @@ public class UserSelectionDialogResult extends BaseDialogResult implements Parce return mEntrySubkey; } + /** Returns the pending intent response from the provider. */ + @Nullable + public ProviderPendingIntentResponse getPendingIntentProviderResponse() { + return mProviderPendingIntentResponse; + } + protected UserSelectionDialogResult(@NonNull Parcel in) { super(in); String providerId = in.readString8(); @@ -97,6 +115,7 @@ public class UserSelectionDialogResult extends BaseDialogResult implements Parce AnnotationValidations.validate(NonNull.class, null, mEntryKey); mEntrySubkey = entrySubkey; AnnotationValidations.validate(NonNull.class, null, mEntrySubkey); + mProviderPendingIntentResponse = in.readTypedObject(ProviderPendingIntentResponse.CREATOR); } @Override @@ -105,6 +124,7 @@ public class UserSelectionDialogResult extends BaseDialogResult implements Parce dest.writeString8(mProviderId); dest.writeString8(mEntryKey); dest.writeString8(mEntrySubkey); + dest.writeTypedObject(mProviderPendingIntentResponse, flags); } @Override diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index d3cb59dbb034..f561278bb25a 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -1310,6 +1310,22 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri new Key<int[]>("android.control.availableSettingsOverrides", int[].class); /** + * <p>Whether the camera device supports {@link CaptureRequest#CONTROL_AUTOFRAMING android.control.autoframing}.</p> + * <p>Will be <code>false</code> if auto-framing is not available.</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CaptureRequest#CONTROL_AUTOFRAMING + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + */ + @PublicKey + @NonNull + public static final Key<Boolean> CONTROL_AUTOFRAMING_AVAILABLE = + new Key<Boolean>("android.control.autoframingAvailable", boolean.class); + + /** * <p>List of edge enhancement modes for {@link CaptureRequest#EDGE_MODE android.edge.mode} that are supported by this camera * device.</p> * <p>Full-capability camera devices must always support OFF; camera devices that support diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 545aa8f2e80d..44f8b1bfda07 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -3244,6 +3244,28 @@ public abstract class CameraMetadata<TKey> { public static final int CONTROL_SETTINGS_OVERRIDE_VENDOR_START = 0x4000; // + // Enumeration values for CaptureRequest#CONTROL_AUTOFRAMING + // + + /** + * <p>Disable autoframing.</p> + * @see CaptureRequest#CONTROL_AUTOFRAMING + */ + public static final int CONTROL_AUTOFRAMING_OFF = 0; + + /** + * <p>Enable autoframing to keep people in the frame's field of view.</p> + * @see CaptureRequest#CONTROL_AUTOFRAMING + */ + public static final int CONTROL_AUTOFRAMING_ON = 1; + + /** + * <p>Automatically select ON or OFF based on the system level preferences.</p> + * @see CaptureRequest#CONTROL_AUTOFRAMING + */ + public static final int CONTROL_AUTOFRAMING_AUTO = 2; + + // // Enumeration values for CaptureRequest#EDGE_MODE // @@ -3997,6 +4019,29 @@ public abstract class CameraMetadata<TKey> { public static final int CONTROL_AF_SCENE_CHANGE_DETECTED = 1; // + // Enumeration values for CaptureResult#CONTROL_AUTOFRAMING_STATE + // + + /** + * <p>Auto-framing is inactive.</p> + * @see CaptureResult#CONTROL_AUTOFRAMING_STATE + */ + public static final int CONTROL_AUTOFRAMING_STATE_INACTIVE = 0; + + /** + * <p>Auto-framing is in process - either zooming in, zooming out or pan is taking place.</p> + * @see CaptureResult#CONTROL_AUTOFRAMING_STATE + */ + public static final int CONTROL_AUTOFRAMING_STATE_FRAMING = 1; + + /** + * <p>Auto-framing has reached a stable state (frame/fov is not being adjusted). The state + * may transition back to FRAMING if the scene changes.</p> + * @see CaptureResult#CONTROL_AUTOFRAMING_STATE + */ + public static final int CONTROL_AUTOFRAMING_STATE_CONVERGED = 2; + + // // Enumeration values for CaptureResult#FLASH_STATE // diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 407ea07838de..8bb6fa5a1a14 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -2513,6 +2513,42 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> new Key<Integer>("android.control.settingsOverride", int.class); /** + * <p>Automatic crop, pan and zoom to keep objects in the center of the frame.</p> + * <p>Auto-framing is a special mode provided by the camera device to dynamically crop, zoom + * or pan the camera feed to try to ensure that the people in a scene occupy a reasonable + * portion of the viewport. It is primarily designed to support video calling in + * situations where the user isn't directly in front of the device, especially for + * wide-angle cameras. + * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} and {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in CaptureResult will be used + * to denote the coordinates of the auto-framed region. + * Zoom and video stabilization controls are disabled when auto-framing is enabled. The 3A + * regions must map the screen coordinates into the scaler crop returned from the capture + * result instead of using the active array sensor.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #CONTROL_AUTOFRAMING_OFF OFF}</li> + * <li>{@link #CONTROL_AUTOFRAMING_ON ON}</li> + * <li>{@link #CONTROL_AUTOFRAMING_AUTO AUTO}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SCALER_CROP_REGION + * @see #CONTROL_AUTOFRAMING_OFF + * @see #CONTROL_AUTOFRAMING_ON + * @see #CONTROL_AUTOFRAMING_AUTO + */ + @PublicKey + @NonNull + public static final Key<Integer> CONTROL_AUTOFRAMING = + new Key<Integer>("android.control.autoframing", int.class); + + /** * <p>Operation mode for edge * enhancement.</p> * <p>Edge enhancement improves sharpness and details in the captured image. OFF means diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index c4f0cabc5f78..c5246b52a6cf 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -2717,6 +2717,79 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { new Key<Integer>("android.control.settingsOverride", int.class); /** + * <p>Automatic crop, pan and zoom to keep objects in the center of the frame.</p> + * <p>Auto-framing is a special mode provided by the camera device to dynamically crop, zoom + * or pan the camera feed to try to ensure that the people in a scene occupy a reasonable + * portion of the viewport. It is primarily designed to support video calling in + * situations where the user isn't directly in front of the device, especially for + * wide-angle cameras. + * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} and {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in CaptureResult will be used + * to denote the coordinates of the auto-framed region. + * Zoom and video stabilization controls are disabled when auto-framing is enabled. The 3A + * regions must map the screen coordinates into the scaler crop returned from the capture + * result instead of using the active array sensor.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #CONTROL_AUTOFRAMING_OFF OFF}</li> + * <li>{@link #CONTROL_AUTOFRAMING_ON ON}</li> + * <li>{@link #CONTROL_AUTOFRAMING_AUTO AUTO}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CaptureRequest#CONTROL_ZOOM_RATIO + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SCALER_CROP_REGION + * @see #CONTROL_AUTOFRAMING_OFF + * @see #CONTROL_AUTOFRAMING_ON + * @see #CONTROL_AUTOFRAMING_AUTO + */ + @PublicKey + @NonNull + public static final Key<Integer> CONTROL_AUTOFRAMING = + new Key<Integer>("android.control.autoframing", int.class); + + /** + * <p>Current state of auto-framing.</p> + * <p>When the camera doesn't have auto-framing available (i.e + * <code>{@link CameraCharacteristics#CONTROL_AUTOFRAMING_AVAILABLE android.control.autoframingAvailable}</code> == false) or it is not enabled (i.e + * <code>{@link CaptureRequest#CONTROL_AUTOFRAMING android.control.autoframing}</code> == OFF), the state will always be INACTIVE. + * Other states indicate the current auto-framing state:</p> + * <ul> + * <li>When <code>{@link CaptureRequest#CONTROL_AUTOFRAMING android.control.autoframing}</code> is set to ON, auto-framing will take + * place. While the frame is aligning itself to center the object (doing things like + * zooming in, zooming out or pan), the state will be FRAMING.</li> + * <li>When field of view is not being adjusted anymore and has reached a stable state, the + * state will be CONVERGED.</li> + * </ul> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #CONTROL_AUTOFRAMING_STATE_INACTIVE INACTIVE}</li> + * <li>{@link #CONTROL_AUTOFRAMING_STATE_FRAMING FRAMING}</li> + * <li>{@link #CONTROL_AUTOFRAMING_STATE_CONVERGED CONVERGED}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * <p><b>Limited capability</b> - + * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> + * + * @see CaptureRequest#CONTROL_AUTOFRAMING + * @see CameraCharacteristics#CONTROL_AUTOFRAMING_AVAILABLE + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see #CONTROL_AUTOFRAMING_STATE_INACTIVE + * @see #CONTROL_AUTOFRAMING_STATE_FRAMING + * @see #CONTROL_AUTOFRAMING_STATE_CONVERGED + */ + @PublicKey + @NonNull + public static final Key<Integer> CONTROL_AUTOFRAMING_STATE = + new Key<Integer>("android.control.autoframingState", int.class); + + /** * <p>Operation mode for edge * enhancement.</p> * <p>Edge enhancement improves sharpness and details in the captured image. OFF means diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index e483328feb04..ac1583aec376 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -895,9 +895,21 @@ public class Process { return isIsolated(myUid()); } - /** {@hide} */ - @UnsupportedAppUsage + /** + * @deprecated Use {@link #isIsolatedUid(int)} instead. + * {@hide} + */ + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.TIRAMISU, + publicAlternatives = "Use {@link #isIsolatedUid(int)} instead.") public static final boolean isIsolated(int uid) { + return isIsolatedUid(uid); + } + + /** + * Returns whether the process with the given {@code uid} is an isolated sandbox. + */ + public static final boolean isIsolatedUid(int uid) { uid = UserHandle.getAppId(uid); return (uid >= FIRST_ISOLATED_UID && uid <= LAST_ISOLATED_UID) || (uid >= FIRST_APP_ZYGOTE_ISOLATED_UID && uid <= LAST_APP_ZYGOTE_ISOLATED_UID); diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java index 7095d1b20d84..8a09cd77efdb 100644 --- a/core/java/android/provider/DeviceConfig.java +++ b/core/java/android/provider/DeviceConfig.java @@ -25,9 +25,6 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.app.ActivityThread; -import android.content.ContentResolver; -import android.content.Context; import android.content.pm.PackageManager; import android.database.ContentObserver; import android.net.Uri; @@ -131,6 +128,13 @@ public final class DeviceConfig { public static final String NAMESPACE_APP_STANDBY = "app_standby"; /** + * Namespace for all App Cloning related features. + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final String NAMESPACE_APP_CLONING = "app_cloning"; + + /** * Namespace for AttentionManagerService related features. * * @hide @@ -875,9 +879,8 @@ public final class DeviceConfig { @NonNull @RequiresPermission(READ_DEVICE_CONFIG) public static Properties getProperties(@NonNull String namespace, @NonNull String ... names) { - ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver(); return new Properties(namespace, - Settings.Config.getStrings(contentResolver, namespace, Arrays.asList(names))); + Settings.Config.getStrings(namespace, Arrays.asList(names))); } /** @@ -1016,8 +1019,7 @@ public final class DeviceConfig { @RequiresPermission(WRITE_DEVICE_CONFIG) public static boolean setProperty(@NonNull String namespace, @NonNull String name, @Nullable String value, boolean makeDefault) { - ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver(); - return Settings.Config.putString(contentResolver, namespace, name, value, makeDefault); + return Settings.Config.putString(namespace, name, value, makeDefault); } /** @@ -1038,8 +1040,7 @@ public final class DeviceConfig { @SystemApi @RequiresPermission(WRITE_DEVICE_CONFIG) public static boolean setProperties(@NonNull Properties properties) throws BadConfigException { - ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver(); - return Settings.Config.setStrings(contentResolver, properties.getNamespace(), + return Settings.Config.setStrings(properties.getNamespace(), properties.mMap); } @@ -1055,8 +1056,7 @@ public final class DeviceConfig { @SystemApi @RequiresPermission(WRITE_DEVICE_CONFIG) public static boolean deleteProperty(@NonNull String namespace, @NonNull String name) { - ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver(); - return Settings.Config.deleteString(contentResolver, namespace, name); + return Settings.Config.deleteString(namespace, name); } /** @@ -1087,8 +1087,7 @@ public final class DeviceConfig { @SystemApi @RequiresPermission(WRITE_DEVICE_CONFIG) public static void resetToDefaults(@ResetMode int resetMode, @Nullable String namespace) { - ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver(); - Settings.Config.resetToDefaults(contentResolver, resetMode, namespace); + Settings.Config.resetToDefaults(resetMode, namespace); } /** @@ -1105,8 +1104,7 @@ public final class DeviceConfig { */ @RequiresPermission(WRITE_DEVICE_CONFIG) public static void setSyncDisabledMode(@SyncDisabledMode int syncDisabledMode) { - ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver(); - Settings.Config.setSyncDisabledMode(contentResolver, syncDisabledMode); + Settings.Config.setSyncDisabledMode(syncDisabledMode); } /** @@ -1117,8 +1115,7 @@ public final class DeviceConfig { */ @RequiresPermission(WRITE_DEVICE_CONFIG) public static @SyncDisabledMode int getSyncDisabledMode() { - ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver(); - return Settings.Config.getSyncDisabledMode(contentResolver); + return Settings.Config.getSyncDisabledMode(); } /** @@ -1141,8 +1138,7 @@ public final class DeviceConfig { @NonNull String namespace, @NonNull @CallbackExecutor Executor executor, @NonNull OnPropertiesChangedListener onPropertiesChangedListener) { - enforceReadPermission(ActivityThread.currentApplication().getApplicationContext(), - namespace); + enforceReadPermission(namespace); synchronized (sLock) { Pair<String, Executor> oldNamespace = sListeners.get(onPropertiesChangedListener); if (oldNamespace == null) { @@ -1209,7 +1205,7 @@ public final class DeviceConfig { } } }; - ActivityThread.currentApplication().getContentResolver() + Settings.Config .registerContentObserver(createNamespaceUri(namespace), true, contentObserver); sNamespaces.put(namespace, new Pair<>(contentObserver, 1)); } @@ -1233,8 +1229,7 @@ public final class DeviceConfig { sNamespaces.put(namespace, new Pair<>(namespaceCount.first, namespaceCount.second - 1)); } else { // Decrementing a namespace to zero means we no longer need its ContentObserver. - ActivityThread.currentApplication().getContentResolver() - .unregisterContentObserver(namespaceCount.first); + Settings.Config.unregisterContentObserver(namespaceCount.first); sNamespaces.remove(namespace); } } @@ -1274,8 +1269,8 @@ public final class DeviceConfig { * Enforces READ_DEVICE_CONFIG permission if namespace is not one of public namespaces. * @hide */ - public static void enforceReadPermission(Context context, String namespace) { - if (context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) + public static void enforceReadPermission(String namespace) { + if (Settings.Config.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) != PackageManager.PERMISSION_GRANTED) { if (!PUBLIC_NAMESPACES.contains(namespace)) { throw new SecurityException("Permission denial: reading from settings requires:" diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 52b1adb876fc..ef448f5dbd1b 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -47,9 +47,11 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PermissionName; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.database.ContentObserver; import android.database.Cursor; import android.database.SQLException; import android.location.ILocationManager; @@ -3344,7 +3346,7 @@ public final class Settings { public ArrayMap<String, String> getStringsForPrefix(ContentResolver cr, String prefix, List<String> names) { String namespace = prefix.substring(0, prefix.length() - 1); - DeviceConfig.enforceReadPermission(ActivityThread.currentApplication(), namespace); + DeviceConfig.enforceReadPermission(namespace); ArrayMap<String, String> keyValues = new ArrayMap<>(); int currentGeneration = -1; @@ -18002,20 +18004,36 @@ public final class Settings { /** * Look up a name in the database. - * @param resolver to access the database with * @param name to look up in the table * @return the corresponding value, or null if not present * * @hide */ @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) - static String getString(ContentResolver resolver, String name) { + static String getString(String name) { + ContentResolver resolver = getContentResolver(); return sNameValueCache.getStringForUser(resolver, name, resolver.getUserId()); } /** * Look up a list of names in the database, within the specified namespace. * + * @param namespace to which the names belong + * @param names to look up in the table + * @return a non null, but possibly empty, map from name to value for any of the names that + * were found during lookup. + * + * @hide + */ + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) + public static Map<String, String> getStrings(@NonNull String namespace, + @NonNull List<String> names) { + return getStrings(getContentResolver(), namespace, names); + } + + /** + * Look up a list of names in the database, within the specified namespace. + * * @param resolver to access the database with * @param namespace to which the names belong * @param names to look up in the table @@ -18053,7 +18071,6 @@ public final class Settings { * <strong>not</strong> be set as the default. * </p> * - * @param resolver to access the database with. * @param namespace to store the name/value pair in. * @param name to store. * @param value to associate with the name. @@ -18065,8 +18082,9 @@ public final class Settings { * @hide */ @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG) - static boolean putString(@NonNull ContentResolver resolver, @NonNull String namespace, + public static boolean putString(@NonNull String namespace, @NonNull String name, @Nullable String value, boolean makeDefault) { + ContentResolver resolver = getContentResolver(); return sNameValueCache.putStringForUser(resolver, createCompositeName(namespace, name), value, null, makeDefault, resolver.getUserId(), DEFAULT_OVERRIDEABLE_BY_RESTORE); @@ -18076,6 +18094,23 @@ public final class Settings { * Clear all name/value pairs for the provided namespace and save new name/value pairs in * their place. * + * @param namespace to which the names should be set. + * @param keyValues map of key names (without the prefix) to values. + * @return true if the name/value pairs were set, false if setting was blocked + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG) + public static boolean setStrings(@NonNull String namespace, + @NonNull Map<String, String> keyValues) + throws DeviceConfig.BadConfigException { + return setStrings(getContentResolver(), namespace, keyValues); + } + + /** + * Clear all name/value pairs for the provided namespace and save new name/value pairs in + * their place. + * * @param resolver to access the database with. * @param namespace to which the names should be set. * @param keyValues map of key names (without the prefix) to values. @@ -18106,7 +18141,6 @@ public final class Settings { /** * Delete a name/value pair from the database for the specified namespace. * - * @param resolver to access the database with. * @param namespace to delete the name/value pair from. * @param name to delete. * @return true if the value was deleted, false on database errors. If the name/value pair @@ -18117,8 +18151,9 @@ public final class Settings { * @hide */ @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG) - static boolean deleteString(@NonNull ContentResolver resolver, @NonNull String namespace, + static boolean deleteString(@NonNull String namespace, @NonNull String name) { + ContentResolver resolver = getContentResolver(); return sNameValueCache.deleteStringForUser(resolver, createCompositeName(namespace, name), resolver.getUserId()); } @@ -18129,7 +18164,6 @@ public final class Settings { * The method accepts an optional prefix parameter. If provided, only pairs with a name that * starts with the exact prefix will be reset. Otherwise all will be reset. * - * @param resolver Handle to the content resolver. * @param resetMode The reset mode to use. * @param namespace Optionally, to limit which which namespace is reset. * @@ -18138,9 +18172,10 @@ public final class Settings { * @hide */ @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG) - static void resetToDefaults(@NonNull ContentResolver resolver, @ResetMode int resetMode, + static void resetToDefaults(@ResetMode int resetMode, @Nullable String namespace) { try { + ContentResolver resolver = getContentResolver(); Bundle arg = new Bundle(); arg.putInt(CALL_METHOD_USER_KEY, resolver.getUserId()); arg.putInt(CALL_METHOD_RESET_MODE_KEY, resetMode); @@ -18163,9 +18198,9 @@ public final class Settings { */ @SuppressLint("AndroidFrameworkRequiresPermission") @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG) - static void setSyncDisabledMode( - @NonNull ContentResolver resolver, @SyncDisabledMode int disableSyncMode) { + static void setSyncDisabledMode(@SyncDisabledMode int disableSyncMode) { try { + ContentResolver resolver = getContentResolver(); Bundle args = new Bundle(); args.putInt(CALL_METHOD_SYNC_DISABLED_MODE_KEY, disableSyncMode); IContentProvider cp = sProviderHolder.getProvider(resolver); @@ -18184,8 +18219,9 @@ public final class Settings { */ @SuppressLint("AndroidFrameworkRequiresPermission") @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG) - static int getSyncDisabledMode(@NonNull ContentResolver resolver) { + static int getSyncDisabledMode() { try { + ContentResolver resolver = getContentResolver(); Bundle args = Bundle.EMPTY; IContentProvider cp = sProviderHolder.getProvider(resolver); Bundle bundle = cp.call(resolver.getAttributionSource(), @@ -18202,7 +18238,6 @@ public final class Settings { /** * Register callback for monitoring Config table. * - * @param resolver Handle to the content resolver. * @param callback callback to register * * @hide @@ -18213,6 +18248,50 @@ public final class Settings { registerMonitorCallbackAsUser(resolver, resolver.getUserId(), callback); } + + /** + * Register a content observer + * + * @hide + */ + public static void registerContentObserver(@NonNull Uri uri, boolean notifyForDescendants, + @NonNull ContentObserver observer) { + ActivityThread.currentApplication().getContentResolver() + .registerContentObserver(uri, notifyForDescendants, observer); + } + + /** + * Unregister a content observer + * + * @hide + */ + public static void unregisterContentObserver(@NonNull ContentObserver observer) { + ActivityThread.currentApplication().getContentResolver() + .unregisterContentObserver(observer); + } + + /** + * Determine whether the calling process of an IPC <em>or you</em> have been + * granted a particular permission. This is the same as + * {@link #checkCallingPermission}, except it grants your own permissions + * if you are not currently processing an IPC. Use with care! + * + * @param permission The name of the permission being checked. + * + * @return {@link PackageManager#PERMISSION_GRANTED} if the calling + * pid/uid is allowed that permission, or + * {@link PackageManager#PERMISSION_DENIED} if it is not. + * + * @see PackageManager#checkPermission(String, String) + * @see #checkPermission + * @see #checkCallingPermission + * @hide + */ + public static int checkCallingOrSelfPermission(@NonNull @PermissionName String permission) { + return ActivityThread.currentApplication() + .getApplicationContext().checkCallingOrSelfPermission(permission); + } + private static void registerMonitorCallbackAsUser( @NonNull ContentResolver resolver, @UserIdInt int userHandle, @NonNull RemoteCallback callback) { @@ -18245,6 +18324,10 @@ public final class Settings { Preconditions.checkNotNull(namespace); return namespace + "/"; } + + private static ContentResolver getContentResolver() { + return ActivityThread.currentApplication().getContentResolver(); + } } /** diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index 6f3e786ffc4e..24b7c3c439d4 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -43,8 +43,13 @@ import java.util.Objects; public abstract class CredentialProviderService extends Service { /** Extra to be used by provider to populate the credential when ending the activity started * through the {@code pendingIntent} on the selected {@link SaveEntry}. **/ - public static final String EXTRA_SAVE_CREDENTIAL = - "android.service.credentials.extra.SAVE_CREDENTIAL"; + public static final String EXTRA_CREATE_CREDENTIAL_RESPONSE = + "android.service.credentials.extra.CREATE_CREDENTIAL_RESPONSE"; + + /** Extra to be used by provider to populate the {@link CredentialsDisplayContent} when + * an authentication action entry is selected. **/ + public static final String EXTRA_GET_CREDENTIALS_DISPLAY_CONTENT = + "android.service.credentials.extra.GET_CREDENTIALS_DISPLAY_CONTENT"; /** * Provider must read the value against this extra to receive the complete create credential @@ -53,6 +58,10 @@ public abstract class CredentialProviderService extends Service { public static final String EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS = "android.service.credentials.extra.CREATE_CREDENTIAL_REQUEST_PARAMS"; + /** Extra to be used by the provider when setting the credential result. */ + public static final String EXTRA_GET_CREDENTIAL = + "android.service.credentials.extra.GET_CREDENTIAL"; + private static final String TAG = "CredProviderService"; public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index cfc79e4fef66..e821af1ab313 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -2365,6 +2365,7 @@ public abstract class NotificationListenerService extends Service { UserHandle user= (UserHandle) args.arg2; NotificationChannel channel = (NotificationChannel) args.arg3; int modificationType = (int) args.arg4; + args.recycle(); onNotificationChannelModified(pkgName, user, channel, modificationType); } break; @@ -2374,6 +2375,7 @@ public abstract class NotificationListenerService extends Service { UserHandle user = (UserHandle) args.arg2; NotificationChannelGroup group = (NotificationChannelGroup) args.arg3; int modificationType = (int) args.arg4; + args.recycle(); onNotificationChannelGroupModified(pkgName, user, group, modificationType); } break; diff --git a/core/java/android/transparency/BinaryTransparencyManager.java b/core/java/android/transparency/BinaryTransparencyManager.java index 18783f507085..f6d7c611e9d9 100644 --- a/core/java/android/transparency/BinaryTransparencyManager.java +++ b/core/java/android/transparency/BinaryTransparencyManager.java @@ -24,7 +24,7 @@ import android.util.Slog; import com.android.internal.os.IBinaryTransparencyService; -import java.util.Map; +import java.util.List; /** * BinaryTransparencyManager defines a number of system interfaces that other system apps or @@ -66,12 +66,15 @@ public class BinaryTransparencyManager { } /** - * Returns a map of all installed APEXs consisting of package name to SHA256 hash of the - * package. - * @return A Map with the following entries: {apex package name : sha256 digest of package} + * Gets binary measurements of all installed APEXs, each packed in a Bundle. + * @return A List of {@link android.os.Bundle}s with the following keys: + * {@link com.android.server.BinaryTransparencyService#BUNDLE_PACKAGE_INFO} + * {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST_ALGORITHM} + * {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST} */ + // TODO(b/259422958): Fix static constants referenced here - should be defined here @NonNull - public Map getApexInfo() { + public List getApexInfo() { try { Slog.d(TAG, "Calling backend's getApexInfo()"); return mService.getApexInfo(); diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index b24303b41abd..720813ad81ef 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -1073,7 +1073,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall private void handleSyncBufferCallback(SurfaceHolder.Callback[] callbacks, SyncBufferTransactionCallback syncBufferTransactionCallback) { - getViewRootImpl().addToSync(syncBufferCallback -> + getViewRootImpl().addToSync((parentSyncGroup, syncBufferCallback) -> redrawNeededAsync(callbacks, () -> { Transaction t = null; if (mBlastBufferQueue != null) { @@ -1081,7 +1081,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall t = syncBufferTransactionCallback.waitForTransaction(); } - syncBufferCallback.onBufferReady(t); + syncBufferCallback.onTransactionReady(t); onDrawFinished(); })); } @@ -1092,9 +1092,9 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall mSyncGroups.add(syncGroup); } - syncGroup.addToSync(syncBufferCallback -> redrawNeededAsync(callbacks, - () -> { - syncBufferCallback.onBufferReady(null); + syncGroup.addToSync((parentSyncGroup, syncBufferCallback) -> + redrawNeededAsync(callbacks, () -> { + syncBufferCallback.onTransactionReady(null); onDrawFinished(); synchronized (mSyncGroups) { mSyncGroups.remove(syncGroup); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 7e8ebd7fe076..5e1dc340a7a7 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -231,6 +231,7 @@ import java.util.List; import java.util.Objects; import java.util.Queue; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; /** * The top of a view hierarchy, implementing the needed protocol between View @@ -851,7 +852,7 @@ public final class ViewRootImpl implements ViewParent, } private SurfaceSyncGroup mSyncGroup; - private SurfaceSyncGroup.SyncBufferCallback mSyncBufferCallback; + private SurfaceSyncGroup.TransactionReadyCallback mTransactionReadyCallback; private int mNumSyncsInProgress = 0; private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks; @@ -3610,8 +3611,8 @@ public final class ViewRootImpl implements ViewParent, mPendingTransitions.clear(); } - if (mSyncBufferCallback != null) { - mSyncBufferCallback.onBufferReady(null); + if (mTransactionReadyCallback != null) { + mTransactionReadyCallback.onTransactionReady(null); } } else if (cancelAndRedraw) { mLastPerformTraversalsSkipDrawReason = cancelDueToPreDrawListener @@ -3626,8 +3627,8 @@ public final class ViewRootImpl implements ViewParent, } mPendingTransitions.clear(); } - if (!performDraw() && mSyncBufferCallback != null) { - mSyncBufferCallback.onBufferReady(null); + if (!performDraw() && mTransactionReadyCallback != null) { + mTransactionReadyCallback.onTransactionReady(null); } } @@ -3641,7 +3642,7 @@ public final class ViewRootImpl implements ViewParent, if (!cancelAndRedraw) { mReportNextDraw = false; mLastReportNextDrawReason = null; - mSyncBufferCallback = null; + mTransactionReadyCallback = null; mSyncBuffer = false; if (isInLocalSync()) { mSyncGroup.markSyncReady(); @@ -4388,7 +4389,7 @@ public final class ViewRootImpl implements ViewParent, return false; } - final boolean fullRedrawNeeded = mFullRedrawNeeded || mSyncBufferCallback != null; + final boolean fullRedrawNeeded = mFullRedrawNeeded || mTransactionReadyCallback != null; mFullRedrawNeeded = false; mIsDrawing = true; @@ -4396,9 +4397,9 @@ public final class ViewRootImpl implements ViewParent, addFrameCommitCallbackIfNeeded(); - boolean usingAsyncReport = isHardwareEnabled() && mSyncBufferCallback != null; + boolean usingAsyncReport = isHardwareEnabled() && mTransactionReadyCallback != null; if (usingAsyncReport) { - registerCallbacksForSync(mSyncBuffer, mSyncBufferCallback); + registerCallbacksForSync(mSyncBuffer, mTransactionReadyCallback); } else if (mHasPendingTransactions) { // These callbacks are only needed if there's no sync involved and there were calls to // applyTransactionOnDraw. These callbacks check if the draw failed for any reason and @@ -4449,10 +4450,11 @@ public final class ViewRootImpl implements ViewParent, } if (mSurfaceHolder != null && mSurface.isValid()) { - final SurfaceSyncGroup.SyncBufferCallback syncBufferCallback = mSyncBufferCallback; + final SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback = + mTransactionReadyCallback; SurfaceCallbackHelper sch = new SurfaceCallbackHelper(() -> - mHandler.post(() -> syncBufferCallback.onBufferReady(null))); - mSyncBufferCallback = null; + mHandler.post(() -> transactionReadyCallback.onTransactionReady(null))); + mTransactionReadyCallback = null; SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); @@ -4463,8 +4465,8 @@ public final class ViewRootImpl implements ViewParent, } } } - if (mSyncBufferCallback != null && !usingAsyncReport) { - mSyncBufferCallback.onBufferReady(null); + if (mTransactionReadyCallback != null && !usingAsyncReport) { + mTransactionReadyCallback.onTransactionReady(null); } if (mPerformContentCapture) { performContentCaptureInitialReport(); @@ -11134,7 +11136,7 @@ public final class ViewRootImpl implements ViewParent, } private void registerCallbacksForSync(boolean syncBuffer, - final SurfaceSyncGroup.SyncBufferCallback syncBufferCallback) { + final SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) { if (!isHardwareEnabled()) { return; } @@ -11161,7 +11163,7 @@ public final class ViewRootImpl implements ViewParent, // pendingDrawFinished. if ((syncResult & (SYNC_LOST_SURFACE_REWARD_IF_FOUND | SYNC_CONTEXT_IS_STOPPED)) != 0) { - syncBufferCallback.onBufferReady( + transactionReadyCallback.onTransactionReady( mBlastBufferQueue.gatherPendingTransactions(frame)); return null; } @@ -11171,7 +11173,8 @@ public final class ViewRootImpl implements ViewParent, } if (syncBuffer) { - mBlastBufferQueue.syncNextTransaction(syncBufferCallback::onBufferReady); + mBlastBufferQueue.syncNextTransaction( + transactionReadyCallback::onTransactionReady); } return didProduceBuffer -> { @@ -11191,7 +11194,7 @@ public final class ViewRootImpl implements ViewParent, // since the frame didn't draw on this vsync. It's possible the frame will // draw later, but it's better to not be sync than to block on a frame that // may never come. - syncBufferCallback.onBufferReady( + transactionReadyCallback.onTransactionReady( mBlastBufferQueue.gatherPendingTransactions(frame)); return; } @@ -11200,22 +11203,49 @@ public final class ViewRootImpl implements ViewParent, // syncNextTransaction callback. Instead, just report back to the Syncer so it // knows that this sync request is complete. if (!syncBuffer) { - syncBufferCallback.onBufferReady(null); + transactionReadyCallback.onTransactionReady(null); } }; } }); } - public final SurfaceSyncGroup.SyncTarget mSyncTarget = new SurfaceSyncGroup.SyncTarget() { + private final Executor mPostAtFrontExecutor = new Executor() { @Override - public void onReadyToSync(SurfaceSyncGroup.SyncBufferCallback syncBufferCallback) { - readyToSync(syncBufferCallback); + public void execute(Runnable command) { + mHandler.postAtFrontOfQueue(command); } + }; + public final SurfaceSyncGroup.SyncTarget mSyncTarget = new SurfaceSyncGroup.SyncTarget() { @Override - public void onSyncComplete() { - mHandler.postAtFrontOfQueue(() -> { + public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, + SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) { + updateSyncInProgressCount(parentSyncGroup); + if (!isInLocalSync()) { + // Always sync the buffer if the sync request did not come from VRI. + mSyncBuffer = true; + } + if (mAttachInfo.mThreadedRenderer != null) { + HardwareRenderer.setRtAnimationsEnabled(false); + } + + if (mTransactionReadyCallback != null) { + Log.d(mTag, "Already set sync for the next draw."); + mTransactionReadyCallback.onTransactionReady(null); + } + if (DEBUG_BLAST) { + Log.d(mTag, "Setting syncFrameCallback"); + } + mTransactionReadyCallback = transactionReadyCallback; + if (!mIsInTraversal && !mTraversalScheduled) { + scheduleTraversals(); + } + } + + private void updateSyncInProgressCount(SurfaceSyncGroup parentSyncGroup) { + mNumSyncsInProgress++; + parentSyncGroup.addSyncCompleteCallback(mPostAtFrontExecutor, () -> { if (--mNumSyncsInProgress == 0 && mAttachInfo.mThreadedRenderer != null) { HardwareRenderer.setRtAnimationsEnabled(true); } @@ -11228,29 +11258,6 @@ public final class ViewRootImpl implements ViewParent, return mSyncTarget; } - private void readyToSync(SurfaceSyncGroup.SyncBufferCallback syncBufferCallback) { - mNumSyncsInProgress++; - if (!isInLocalSync()) { - // Always sync the buffer if the sync request did not come from VRI. - mSyncBuffer = true; - } - if (mAttachInfo.mThreadedRenderer != null) { - HardwareRenderer.setRtAnimationsEnabled(false); - } - - if (mSyncBufferCallback != null) { - Log.d(mTag, "Already set sync for the next draw."); - mSyncBufferCallback.onBufferReady(null); - } - if (DEBUG_BLAST) { - Log.d(mTag, "Setting syncFrameCallback"); - } - mSyncBufferCallback = syncBufferCallback; - if (!mIsInTraversal && !mTraversalScheduled) { - scheduleTraversals(); - } - } - void mergeSync(SurfaceSyncGroup otherSyncGroup) { if (!isInLocalSync()) { return; diff --git a/core/java/android/view/inputmethod/TextAppearanceInfo.java b/core/java/android/view/inputmethod/TextAppearanceInfo.java index 1df4fc54089e..500c41cd1fed 100644 --- a/core/java/android/view/inputmethod/TextAppearanceInfo.java +++ b/core/java/android/view/inputmethod/TextAppearanceInfo.java @@ -16,12 +16,13 @@ package android.view.inputmethod; +import static android.graphics.Typeface.NORMAL; + import android.annotation.ColorInt; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Px; -import android.content.res.ColorStateList; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.fonts.FontStyle; @@ -30,7 +31,7 @@ import android.inputmethodservice.InputMethodService; import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; -import android.text.InputFilter; +import android.text.method.TransformationMethod; import android.widget.TextView; import java.util.Objects; @@ -38,7 +39,6 @@ import java.util.Objects; /** * Information about text appearance in an editor, passed through * {@link CursorAnchorInfo} for use by {@link InputMethodService}. - * * @see TextView * @see Paint * @see CursorAnchorInfo.Builder#setTextAppearanceInfo(TextAppearanceInfo) @@ -46,12 +46,12 @@ import java.util.Objects; */ public final class TextAppearanceInfo implements Parcelable { /** - * The text size (in pixels) for current {@link TextView}. + * The text size (in pixels) for current editor. */ private final @Px float mTextSize; /** - * The LocaleList of the text. + * The {@link LocaleList} of the text. */ @NonNull private final LocaleList mTextLocales; @@ -64,7 +64,8 @@ public final class TextAppearanceInfo implements Parcelable { /** * The weight of the text. */ - private final @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int mTextFontWeight; + @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX) + private final int mTextFontWeight; /** * The style (normal, bold, italic, bold|italic) of the text, see {@link Typeface}. @@ -72,8 +73,7 @@ public final class TextAppearanceInfo implements Parcelable { private final @Typeface.Style int mTextStyle; /** - * Whether the transformation method applied to the current {@link TextView} is set to - * ALL CAPS. + * Whether the transformation method applied to the current editor is set to all caps. */ private final boolean mAllCaps; @@ -93,6 +93,11 @@ public final class TextAppearanceInfo implements Parcelable { private final @Px float mShadowRadius; /** + * The shadow color of the text shadow. + */ + private final @ColorInt int mShadowColor; + + /** * The elegant text height, especially for less compacted complex script text. */ private final boolean mElegantTextHeight; @@ -135,67 +140,46 @@ public final class TextAppearanceInfo implements Parcelable { /** * The color of the text selection highlight. */ - private final @ColorInt int mTextColorHighlight; + private final @ColorInt int mHighlightTextColor; /** - * The current text color. + * The current text color of the editor. */ private final @ColorInt int mTextColor; /** - * The current color of the hint text. - */ - private final @ColorInt int mTextColorHint; - - /** - * The text color for links. - */ - @Nullable private final ColorStateList mTextColorLink; - - /** - * The max length of text. - */ - private final int mMaxLength; - - - public TextAppearanceInfo(@NonNull TextView textView) { - mTextSize = textView.getTextSize(); - mTextLocales = textView.getTextLocales(); - Typeface typeface = textView.getPaint().getTypeface(); - String systemFontFamilyName = null; - int textFontWeight = -1; - if (typeface != null) { - systemFontFamilyName = typeface.getSystemFontFamilyName(); - textFontWeight = typeface.getWeight(); - } - mSystemFontFamilyName = systemFontFamilyName; - mTextFontWeight = textFontWeight; - mTextStyle = textView.getTypefaceStyle(); - mAllCaps = textView.isAllCaps(); - mShadowRadius = textView.getShadowRadius(); - mShadowDx = textView.getShadowDx(); - mShadowDy = textView.getShadowDy(); - mElegantTextHeight = textView.isElegantTextHeight(); - mFallbackLineSpacing = textView.isFallbackLineSpacing(); - mLetterSpacing = textView.getLetterSpacing(); - mFontFeatureSettings = textView.getFontFeatureSettings(); - mFontVariationSettings = textView.getFontVariationSettings(); - mLineBreakStyle = textView.getLineBreakStyle(); - mLineBreakWordStyle = textView.getLineBreakWordStyle(); - mTextScaleX = textView.getTextScaleX(); - mTextColorHighlight = textView.getHighlightColor(); - mTextColor = textView.getCurrentTextColor(); - mTextColorHint = textView.getCurrentHintTextColor(); - mTextColorLink = textView.getLinkTextColors(); - int maxLength = -1; - for (InputFilter filter: textView.getFilters()) { - if (filter instanceof InputFilter.LengthFilter) { - maxLength = ((InputFilter.LengthFilter) filter).getMax(); - // There is at most one LengthFilter. - break; - } - } - mMaxLength = maxLength; + * The current color of the hint text. + */ + private final @ColorInt int mHintTextColor; + + /** + * The text color used to paint the links in the editor. + */ + private final @ColorInt int mLinkTextColor; + + private TextAppearanceInfo(@NonNull final TextAppearanceInfo.Builder builder) { + mTextSize = builder.mTextSize; + mTextLocales = builder.mTextLocales; + mSystemFontFamilyName = builder.mSystemFontFamilyName; + mTextFontWeight = builder.mTextFontWeight; + mTextStyle = builder.mTextStyle; + mAllCaps = builder.mAllCaps; + mShadowDx = builder.mShadowDx; + mShadowDy = builder.mShadowDy; + mShadowRadius = builder.mShadowRadius; + mShadowColor = builder.mShadowColor; + mElegantTextHeight = builder.mElegantTextHeight; + mFallbackLineSpacing = builder.mFallbackLineSpacing; + mLetterSpacing = builder.mLetterSpacing; + mFontFeatureSettings = builder.mFontFeatureSettings; + mFontVariationSettings = builder.mFontVariationSettings; + mLineBreakStyle = builder.mLineBreakStyle; + mLineBreakWordStyle = builder.mLineBreakWordStyle; + mTextScaleX = builder.mTextScaleX; + mHighlightTextColor = builder.mHighlightTextColor; + mTextColor = builder.mTextColor; + mHintTextColor = builder.mHintTextColor; + mLinkTextColor = builder.mLinkTextColor; } @Override @@ -214,6 +198,7 @@ public final class TextAppearanceInfo implements Parcelable { dest.writeFloat(mShadowDx); dest.writeFloat(mShadowDy); dest.writeFloat(mShadowRadius); + dest.writeInt(mShadowColor); dest.writeBoolean(mElegantTextHeight); dest.writeBoolean(mFallbackLineSpacing); dest.writeFloat(mLetterSpacing); @@ -222,14 +207,13 @@ public final class TextAppearanceInfo implements Parcelable { dest.writeInt(mLineBreakStyle); dest.writeInt(mLineBreakWordStyle); dest.writeFloat(mTextScaleX); - dest.writeInt(mTextColorHighlight); + dest.writeInt(mHighlightTextColor); dest.writeInt(mTextColor); - dest.writeInt(mTextColorHint); - dest.writeTypedObject(mTextColorLink, flags); - dest.writeInt(mMaxLength); + dest.writeInt(mHintTextColor); + dest.writeInt(mLinkTextColor); } - private TextAppearanceInfo(@NonNull Parcel in) { + TextAppearanceInfo(@NonNull Parcel in) { mTextSize = in.readFloat(); mTextLocales = LocaleList.CREATOR.createFromParcel(in); mAllCaps = in.readBoolean(); @@ -239,6 +223,7 @@ public final class TextAppearanceInfo implements Parcelable { mShadowDx = in.readFloat(); mShadowDy = in.readFloat(); mShadowRadius = in.readFloat(); + mShadowColor = in.readInt(); mElegantTextHeight = in.readBoolean(); mFallbackLineSpacing = in.readBoolean(); mLetterSpacing = in.readFloat(); @@ -247,11 +232,10 @@ public final class TextAppearanceInfo implements Parcelable { mLineBreakStyle = in.readInt(); mLineBreakWordStyle = in.readInt(); mTextScaleX = in.readFloat(); - mTextColorHighlight = in.readInt(); + mHighlightTextColor = in.readInt(); mTextColor = in.readInt(); - mTextColorHint = in.readInt(); - mTextColorLink = in.readTypedObject(ColorStateList.CREATOR); - mMaxLength = in.readInt(); + mHintTextColor = in.readInt(); + mLinkTextColor = in.readInt(); } @NonNull @@ -268,14 +252,14 @@ public final class TextAppearanceInfo implements Parcelable { }; /** - * Returns the text size (in pixels) for current {@link TextView}. + * Returns the text size (in pixels) for current editor. */ public @Px float getTextSize() { return mTextSize; } /** - * Returns the LocaleList of the text. + * Returns the {@link LocaleList} of the text. */ @NonNull public LocaleList getTextLocales() { @@ -286,31 +270,38 @@ public final class TextAppearanceInfo implements Parcelable { * Returns the font family name if the {@link Typeface} of the text is created from a * system font family. Returns null if no {@link Typeface} is specified, or it is not created * from a system font family. + * + * @see Typeface#getSystemFontFamilyName() */ @Nullable - public String getFontFamilyName() { + public String getSystemFontFamilyName() { return mSystemFontFamilyName; } /** - * Returns the weight of the text. Returns -1 when no {@link Typeface} is specified. + * Returns the weight of the text, or {@code FontStyle#FONT_WEIGHT_UNSPECIFIED} + * when no {@link Typeface} is specified. */ - public @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int getTextFontWeight() { + @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX) + public int getTextFontWeight() { return mTextFontWeight; } /** * Returns the style (normal, bold, italic, bold|italic) of the text. Returns - * {@link Typeface#NORMAL} when no {@link Typeface} is specified. See {@link Typeface} for - * more information. + * {@link Typeface#NORMAL} when no {@link Typeface} is specified. + * + * @see Typeface */ public @Typeface.Style int getTextStyle() { return mTextStyle; } /** - * Returns whether the transformation method applied to the current {@link TextView} is set to - * ALL CAPS. + * Returns whether the transformation method applied to the current editor is set to all caps. + * + * @see TextView#setAllCaps(boolean) + * @see TextView#setTransformationMethod(TransformationMethod) */ public boolean isAllCaps() { return mAllCaps; @@ -318,6 +309,8 @@ public final class TextAppearanceInfo implements Parcelable { /** * Returns the horizontal offset (in pixels) of the text shadow. + * + * @see Paint#setShadowLayer(float, float, float, int) */ public @Px float getShadowDx() { return mShadowDx; @@ -325,6 +318,8 @@ public final class TextAppearanceInfo implements Parcelable { /** * Returns the vertical offset (in pixels) of the text shadow. + * + * @see Paint#setShadowLayer(float, float, float, int) */ public @Px float getShadowDy() { return mShadowDy; @@ -332,15 +327,28 @@ public final class TextAppearanceInfo implements Parcelable { /** * Returns the blur radius (in pixels) of the text shadow. + * + * @see Paint#setShadowLayer(float, float, float, int) */ public @Px float getShadowRadius() { return mShadowRadius; } /** + * Returns the color of the text shadow. + * + * @see Paint#setShadowLayer(float, float, float, int) + */ + public @ColorInt int getShadowColor() { + return mShadowColor; + } + + /** * Returns {@code true} if the elegant height metrics flag is set. This setting selects font * variants that have not been compacted to fit Latin-based vertical metrics, and also increases * top and bottom bounds to provide more space. + * + * @see Paint#isElegantTextHeight() */ public boolean isElegantTextHeight() { return mElegantTextHeight; @@ -411,13 +419,17 @@ public final class TextAppearanceInfo implements Parcelable { /** * Returns the color of the text selection highlight. + * + * @see TextView#getHighlightColor() */ - public @ColorInt int getTextColorHighlight() { - return mTextColorHighlight; + public @ColorInt int getHighlightTextColor() { + return mHighlightTextColor; } /** - * Returns the current text color. + * Returns the current text color of the editor. + * + * @see TextView#getCurrentTextColor() */ public @ColorInt int getTextColor() { return mTextColor; @@ -425,27 +437,22 @@ public final class TextAppearanceInfo implements Parcelable { /** * Returns the current color of the hint text. + * + * @see TextView#getCurrentHintTextColor() */ - public @ColorInt int getTextColorHint() { - return mTextColorHint; + public @ColorInt int getHintTextColor() { + return mHintTextColor; } /** - * Returns the text color for links. + * Returns the text color used to paint the links in the editor. + * + * @see TextView#getLinkTextColors() */ - @Nullable - public ColorStateList getTextColorLink() { - return mTextColorLink; + public @ColorInt int getLinkTextColor() { + return mLinkTextColor; } - /** - * Returns the max length of text, which is used to set an input filter to constrain the text - * length to the specified number. Returns -1 when there is no {@link InputFilter.LengthFilter} - * in the Editor. - */ - public int getMaxLength() { - return mMaxLength; - } @Override public boolean equals(Object o) { @@ -456,27 +463,29 @@ public final class TextAppearanceInfo implements Parcelable { && mTextFontWeight == that.mTextFontWeight && mTextStyle == that.mTextStyle && mAllCaps == that.mAllCaps && Float.compare(that.mShadowDx, mShadowDx) == 0 && Float.compare(that.mShadowDy, mShadowDy) == 0 && Float.compare( - that.mShadowRadius, mShadowRadius) == 0 && mMaxLength == that.mMaxLength + that.mShadowRadius, mShadowRadius) == 0 && that.mShadowColor == mShadowColor && mElegantTextHeight == that.mElegantTextHeight && mFallbackLineSpacing == that.mFallbackLineSpacing && Float.compare( that.mLetterSpacing, mLetterSpacing) == 0 && mLineBreakStyle == that.mLineBreakStyle && mLineBreakWordStyle == that.mLineBreakWordStyle - && mTextColorHighlight == that.mTextColorHighlight && mTextColor == that.mTextColor - && mTextColorLink.getDefaultColor() == that.mTextColorLink.getDefaultColor() - && mTextColorHint == that.mTextColorHint && Objects.equals( - mTextLocales, that.mTextLocales) && Objects.equals(mSystemFontFamilyName, - that.mSystemFontFamilyName) && Objects.equals(mFontFeatureSettings, - that.mFontFeatureSettings) && Objects.equals(mFontVariationSettings, - that.mFontVariationSettings) && Float.compare(that.mTextScaleX, mTextScaleX) == 0; + && mHighlightTextColor == that.mHighlightTextColor + && mTextColor == that.mTextColor + && mLinkTextColor == that.mLinkTextColor + && mHintTextColor == that.mHintTextColor + && Objects.equals(mTextLocales, that.mTextLocales) + && Objects.equals(mSystemFontFamilyName, that.mSystemFontFamilyName) + && Objects.equals(mFontFeatureSettings, that.mFontFeatureSettings) + && Objects.equals(mFontVariationSettings, that.mFontVariationSettings) + && Float.compare(that.mTextScaleX, mTextScaleX) == 0; } @Override public int hashCode() { return Objects.hash(mTextSize, mTextLocales, mSystemFontFamilyName, mTextFontWeight, - mTextStyle, mAllCaps, mShadowDx, mShadowDy, mShadowRadius, mElegantTextHeight, - mFallbackLineSpacing, mLetterSpacing, mFontFeatureSettings, mFontVariationSettings, - mLineBreakStyle, mLineBreakWordStyle, mTextScaleX, mTextColorHighlight, mTextColor, - mTextColorHint, mTextColorLink, mMaxLength); + mTextStyle, mAllCaps, mShadowDx, mShadowDy, mShadowRadius, mShadowColor, + mElegantTextHeight, mFallbackLineSpacing, mLetterSpacing, mFontFeatureSettings, + mFontVariationSettings, mLineBreakStyle, mLineBreakWordStyle, mTextScaleX, + mHighlightTextColor, mTextColor, mHintTextColor, mLinkTextColor); } @Override @@ -491,6 +500,7 @@ public final class TextAppearanceInfo implements Parcelable { + ", mShadowDx=" + mShadowDx + ", mShadowDy=" + mShadowDy + ", mShadowRadius=" + mShadowRadius + + ", mShadowColor=" + mShadowColor + ", mElegantTextHeight=" + mElegantTextHeight + ", mFallbackLineSpacing=" + mFallbackLineSpacing + ", mLetterSpacing=" + mLetterSpacing @@ -499,11 +509,290 @@ public final class TextAppearanceInfo implements Parcelable { + ", mLineBreakStyle=" + mLineBreakStyle + ", mLineBreakWordStyle=" + mLineBreakWordStyle + ", mTextScaleX=" + mTextScaleX - + ", mTextColorHighlight=" + mTextColorHighlight + + ", mHighlightTextColor=" + mHighlightTextColor + ", mTextColor=" + mTextColor - + ", mTextColorHint=" + mTextColorHint - + ", mTextColorLink=" + mTextColorLink - + ", mMaxLength=" + mMaxLength + + ", mHintTextColor=" + mHintTextColor + + ", mLinkTextColor=" + mLinkTextColor + '}'; } + + /** + * Builder for {@link TextAppearanceInfo}. + */ + public static final class Builder { + private @Px float mTextSize = -1; + private @NonNull LocaleList mTextLocales = LocaleList.getAdjustedDefault(); + @Nullable private String mSystemFontFamilyName = null; + @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX) + private int mTextFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED; + private @Typeface.Style int mTextStyle = NORMAL; + private boolean mAllCaps = false; + private @Px float mShadowDx = 0; + private @Px float mShadowDy = 0; + private @Px float mShadowRadius = 0; + private @ColorInt int mShadowColor = 0; + private boolean mElegantTextHeight = false; + private boolean mFallbackLineSpacing = false; + private float mLetterSpacing = 0; + @Nullable private String mFontFeatureSettings = null; + @Nullable private String mFontVariationSettings = null; + @LineBreakConfig.LineBreakStyle + private int mLineBreakStyle = LineBreakConfig.LINE_BREAK_STYLE_NONE; + @LineBreakConfig.LineBreakWordStyle + private int mLineBreakWordStyle = LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE; + private float mTextScaleX = 1; + private @ColorInt int mHighlightTextColor = 0; + private @ColorInt int mTextColor = 0; + private @ColorInt int mHintTextColor = 0; + private @ColorInt int mLinkTextColor = 0; + + /** + * Set the text size (in pixels) obtained from the current editor. + */ + @NonNull + public Builder setTextSize(@Px float textSize) { + mTextSize = textSize; + return this; + } + + /** + * Set the {@link LocaleList} of the text. + */ + @NonNull + public Builder setTextLocales(@NonNull LocaleList textLocales) { + mTextLocales = textLocales; + return this; + } + + /** + * Set the system font family name if the {@link Typeface} of the text is created from a + * system font family. + * + * @see Typeface#getSystemFontFamilyName() + */ + @NonNull + public Builder setSystemFontFamilyName(@Nullable String systemFontFamilyName) { + mSystemFontFamilyName = systemFontFamilyName; + return this; + } + + /** + * Set the weight of the text. + */ + @NonNull + public Builder setTextFontWeight( + @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, + to = FontStyle.FONT_WEIGHT_MAX) int textFontWeight) { + mTextFontWeight = textFontWeight; + return this; + } + + /** + * Set the style (normal, bold, italic, bold|italic) of the text. + * + * @see Typeface + */ + @NonNull + public Builder setTextStyle(@Typeface.Style int textStyle) { + mTextStyle = textStyle; + return this; + } + + /** + * Set whether the transformation method applied to the current editor is set to all caps. + * + * @see TextView#setAllCaps(boolean) + * @see TextView#setTransformationMethod(TransformationMethod) + */ + @NonNull + public Builder setAllCaps(boolean allCaps) { + mAllCaps = allCaps; + return this; + } + + /** + * Set the horizontal offset (in pixels) of the text shadow. + * + * @see Paint#setShadowLayer(float, float, float, int) + */ + @NonNull + public Builder setShadowDx(@Px float shadowDx) { + mShadowDx = shadowDx; + return this; + } + + /** + * Set the vertical offset (in pixels) of the text shadow. + * + * @see Paint#setShadowLayer(float, float, float, int) + */ + @NonNull + public Builder setShadowDy(@Px float shadowDy) { + mShadowDy = shadowDy; + return this; + } + + /** + * Set the blur radius (in pixels) of the text shadow. + * + * @see Paint#setShadowLayer(float, float, float, int) + */ + @NonNull + public Builder setShadowRadius(@Px float shadowRadius) { + mShadowRadius = shadowRadius; + return this; + } + + /** + * Set the color of the text shadow. + * + * @see Paint#setShadowLayer(float, float, float, int) + */ + @NonNull + public Builder setShadowColor(@ColorInt int shadowColor) { + mShadowColor = shadowColor; + return this; + } + + /** + * Set the elegant height metrics flag. This setting selects font variants that + * have not been compacted to fit Latin-based vertical metrics, and also increases + * top and bottom bounds to provide more space. + * + * @see Paint#isElegantTextHeight() + */ + @NonNull + public Builder setElegantTextHeight(boolean elegantTextHeight) { + mElegantTextHeight = elegantTextHeight; + return this; + } + + /** + * Set whether to expand linespacing based on fallback fonts. + * + * @see TextView#setFallbackLineSpacing(boolean) + */ + @NonNull + public Builder setFallbackLineSpacing(boolean fallbackLineSpacing) { + mFallbackLineSpacing = fallbackLineSpacing; + return this; + } + + /** + * Set the text letter-spacing, which determines the spacing between characters. + * The value is in 'EM' units. Normally, this value is 0.0. + */ + @NonNull + public Builder setLetterSpacing(float letterSpacing) { + mLetterSpacing = letterSpacing; + return this; + } + + /** + * Set the font feature settings. + * + * @see Paint#getFontFeatureSettings() + */ + @NonNull + public Builder setFontFeatureSettings(@Nullable String fontFeatureSettings) { + mFontFeatureSettings = fontFeatureSettings; + return this; + } + + /** + * Set the font variation settings. Returns null if no variation is specified. + * + * @see Paint#getFontVariationSettings() + */ + @NonNull + public Builder setFontVariationSettings(@Nullable String fontVariationSettings) { + mFontVariationSettings = fontVariationSettings; + return this; + } + + /** + * Set the line-break strategies for text wrapping. + * + * @see TextView#setLineBreakStyle(int) + */ + @NonNull + public Builder setLineBreakStyle(@LineBreakConfig.LineBreakStyle int lineBreakStyle) { + mLineBreakStyle = lineBreakStyle; + return this; + } + + /** + * Set the line-break word strategies for text wrapping. + * + * @see TextView#setLineBreakWordStyle(int) + */ + @NonNull + public Builder setLineBreakWordStyle( + @LineBreakConfig.LineBreakWordStyle int lineBreakWordStyle) { + mLineBreakWordStyle = lineBreakWordStyle; + return this; + } + + /** + * Set the extent by which text should be stretched horizontally. + */ + @NonNull + public Builder setTextScaleX(float textScaleX) { + mTextScaleX = textScaleX; + return this; + } + + /** + * Set the color of the text selection highlight. + * + * @see TextView#getHighlightColor() + */ + @NonNull + public Builder setHighlightTextColor(@ColorInt int highlightTextColor) { + mHighlightTextColor = highlightTextColor; + return this; + } + + /** + * Set the current text color of the editor. + * + * @see TextView#getCurrentTextColor() + */ + @NonNull + public Builder setTextColor(@ColorInt int textColor) { + mTextColor = textColor; + return this; + } + + /** + * Set the current color of the hint text. + * + * @see TextView#getCurrentHintTextColor() + */ + @NonNull + public Builder setHintTextColor(@ColorInt int hintTextColor) { + mHintTextColor = hintTextColor; + return this; + } + + /** + * Set the text color used to paint the links in the editor. + * + * @see TextView#getLinkTextColors() + */ + @NonNull + public Builder setLinkTextColor(@ColorInt int linkTextColor) { + mLinkTextColor = linkTextColor; + return this; + } + + /** + * Returns {@link TextAppearanceInfo} using parameters in this + * {@link TextAppearanceInfo.Builder}. + */ + @NonNull + public TextAppearanceInfo build() { + return new TextAppearanceInfo(this); + } + } } diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 56524a2c01ef..5740f86b3486 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -21,6 +21,7 @@ import static android.widget.TextView.ACCESSIBILITY_ACTION_SMART_START_ID; import android.R; import android.animation.ValueAnimator; +import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -37,6 +38,7 @@ import android.content.UndoOperation; import android.content.UndoOwner; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; @@ -49,8 +51,10 @@ import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.RenderNode; +import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.fonts.FontStyle; import android.os.Build; import android.os.Bundle; import android.os.LocaleList; @@ -4762,8 +4766,41 @@ public class Editor { } if (includeTextAppearance) { - TextAppearanceInfo textAppearanceInfo = new TextAppearanceInfo(mTextView); - builder.setTextAppearanceInfo(textAppearanceInfo); + Typeface typeface = mTextView.getPaint().getTypeface(); + String systemFontFamilyName = null; + int textFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED; + if (typeface != null) { + systemFontFamilyName = typeface.getSystemFontFamilyName(); + textFontWeight = typeface.getWeight(); + } + ColorStateList linkTextColors = mTextView.getLinkTextColors(); + @ColorInt int linkTextColor = linkTextColors != null + ? linkTextColors.getDefaultColor() : 0; + + TextAppearanceInfo.Builder appearanceBuilder = new TextAppearanceInfo.Builder(); + appearanceBuilder.setTextSize(mTextView.getTextSize()) + .setTextLocales(mTextView.getTextLocales()) + .setSystemFontFamilyName(systemFontFamilyName) + .setTextFontWeight(textFontWeight) + .setTextStyle(mTextView.getTypefaceStyle()) + .setAllCaps(mTextView.isAllCaps()) + .setShadowDx(mTextView.getShadowDx()) + .setShadowDy(mTextView.getShadowDy()) + .setShadowRadius(mTextView.getShadowRadius()) + .setShadowColor(mTextView.getShadowColor()) + .setElegantTextHeight(mTextView.isElegantTextHeight()) + .setFallbackLineSpacing(mTextView.isFallbackLineSpacing()) + .setLetterSpacing(mTextView.getLetterSpacing()) + .setFontFeatureSettings(mTextView.getFontFeatureSettings()) + .setFontVariationSettings(mTextView.getFontVariationSettings()) + .setLineBreakStyle(mTextView.getLineBreakStyle()) + .setLineBreakWordStyle(mTextView.getLineBreakWordStyle()) + .setTextScaleX(mTextView.getTextScaleX()) + .setHighlightTextColor(mTextView.getHighlightColor()) + .setTextColor(mTextView.getCurrentTextColor()) + .setHintTextColor(mTextView.getCurrentHintTextColor()) + .setLinkTextColor(linkTextColor); + builder.setTextAppearanceInfo(appearanceBuilder.build()); } imm.updateCursorAnchorInfo(mTextView, builder.build()); diff --git a/core/java/android/window/IBackAnimationFinishedCallback.aidl b/core/java/android/window/IBackAnimationFinishedCallback.aidl index 8afc003256ed..f034339add6e 100644 --- a/core/java/android/window/IBackAnimationFinishedCallback.aidl +++ b/core/java/android/window/IBackAnimationFinishedCallback.aidl @@ -22,6 +22,6 @@ package android.window; * @param trigger Whether the back gesture has passed the triggering threshold. * {@hide} */ -oneway interface IBackAnimationFinishedCallback { +interface IBackAnimationFinishedCallback { void onAnimationFinished(in boolean triggerBack); }
\ No newline at end of file diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java index 4248096f307d..395073941930 100644 --- a/core/java/android/window/SurfaceSyncGroup.java +++ b/core/java/android/window/SurfaceSyncGroup.java @@ -57,12 +57,13 @@ import java.util.function.Supplier; * option is provided. * * The following is what happens within the {@link SurfaceSyncGroup} - * 1. Each SyncTarget will get a {@link SyncTarget#onReadyToSync} callback that contains a - * {@link SyncBufferCallback}. - * 2. Each {@link SyncTarget} needs to invoke {@link SyncBufferCallback#onBufferReady(Transaction)}. - * This makes sure the SurfaceSyncGroup knows when the SyncTarget is complete, allowing the - * SurfaceSyncGroup to get the Transaction that contains the buffer. - * 3. When the final SyncBufferCallback finishes for the SurfaceSyncGroup, in most cases the + * 1. Each SyncTarget will get a {@link SyncTarget#onAddedToSyncGroup} callback that contains a + * {@link TransactionReadyCallback}. + * 2. Each {@link SyncTarget} needs to invoke + * {@link TransactionReadyCallback#onTransactionReady(Transaction)}. This makes sure the + * SurfaceSyncGroup knows when the SyncTarget is complete, allowing the SurfaceSyncGroup to get the + * Transaction that contains the buffer. + * 3. When the final TransactionReadyCallback finishes for the SurfaceSyncGroup, in most cases the * transaction is applied and then the sync complete callbacks are invoked, letting the callers know * the sync is now complete. * @@ -86,8 +87,6 @@ public final class SurfaceSyncGroup { private final Transaction mTransaction = sTransactionFactory.get(); @GuardedBy("mLock") private boolean mSyncReady; - @GuardedBy("mLock") - private final Set<SyncTarget> mSyncTargets = new ArraySet<>(); @GuardedBy("mLock") private Consumer<Transaction> mSyncRequestCompleteCallback; @@ -197,14 +196,13 @@ public final class SurfaceSyncGroup { * Add a {@link SyncTarget} to a sync set. The sync set will wait for all * SyncableSurfaces to complete before notifying. * - * @param syncTarget A SyncableSurface that implements how to handle syncing - * buffers. + * @param syncTarget A SyncTarget that implements how to handle syncing transactions. * @return true if the SyncTarget was successfully added to the SyncGroup, false otherwise. */ public boolean addToSync(SyncTarget syncTarget) { - SyncBufferCallback syncBufferCallback = new SyncBufferCallback() { + TransactionReadyCallback transactionReadyCallback = new TransactionReadyCallback() { @Override - public void onBufferReady(Transaction t) { + public void onTransactionReady(Transaction t) { synchronized (mLock) { if (t != null) { mTransaction.merge(t); @@ -221,10 +219,9 @@ public final class SurfaceSyncGroup { + "SyncTargets can be added."); return false; } - mPendingSyncs.add(syncBufferCallback.hashCode()); - mSyncTargets.add(syncTarget); + mPendingSyncs.add(transactionReadyCallback.hashCode()); } - syncTarget.onReadyToSync(syncBufferCallback); + syncTarget.onAddedToSyncGroup(this, transactionReadyCallback); return true; } @@ -256,17 +253,13 @@ public final class SurfaceSyncGroup { Log.d(TAG, "Successfully finished sync id=" + this); } - for (SyncTarget syncTarget : mSyncTargets) { - syncTarget.onSyncComplete(); - } - mSyncTargets.clear(); mSyncRequestCompleteCallback.accept(mTransaction); mFinished = true; } /** * Add a Transaction to this sync set. This allows the caller to provide other info that - * should be synced with the buffers. + * should be synced with the transactions. */ public void addTransactionToSync(Transaction t) { synchronized (mLock) { @@ -334,9 +327,10 @@ public final class SurfaceSyncGroup { } @Override - public void onReadyToSync(SyncBufferCallback syncBufferCallback) { + public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, + TransactionReadyCallback transactionReadyCallback) { mFrameCallbackConsumer.accept( - () -> mSurfaceView.syncNextFrame(syncBufferCallback::onBufferReady)); + () -> mSurfaceView.syncNextFrame(transactionReadyCallback::onTransactionReady)); } } @@ -345,22 +339,19 @@ public final class SurfaceSyncGroup { */ public interface SyncTarget { /** - * Called when the Syncable is ready to begin handing a sync request. When invoked, the - * implementor is required to call {@link SyncBufferCallback#onBufferReady(Transaction)} - * and {@link SyncBufferCallback#onBufferReady(Transaction)} in order for this Syncable - * to be marked as complete. + * Called when the SyncTarget has been added to a SyncGroup as is ready to begin handing a + * sync request. When invoked, the implementor is required to call + * {@link TransactionReadyCallback#onTransactionReady(Transaction)} in order for this + * SurfaceSyncGroup to fully complete. * * Always invoked on the thread that initiated the call to {@link #addToSync(SyncTarget)} * - * @param syncBufferCallback A SyncBufferCallback that the caller must invoke onBufferReady - */ - void onReadyToSync(SyncBufferCallback syncBufferCallback); - - /** - * There's no guarantee about the thread this callback is invoked on. + * @param parentSyncGroup The sync group this target has been added to. + * @param transactionReadyCallback A TransactionReadyCallback that the caller must invoke + * onTransactionReady */ - default void onSyncComplete() { - } + void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, + TransactionReadyCallback transactionReadyCallback); } /** @@ -368,14 +359,14 @@ public final class SurfaceSyncGroup { * completed. The caller should invoke the calls when the rendering has started and finished a * frame. */ - public interface SyncBufferCallback { + public interface TransactionReadyCallback { /** - * Invoked when the transaction contains the buffer and is ready to sync. + * Invoked when the transaction is ready to sync. * - * @param t The transaction that contains the buffer to be synced. This can be null if - * there's nothing to sync + * @param t The transaction that contains the anything to be included in the synced. This + * can be null if there's nothing to sync */ - void onBufferReady(@Nullable Transaction t); + void onTransactionReady(@Nullable Transaction t); } /** diff --git a/core/java/android/window/TaskFragmentInfo.java b/core/java/android/window/TaskFragmentInfo.java index e2c8a31cc987..dc60eddaf7db 100644 --- a/core/java/android/window/TaskFragmentInfo.java +++ b/core/java/android/window/TaskFragmentInfo.java @@ -83,6 +83,12 @@ public final class TaskFragmentInfo implements Parcelable { private final boolean mIsTaskFragmentClearedForPip; /** + * Whether the last running activity of the TaskFragment was removed because it was reordered to + * front of the Task. + */ + private final boolean mIsClearedForReorderActivityToFront; + + /** * The maximum {@link ActivityInfo.WindowLayout#minWidth} and * {@link ActivityInfo.WindowLayout#minHeight} aggregated from the TaskFragment's child * activities. @@ -96,7 +102,7 @@ public final class TaskFragmentInfo implements Parcelable { @NonNull Configuration configuration, int runningActivityCount, boolean isVisible, @NonNull List<IBinder> activities, @NonNull Point positionInParent, boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip, - @NonNull Point minimumDimensions) { + boolean isClearedForReorderActivityToFront, @NonNull Point minimumDimensions) { mFragmentToken = requireNonNull(fragmentToken); mToken = requireNonNull(token); mConfiguration.setTo(configuration); @@ -106,6 +112,7 @@ public final class TaskFragmentInfo implements Parcelable { mPositionInParent.set(positionInParent); mIsTaskClearedForReuse = isTaskClearedForReuse; mIsTaskFragmentClearedForPip = isTaskFragmentClearedForPip; + mIsClearedForReorderActivityToFront = isClearedForReorderActivityToFront; mMinimumDimensions.set(minimumDimensions); } @@ -160,6 +167,11 @@ public final class TaskFragmentInfo implements Parcelable { return mIsTaskFragmentClearedForPip; } + /** @hide */ + public boolean isClearedForReorderActivityToFront() { + return mIsClearedForReorderActivityToFront; + } + @WindowingMode public int getWindowingMode() { return mConfiguration.windowConfiguration.getWindowingMode(); @@ -207,6 +219,7 @@ public final class TaskFragmentInfo implements Parcelable { && mPositionInParent.equals(that.mPositionInParent) && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse && mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip + && mIsClearedForReorderActivityToFront == that.mIsClearedForReorderActivityToFront && mMinimumDimensions.equals(that.mMinimumDimensions); } @@ -220,6 +233,7 @@ public final class TaskFragmentInfo implements Parcelable { mPositionInParent.readFromParcel(in); mIsTaskClearedForReuse = in.readBoolean(); mIsTaskFragmentClearedForPip = in.readBoolean(); + mIsClearedForReorderActivityToFront = in.readBoolean(); mMinimumDimensions.readFromParcel(in); } @@ -235,6 +249,7 @@ public final class TaskFragmentInfo implements Parcelable { mPositionInParent.writeToParcel(dest, flags); dest.writeBoolean(mIsTaskClearedForReuse); dest.writeBoolean(mIsTaskFragmentClearedForPip); + dest.writeBoolean(mIsClearedForReorderActivityToFront); mMinimumDimensions.writeToParcel(dest, flags); } @@ -262,8 +277,9 @@ public final class TaskFragmentInfo implements Parcelable { + " activities=" + mActivities + " positionInParent=" + mPositionInParent + " isTaskClearedForReuse=" + mIsTaskClearedForReuse - + " isTaskFragmentClearedForPip" + mIsTaskFragmentClearedForPip - + " minimumDimensions" + mMinimumDimensions + + " isTaskFragmentClearedForPip=" + mIsTaskFragmentClearedForPip + + " mIsClearedForReorderActivityToFront=" + mIsClearedForReorderActivityToFront + + " minimumDimensions=" + mMinimumDimensions + "}"; } diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index c7a2d24a8c94..fc64eb9c35c5 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -728,6 +728,29 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Sets the TaskFragment {@code container} to have a companion TaskFragment {@code companion}. + * This indicates that the organizer will remove the TaskFragment when the companion + * TaskFragment is removed. + * + * @param container the TaskFragment container + * @param companion the companion TaskFragment. If it is {@code null}, the transaction will + * reset the companion TaskFragment. + * @hide + */ + @NonNull + public WindowContainerTransaction setCompanionTaskFragment(@NonNull IBinder container, + @Nullable IBinder companion) { + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder( + HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT) + .setContainer(container) + .setReparentContainer(companion) + .build(); + mHierarchyOps.add(hierarchyOp); + return this; + } + + /** * Sets/removes the always on top flag for this {@code windowContainer}. See * {@link com.android.server.wm.ConfigurationContainer#setAlwaysOnTop(boolean)}. * Please note that this method is only intended to be used for a @@ -1205,6 +1228,7 @@ public final class WindowContainerTransaction implements Parcelable { public static final int HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP = 19; public static final int HIERARCHY_OP_TYPE_REMOVE_TASK = 20; public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 21; + public static final int HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 22; // The following key(s) are for use with mLaunchOptions: // When launching a task (eg. from recents), this is the taskId to be launched. @@ -1419,6 +1443,11 @@ public final class WindowContainerTransaction implements Parcelable { } @NonNull + public IBinder getCompanionContainer() { + return mReparent; + } + + @NonNull public IBinder getCallingActivity() { return mReparent; } @@ -1528,6 +1557,9 @@ public final class WindowContainerTransaction implements Parcelable { return "{RemoveTask: task=" + mContainer + "}"; case HIERARCHY_OP_TYPE_FINISH_ACTIVITY: return "{finishActivity: activity=" + mContainer + "}"; + case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: + return "{setCompanionTaskFragment: container = " + mContainer + " companion = " + + mReparent + "}"; default: return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent + " mToTop=" + mToTop diff --git a/core/java/android/window/WindowProvider.java b/core/java/android/window/WindowProvider.java index b078b9362b90..dbdc68fbfd44 100644 --- a/core/java/android/window/WindowProvider.java +++ b/core/java/android/window/WindowProvider.java @@ -15,8 +15,10 @@ */ package android.window; +import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Bundle; +import android.os.IBinder; import android.view.WindowManager.LayoutParams.WindowType; /** @@ -36,4 +38,11 @@ public interface WindowProvider { /** Gets the launch options of this provider */ @Nullable Bundle getWindowContextOptions(); + + /** + * Gets the WindowContextToken of this provider. + * @see android.content.Context#getWindowContextToken + */ + @NonNull + IBinder getWindowContextToken(); } diff --git a/core/java/android/window/WindowProviderService.java b/core/java/android/window/WindowProviderService.java index 2d2c8de72646..fdc3e5af8d8b 100644 --- a/core/java/android/window/WindowProviderService.java +++ b/core/java/android/window/WindowProviderService.java @@ -27,7 +27,10 @@ import android.annotation.UiContext; import android.app.ActivityThread; import android.app.LoadedApk; import android.app.Service; +import android.content.ComponentCallbacks; +import android.content.ComponentCallbacksController; import android.content.Context; +import android.content.res.Configuration; import android.hardware.display.DisplayManager; import android.os.Bundle; import android.os.IBinder; @@ -54,6 +57,8 @@ public abstract class WindowProviderService extends Service implements WindowPro private final WindowContextController mController = new WindowContextController(mWindowToken); private WindowManager mWindowManager; private boolean mInitialized; + private final ComponentCallbacksController mCallbacksController = + new ComponentCallbacksController(); /** * Returns {@code true} if the {@code windowContextOptions} declares that it is a @@ -118,6 +123,48 @@ public abstract class WindowProviderService extends Service implements WindowPro return mOptions; } + @SuppressLint({"OnNameExpected", "ExecutorRegistration"}) + // Suppress lint because this is a legacy named function and doesn't have an optional param + // for executor. + // TODO(b/259347943): Update documentation for U. + /** + * Here we override to prevent WindowProviderService from invoking + * {@link Application.registerComponentCallback}, which will result in callback registered + * for process-level Configuration change updates. + */ + @Override + public void registerComponentCallbacks(@NonNull ComponentCallbacks callback) { + // For broadcasting Configuration Changes. + mCallbacksController.registerCallbacks(callback); + } + + @SuppressLint("OnNameExpected") + @Override + public void unregisterComponentCallbacks(@NonNull ComponentCallbacks callback) { + mCallbacksController.unregisterCallbacks(callback); + } + + @SuppressLint("OnNameExpected") + @Override + public void onConfigurationChanged(@Nullable Configuration configuration) { + // This is only called from WindowTokenClient. + mCallbacksController.dispatchConfigurationChanged(configuration); + } + + /** + * Override {@link Service}'s empty implementation and listen to {@link ActivityThread} for + * low memory and trim memory events. + */ + @Override + public void onLowMemory() { + mCallbacksController.dispatchLowMemory(); + } + + @Override + public void onTrimMemory(int level) { + mCallbacksController.dispatchTrimMemory(level); + } + /** * Returns the display ID to launch this {@link WindowProviderService}. * @@ -181,5 +228,6 @@ public abstract class WindowProviderService extends Service implements WindowPro public void onDestroy() { super.onDestroy(); mController.detachIfNeeded(); + mCallbacksController.clearCallbacks(); } } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index e926605ae88a..a237e986f1d7 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -492,6 +492,12 @@ public class ResolverActivity extends Activity implements /* workProfileUserHandle= */ null); } + private UserHandle getIntentUser() { + return getIntent().hasExtra(EXTRA_CALLING_USER) + ? getIntent().getParcelableExtra(EXTRA_CALLING_USER, android.os.UserHandle.class) + : getUser(); + } + private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles( Intent[] initialIntents, List<ResolveInfo> rList, @@ -500,9 +506,7 @@ public class ResolverActivity extends Activity implements // the intent resolver is started in the other profile. Since this is the only case when // this happens, we check for it here and set the current profile's tab. int selectedProfile = getCurrentProfile(); - UserHandle intentUser = getIntent().hasExtra(EXTRA_CALLING_USER) - ? getIntent().getParcelableExtra(EXTRA_CALLING_USER, android.os.UserHandle.class) - : getUser(); + UserHandle intentUser = getIntentUser(); if (!getUser().equals(intentUser)) { if (getPersonalProfileUserHandle().equals(intentUser)) { selectedProfile = PROFILE_PERSONAL; diff --git a/core/java/com/android/internal/expresslog/OWNERS b/core/java/com/android/internal/expresslog/OWNERS new file mode 100644 index 000000000000..ee865b1e4ec8 --- /dev/null +++ b/core/java/com/android/internal/expresslog/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/stats/OWNERS diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 09f099120812..614f96255acb 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -91,7 +91,6 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.UiThread; import android.annotation.WorkerThread; -import android.app.ActivityThread; import android.content.Context; import android.os.Build; import android.os.Handler; @@ -417,7 +416,6 @@ public class InteractionJankMonitor { public InteractionJankMonitor(@NonNull HandlerThread worker) { // Check permission early. DeviceConfig.enforceReadPermission( - ActivityThread.currentApplication().getApplicationContext(), DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR); mRunningTrackers = new SparseArray<>(); diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index 696f0ffba518..556e1467c7df 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -403,7 +403,7 @@ public class BatteryStatsHistory { * Returns true if this instance only supports reading history. */ public boolean isReadOnly() { - return mActiveFile == null; + return mActiveFile == null || mHistoryDir == null; } /** @@ -1292,7 +1292,9 @@ public class BatteryStatsHistory { && mHistoryLastWritten.batteryHealth == cur.batteryHealth && mHistoryLastWritten.batteryPlugType == cur.batteryPlugType && mHistoryLastWritten.batteryTemperature == cur.batteryTemperature - && mHistoryLastWritten.batteryVoltage == cur.batteryVoltage) { + && mHistoryLastWritten.batteryVoltage == cur.batteryVoltage + && mHistoryLastWritten.measuredEnergyDetails == null + && mHistoryLastWritten.cpuUsageDetails == null) { // We can merge this new change in with the last one. Merging is // allowed as long as only the states have changed, and within those states // as long as no bit has changed both between now and the last entry, as @@ -1761,8 +1763,8 @@ public class BatteryStatsHistory { * Saves the accumulated history buffer in the active file, see {@link #getActiveFile()} . */ public void writeHistory() { - if (mActiveFile == null) { - Slog.w(TAG, "writeHistory: no history file associated with this instance"); + if (isReadOnly()) { + Slog.w(TAG, "writeHistory: this instance instance is read-only"); return; } diff --git a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl index 9be686a772c1..a1ad5d5d5039 100644 --- a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl +++ b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl @@ -26,5 +26,7 @@ package com.android.internal.os; interface IBinaryTransparencyService { String getSignedImageInfo(); - Map getApexInfo(); + List getApexInfo(); + + List getMeasurementsForAllPackages(); }
\ No newline at end of file diff --git a/core/java/com/android/internal/os/ProcLocksReader.java b/core/java/com/android/internal/os/ProcLocksReader.java index 2143bc1b4532..9ddb8c75f5c8 100644 --- a/core/java/com/android/internal/os/ProcLocksReader.java +++ b/core/java/com/android/internal/os/ProcLocksReader.java @@ -16,6 +16,8 @@ package com.android.internal.os; +import android.util.IntArray; + import com.android.internal.util.ProcFileReader; import java.io.FileInputStream; @@ -35,6 +37,7 @@ import java.io.IOException; public class ProcLocksReader { private final String mPath; private ProcFileReader mReader = null; + private IntArray mPids = new IntArray(); public ProcLocksReader() { mPath = "/proc/locks"; @@ -51,9 +54,13 @@ public class ProcLocksReader { public interface ProcLocksReaderCallback { /** * Call the callback function of handleBlockingFileLocks(). - * @param pid Each process that hold file locks blocking other processes. + * @param pids Each process that hold file locks blocking other processes. + * pids[0] is the process blocking others + * pids[1..n-1] are the processes being blocked + * NOTE: pids are cleared immediately after onBlockingFileLock() returns. If the caller + * needs to cache it, please make a copy, e.g. by calling pids.toArray(). */ - void onBlockingFileLock(int pid); + void onBlockingFileLock(IntArray pids); } /** @@ -64,8 +71,7 @@ public class ProcLocksReader { public void handleBlockingFileLocks(ProcLocksReaderCallback callback) throws IOException { long last = -1; long id; // ordinal position of the lock in the list - int owner = -1; // the PID of the process that owns the lock - int pid = -1; // the PID of the process blocking others + int pid = -1; // the PID of the process being blocked if (mReader == null) { mReader = new ProcFileReader(new FileInputStream(mPath)); @@ -73,26 +79,49 @@ public class ProcLocksReader { mReader.rewind(); } + mPids.clear(); while (mReader.hasMoreData()) { id = mReader.nextLong(true); // lock id if (id == last) { - mReader.finishLine(); // blocked lock - if (pid < 0) { - pid = owner; // get pid from the previous line - callback.onBlockingFileLock(pid); + // blocked lock found + mReader.nextIgnored(); // -> + mReader.nextIgnored(); // lock type: POSIX? + mReader.nextIgnored(); // lock type: MANDATORY? + mReader.nextIgnored(); // lock type: RW? + + pid = mReader.nextInt(); // pid + if (pid > 0) { + mPids.add(pid); } - continue; + + mReader.finishLine(); } else { - pid = -1; // a new lock - } + // process blocking lock and move on to a new lock + if (mPids.size() > 1) { + callback.onBlockingFileLock(mPids); + mPids.clear(); + } - mReader.nextIgnored(); // lock type: POSIX? - mReader.nextIgnored(); // lock type: MANDATORY? - mReader.nextIgnored(); // lock type: RW? + // new lock found + mReader.nextIgnored(); // lock type: POSIX? + mReader.nextIgnored(); // lock type: MANDATORY? + mReader.nextIgnored(); // lock type: RW? - owner = mReader.nextInt(); // pid - mReader.finishLine(); - last = id; + pid = mReader.nextInt(); // pid + if (pid > 0) { + if (mPids.size() == 0) { + mPids.add(pid); + } else { + mPids.set(0, pid); + } + } + mReader.finishLine(); + last = id; + } + } + // The last unprocessed blocking lock immediately before EOF + if (mPids.size() > 1) { + callback.onBlockingFileLock(mPids); } } } diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java index 28b98d6fab06..8a9445d8554a 100644 --- a/core/java/com/android/internal/os/RuntimeInit.java +++ b/core/java/com/android/internal/os/RuntimeInit.java @@ -16,10 +16,14 @@ package com.android.internal.os; +import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL; + +import android.annotation.TestApi; import android.app.ActivityManager; import android.app.ActivityThread; import android.app.ApplicationErrorReport; import android.app.IActivityManager; +import android.app.compat.CompatChanges; import android.compat.annotation.UnsupportedAppUsage; import android.content.type.DefaultMimeMapFactory; import android.net.TrafficStats; @@ -36,6 +40,7 @@ import com.android.internal.logging.AndroidConfig; import dalvik.system.RuntimeHooks; import dalvik.system.VMRuntime; +import dalvik.system.ZipPathValidator; import libcore.content.type.MimeMap; @@ -260,10 +265,31 @@ public class RuntimeInit { */ TrafficStats.attachSocketTagger(); + /* + * Initialize the zip path validator callback depending on the targetSdk. + */ + initZipPathValidatorCallback(); + initialized = true; } /** + * If targetSDK >= U: set the safe zip path validator callback which disallows dangerous zip + * entry names. + * Otherwise: clear the callback to the default validation. + * + * @hide + */ + @TestApi + public static void initZipPathValidatorCallback() { + if (CompatChanges.isChangeEnabled(VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL)) { + ZipPathValidator.setCallback(new SafeZipPathValidatorCallback()); + } else { + ZipPathValidator.clearCallback(); + } + } + + /** * Returns an HTTP user agent of the form * "Dalvik/1.1.0 (Linux; U; Android Eclair Build/MAIN)". */ diff --git a/core/java/com/android/internal/os/SafeZipPathValidatorCallback.java b/core/java/com/android/internal/os/SafeZipPathValidatorCallback.java new file mode 100644 index 000000000000..a6ee108eadeb --- /dev/null +++ b/core/java/com/android/internal/os/SafeZipPathValidatorCallback.java @@ -0,0 +1,67 @@ +/* + * 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.internal.os; + +import android.annotation.NonNull; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; +import android.os.Build; + +import dalvik.system.ZipPathValidator; + +import java.io.File; +import java.util.zip.ZipException; + +/** + * A child implementation of the {@link dalvik.system.ZipPathValidator.Callback} that removes the + * risk of zip path traversal vulnerabilities. + * + * @hide + */ +public class SafeZipPathValidatorCallback implements ZipPathValidator.Callback { + /** + * This change targets zip path traversal vulnerabilities by throwing + * {@link java.util.zip.ZipException} if zip path entries contain ".." or start with "/". + * <p> + * The exception will be thrown in {@link java.util.zip.ZipInputStream#getNextEntry} or + * {@link java.util.zip.ZipFile#ZipFile(String)}. + * <p> + * This validation is enabled for apps with targetSDK >= U. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public static final long VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL = 242716250L; + + @Override + public void onZipEntryAccess(@NonNull String path) throws ZipException { + if (path.startsWith("/")) { + throw new ZipException("Invalid zip entry path: " + path); + } + if (path.contains("..")) { + // If the string does contain "..", break it down into its actual name elements to + // ensure it actually contains ".." as a name, not just a name like "foo..bar" or even + // "foo..", which should be fine. + File file = new File(path); + while (file != null) { + if (file.getName().equals("..")) { + throw new ZipException("Invalid zip entry path: " + path); + } + file = file.getParentFile(); + } + } + } +} diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java index be3f1720acdd..680f8fe6535f 100644 --- a/core/java/com/android/internal/os/TimeoutRecord.java +++ b/core/java/com/android/internal/os/TimeoutRecord.java @@ -18,6 +18,7 @@ package com.android.internal.os; import android.annotation.IntDef; import android.annotation.NonNull; +import android.content.Intent; import android.os.SystemClock; import com.android.internal.os.anr.AnrLatencyTracker; @@ -92,7 +93,17 @@ public class TimeoutRecord { /** Record for a broadcast receiver timeout. */ @NonNull - public static TimeoutRecord forBroadcastReceiver(@NonNull String reason) { + public static TimeoutRecord forBroadcastReceiver(@NonNull Intent intent) { + String reason = "Broadcast of " + intent.toString(); + return TimeoutRecord.endingNow(TimeoutKind.BROADCAST_RECEIVER, reason); + } + + /** Record for a broadcast receiver timeout. */ + @NonNull + public static TimeoutRecord forBroadcastReceiver(@NonNull Intent intent, + long timeoutDurationMs) { + String reason = "Broadcast of " + intent.toString() + ", waited " + timeoutDurationMs + + "ms"; return TimeoutRecord.endingNow(TimeoutKind.BROADCAST_RECEIVER, reason); } diff --git a/core/jni/OWNERS b/core/jni/OWNERS index a068008f5e22..9f39c32171d7 100644 --- a/core/jni/OWNERS +++ b/core/jni/OWNERS @@ -99,3 +99,5 @@ per-file com_android_internal_os_*MultiStateCounter* = file:/BATTERY_STATS_OWNER # PM per-file com_android_internal_content_* = file:/PACKAGE_MANAGER_OWNERS +# Stats/expresslog +per-file *expresslog* = file:/services/core/java/com/android/server/stats/OWNERS diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp index 2b932cb37332..98814bf602fc 100644 --- a/core/jni/android_view_InputEventReceiver.cpp +++ b/core/jni/android_view_InputEventReceiver.cpp @@ -380,8 +380,8 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, if (kDebugDispatchCycle) { ALOGD("channel '%s' ~ Received motion event.", getInputChannelName().c_str()); } - MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent); - if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) { + const MotionEvent& motionEvent = static_cast<const MotionEvent&>(*inputEvent); + if ((motionEvent.getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) { *outConsumedBatch = true; } inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent); diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp index a30935b621a2..80df0ead4bcd 100644 --- a/core/jni/android_view_MotionEvent.cpp +++ b/core/jni/android_view_MotionEvent.cpp @@ -82,7 +82,7 @@ static void android_view_MotionEvent_setNativePtr(JNIEnv* env, jobject eventObj, reinterpret_cast<jlong>(event)); } -jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent* event) { +jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent& event) { jobject eventObj = env->CallStaticObjectMethod(gMotionEventClassInfo.clazz, gMotionEventClassInfo.obtain); if (env->ExceptionCheck() || !eventObj) { @@ -98,7 +98,7 @@ jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent* ev android_view_MotionEvent_setNativePtr(env, eventObj, destEvent); } - destEvent->copyFrom(event, true); + destEvent->copyFrom(&event, true); return eventObj; } diff --git a/core/jni/android_view_MotionEvent.h b/core/jni/android_view_MotionEvent.h index 9ce4bf367688..32a280ec1974 100644 --- a/core/jni/android_view_MotionEvent.h +++ b/core/jni/android_view_MotionEvent.h @@ -26,7 +26,7 @@ class MotionEvent; /* Obtains an instance of a DVM MotionEvent object as a copy of a native MotionEvent instance. * Returns NULL on error. */ -extern jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent* event); +extern jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent& event); /* Gets the underlying native MotionEvent instance within a DVM MotionEvent object. * Returns NULL if the event is NULL or if it is uninitialized. */ diff --git a/core/proto/android/app/location_time_zone_manager.proto b/core/proto/android/app/location_time_zone_manager.proto index 5fdcfdf35a37..7037a6c4f68a 100644 --- a/core/proto/android/app/location_time_zone_manager.proto +++ b/core/proto/android/app/location_time_zone_manager.proto @@ -40,7 +40,7 @@ enum ControllerStateEnum { message LocationTimeZoneManagerServiceStateProto { option (android.msg_privacy).dest = DEST_AUTOMATIC; - optional GeolocationTimeZoneSuggestionProto last_suggestion = 1; + optional LocationTimeZoneProviderEventProto last_event = 1; repeated TimeZoneProviderStateProto primary_provider_states = 2; repeated TimeZoneProviderStateProto secondary_provider_states = 3; repeated ControllerStateEnum controller_states = 4; diff --git a/core/proto/android/app/time_zone_detector.proto b/core/proto/android/app/time_zone_detector.proto index b52aa828bef9..cd4a36fafef0 100644 --- a/core/proto/android/app/time_zone_detector.proto +++ b/core/proto/android/app/time_zone_detector.proto @@ -22,13 +22,38 @@ import "frameworks/base/core/proto/android/privacy.proto"; option java_multiple_files = true; option java_outer_classname = "TimeZoneDetectorProto"; -// Represents a GeolocationTimeZoneSuggestion that can be / has been passed to the time zone +// Represents a LocationTimeZoneProviderEvent that can be / has been passed to the time zone // detector. +message LocationTimeZoneProviderEventProto { + option (android.msg_privacy).dest = DEST_AUTOMATIC; + + optional GeolocationTimeZoneSuggestionProto suggestion = 1; + repeated string debug_info = 2; + optional LocationTimeZoneAlgorithmStatusProto algorithm_status = 3; +} + +// Represents a LocationTimeZoneAlgorithmStatus that can be / has been passed to the time zone +// detector. +message LocationTimeZoneAlgorithmStatusProto { + option (android.msg_privacy).dest = DEST_AUTOMATIC; + + optional DetectionAlgorithmStatusEnum status = 1; +} + +// The state enum for detection algorithms. +enum DetectionAlgorithmStatusEnum { + DETECTION_ALGORITHM_STATUS_UNKNOWN = 0; + DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED = 1; + DETECTION_ALGORITHM_STATUS_NOT_RUNNING = 2; + DETECTION_ALGORITHM_STATUS_RUNNING = 3; +} + +// Represents a GeolocationTimeZoneSuggestion that can be contained in a +// LocationTimeZoneProviderEvent. message GeolocationTimeZoneSuggestionProto { option (android.msg_privacy).dest = DEST_AUTOMATIC; repeated string zone_ids = 1; - repeated string debug_info = 2; } /* diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 196ea59fe0f4..e24d667e8600 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3177,10 +3177,10 @@ <permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" android:protectionLevel="signature|privileged|vendorPrivileged|oem|verifier|role"/> - <!-- Allows an application to hint that a broadcast is associated with an - "interactive" usage scenario + <!-- Allows an application to hint that a component lifecycle operation such as sending + a broadcast is associated with an "interactive" usage scenario. @hide --> - <permission android:name="android.permission.BROADCAST_OPTION_INTERACTIVE" + <permission android:name="android.permission.COMPONENT_OPTION_INTERACTIVE" android:protectionLevel="signature|privileged" /> <!-- @SystemApi Must be required by activities that handle the intent action diff --git a/core/res/res/anim/dock_bottom_enter.xml b/core/res/res/anim/dock_bottom_enter.xml deleted file mode 100644 index bfb97b6d9b80..000000000000 --- a/core/res/res/anim/dock_bottom_enter.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* Copyright 2012, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<!-- Animation for when a dock window at the bottom of the screen is entering. --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:interpolator="@android:interpolator/decelerate_cubic"> - <translate android:fromYDelta="100%" android:toYDelta="0" - android:duration="@integer/dock_enter_exit_duration"/> -</set> diff --git a/core/res/res/anim/dock_bottom_exit.xml b/core/res/res/anim/dock_bottom_exit.xml deleted file mode 100644 index 4e15448aba3e..000000000000 --- a/core/res/res/anim/dock_bottom_exit.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* Copyright 2012, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<!-- Animation for when a dock window at the bottom of the screen is exiting. --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:interpolator="@android:interpolator/accelerate_cubic"> - <translate android:fromYDelta="0" android:toYDelta="100%" - android:startOffset="100" android:duration="@integer/dock_enter_exit_duration"/> -</set> diff --git a/core/res/res/anim/dock_bottom_exit_keyguard.xml b/core/res/res/anim/dock_bottom_exit_keyguard.xml deleted file mode 100644 index 4de3ce5b8932..000000000000 --- a/core/res/res/anim/dock_bottom_exit_keyguard.xml +++ /dev/null @@ -1,22 +0,0 @@ -<!-- - ~ Copyright (C) 2016 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License - --> - -<!-- Animation for when a dock window at the bottom of the screen is exiting while on Keyguard --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:interpolator="@android:interpolator/fast_out_linear_in"> - <translate android:fromYDelta="0" android:toYDelta="100%" - android:duration="200"/> -</set>
\ No newline at end of file diff --git a/core/res/res/anim/dock_left_enter.xml b/core/res/res/anim/dock_left_enter.xml deleted file mode 100644 index 7f5dfd50afc7..000000000000 --- a/core/res/res/anim/dock_left_enter.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* Copyright 2012, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<!-- Animation for when a dock window at the left of the screen is entering. --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:interpolator="@android:interpolator/decelerate_cubic"> - <translate android:fromXDelta="-100%" android:toXDelta="0" - android:duration="250"/> -</set> diff --git a/core/res/res/anim/dock_left_exit.xml b/core/res/res/anim/dock_left_exit.xml deleted file mode 100644 index 11cbc0b36405..000000000000 --- a/core/res/res/anim/dock_left_exit.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* Copyright 2012, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<!-- Animation for when a dock window at the right of the screen is exiting. --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:interpolator="@android:interpolator/accelerate_cubic"> - <translate android:fromXDelta="0" android:toXDelta="-100%" - android:startOffset="100" android:duration="250"/> -</set> diff --git a/core/res/res/anim/dock_right_enter.xml b/core/res/res/anim/dock_right_enter.xml deleted file mode 100644 index a92c7d234e58..000000000000 --- a/core/res/res/anim/dock_right_enter.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* Copyright 2012, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<!-- Animation for when a dock window at the right of the screen is entering. --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:interpolator="@android:interpolator/decelerate_cubic"> - <translate android:fromXDelta="100%" android:toXDelta="0" - android:duration="250"/> -</set> diff --git a/core/res/res/anim/dock_right_exit.xml b/core/res/res/anim/dock_right_exit.xml deleted file mode 100644 index 80e4dc318192..000000000000 --- a/core/res/res/anim/dock_right_exit.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* Copyright 2012, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<!-- Animation for when a dock window at the right of the screen is exiting. --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:interpolator="@android:interpolator/accelerate_cubic"> - <translate android:fromXDelta="0" android:toXDelta="100%" - android:startOffset="100" android:duration="250"/> -</set> diff --git a/core/res/res/anim/dock_top_enter.xml b/core/res/res/anim/dock_top_enter.xml deleted file mode 100644 index f763fb5c1cc9..000000000000 --- a/core/res/res/anim/dock_top_enter.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* Copyright 2007, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<!-- Animation for when a dock window at the top of the screen is entering. --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:interpolator="@android:interpolator/decelerate_cubic"> - <translate android:fromYDelta="-100%" android:toYDelta="0" - android:duration="@integer/dock_enter_exit_duration"/> -</set> diff --git a/core/res/res/anim/dock_top_exit.xml b/core/res/res/anim/dock_top_exit.xml deleted file mode 100644 index 995b7d0ffc8c..000000000000 --- a/core/res/res/anim/dock_top_exit.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* Copyright 2007, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<!-- Animation for when a dock window at the top of the screen is exiting. --> -<set xmlns:android="http://schemas.android.com/apk/res/android" - android:interpolator="@android:interpolator/accelerate_cubic"> - <translate android:fromYDelta="0" android:toYDelta="-100%" - android:startOffset="100" android:duration="@integer/dock_enter_exit_duration"/> -</set> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 932b24ec08c9..6c18259c031c 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3688,6 +3688,12 @@ experience while the device is non-interactive. --> <bool name="config_emergencyGestureEnabled">true</bool> + <!-- Default value for Use Emergency SOS in Settings false = disabled, true = enabled --> + <bool name="config_defaultEmergencyGestureEnabled">true</bool> + + <!-- Default value for Use Play countdown alarm in Settings false = disabled, true = enabled --> + <bool name="config_defaultEmergencyGestureSoundEnabled">false</bool> + <!-- Allow the gesture power + volume up to change the ringer mode while the device is interactive. --> <bool name="config_volumeHushGestureEnabled">true</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 89ec5ba48142..5811ed9b591f 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1691,15 +1691,6 @@ <!-- From android.policy --> <java-symbol type="anim" name="app_starting_exit" /> - <java-symbol type="anim" name="dock_top_enter" /> - <java-symbol type="anim" name="dock_top_exit" /> - <java-symbol type="anim" name="dock_bottom_enter" /> - <java-symbol type="anim" name="dock_bottom_exit" /> - <java-symbol type="anim" name="dock_bottom_exit_keyguard" /> - <java-symbol type="anim" name="dock_left_enter" /> - <java-symbol type="anim" name="dock_left_exit" /> - <java-symbol type="anim" name="dock_right_enter" /> - <java-symbol type="anim" name="dock_right_exit" /> <java-symbol type="anim" name="fade_in" /> <java-symbol type="anim" name="fade_out" /> <java-symbol type="anim" name="voice_activity_close_exit" /> @@ -3024,6 +3015,8 @@ <java-symbol type="integer" name="config_cameraLiftTriggerSensorType" /> <java-symbol type="string" name="config_cameraLiftTriggerSensorStringType" /> <java-symbol type="bool" name="config_emergencyGestureEnabled" /> + <java-symbol type="bool" name="config_defaultEmergencyGestureEnabled" /> + <java-symbol type="bool" name="config_defaultEmergencyGestureSoundEnabled" /> <java-symbol type="bool" name="config_volumeHushGestureEnabled" /> <java-symbol type="drawable" name="platlogo_m" /> diff --git a/core/tests/coretests/src/android/app/activity/BroadcastTest.java b/core/tests/coretests/src/android/app/activity/BroadcastTest.java index 7e875ada7267..10452fd64430 100644 --- a/core/tests/coretests/src/android/app/activity/BroadcastTest.java +++ b/core/tests/coretests/src/android/app/activity/BroadcastTest.java @@ -541,35 +541,35 @@ public class BroadcastTest extends ActivityTestsBase { public void testBroadcastOption_interactive() throws Exception { final BroadcastOptions options = BroadcastOptions.makeBasic(); - options.setInteractiveBroadcast(true); + options.setInteractive(true); final Intent intent = makeBroadcastIntent(BROADCAST_REGISTERED); try { getContext().sendBroadcast(intent, null, options.toBundle()); - fail("No exception thrown with BroadcastOptions.setInteractiveBroadcast(true)"); + fail("No exception thrown with BroadcastOptions.setInteractive(true)"); } catch (SecurityException se) { // Expected, correct behavior - this case intentionally empty } catch (Exception e) { fail("Unexpected exception " + e.getMessage() - + " thrown with BroadcastOptions.setInteractiveBroadcast(true)"); + + " thrown with BroadcastOptions.setInteractive(true)"); } } public void testBroadcastOption_interactive_PendingIntent() throws Exception { final BroadcastOptions options = BroadcastOptions.makeBasic(); - options.setInteractiveBroadcast(true); + options.setInteractive(true); final Intent intent = makeBroadcastIntent(BROADCAST_REGISTERED); PendingIntent brPending = PendingIntent.getBroadcast(getContext(), 1, intent, PendingIntent.FLAG_IMMUTABLE); try { brPending.send(getContext(), 1, null, null, null, null, options.toBundle()); - fail("No exception thrown with BroadcastOptions.setInteractiveBroadcast(true)"); + fail("No exception thrown with BroadcastOptions.setInteractive(true)"); } catch (SecurityException se) { // Expected, correct behavior - this case intentionally empty } catch (Exception e) { fail("Unexpected exception " + e.getMessage() - + " thrown with BroadcastOptions.setInteractiveBroadcast(true)"); + + " thrown with BroadcastOptions.setInteractive(true)"); } finally { brPending.cancel(); } diff --git a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java index 37cf4700c1d0..4d5b0d2d4ae7 100644 --- a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java +++ b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java @@ -46,6 +46,7 @@ import java.util.Set; public class BackupAgentTest { // An arbitrary user. private static final UserHandle USER_HANDLE = new UserHandle(15); + private static final String DATA_TYPE_BACKED_UP = "test data type"; @Mock FullBackup.BackupScheme mBackupScheme; @@ -73,6 +74,42 @@ public class BackupAgentTest { assertThat(rules).isEqualTo(expectedRules); } + @Test + public void getBackupRestoreEventLogger_beforeOnCreate_isNull() { + BackupAgent agent = new TestFullBackupAgent(); + + assertThat(agent.getBackupRestoreEventLogger()).isNull(); + } + + @Test + public void getBackupRestoreEventLogger_afterOnCreateForBackup_initializedForBackup() { + BackupAgent agent = new TestFullBackupAgent(); + agent.onCreate(USER_HANDLE, OperationType.BACKUP); // TODO: pass in new operation type + + assertThat(agent.getBackupRestoreEventLogger().getOperationType()).isEqualTo(1); + } + + @Test + public void getBackupRestoreEventLogger_afterOnCreateForRestore_initializedForRestore() { + BackupAgent agent = new TestFullBackupAgent(); + agent.onCreate(USER_HANDLE, OperationType.BACKUP); // TODO: pass in new operation type + + assertThat(agent.getBackupRestoreEventLogger().getOperationType()).isEqualTo(1); + } + + @Test + public void getBackupRestoreEventLogger_afterBackup_containsLogsLoggedByAgent() + throws Exception { + BackupAgent agent = new TestFullBackupAgent(); + agent.onCreate(USER_HANDLE, OperationType.BACKUP); // TODO: pass in new operation type + + // TestFullBackupAgent logs DATA_TYPE_BACKED_UP when onFullBackup is called. + agent.onFullBackup(new FullBackupDataOutput(/* quota = */ 0)); + + assertThat(agent.getBackupRestoreEventLogger().getLoggingResults().get(0).getDataType()) + .isEqualTo(DATA_TYPE_BACKED_UP); + } + private BackupAgent getAgentForOperationType(@OperationType int operationType) { BackupAgent agent = new TestFullBackupAgent(); agent.onCreate(USER_HANDLE, operationType); @@ -88,6 +125,11 @@ public class BackupAgentTest { } @Override + public void onFullBackup(FullBackupDataOutput data) { + getBackupRestoreEventLogger().logItemsBackedUp(DATA_TYPE_BACKED_UP, 1); + } + + @Override public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException { // Left empty as this is a full backup agent. diff --git a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java index bbd2ef38d786..b9fdc6d2aa23 100644 --- a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java +++ b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java @@ -25,6 +25,7 @@ import static junit.framework.Assert.fail; import android.app.backup.BackupRestoreEventLogger.BackupRestoreDataType; import android.app.backup.BackupRestoreEventLogger.DataTypeResult; +import android.os.Parcel; import android.platform.test.annotations.Presubmit; import androidx.test.runner.AndroidJUnit4; @@ -35,6 +36,7 @@ import org.junit.runner.RunWith; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -236,10 +238,10 @@ public class BackupRestoreEventLoggerTest { mLogger.logItemsBackupFailed(DATA_TYPE_1, firstCount, ERROR_1); mLogger.logItemsBackupFailed(DATA_TYPE_1, secondCount, ERROR_2); - int firstErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1) - .getErrors().get(ERROR_1); - int secondErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1) - .getErrors().get(ERROR_2); + int firstErrorTypeCount = + getResultForDataType(mLogger, DATA_TYPE_1).getErrors().get(ERROR_1); + int secondErrorTypeCount = + getResultForDataType(mLogger, DATA_TYPE_1).getErrors().get(ERROR_2); assertThat(firstErrorTypeCount).isEqualTo(firstCount); assertThat(secondErrorTypeCount).isEqualTo(secondCount); } @@ -253,16 +255,54 @@ public class BackupRestoreEventLoggerTest { mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstCount, ERROR_1); mLogger.logItemsRestoreFailed(DATA_TYPE_1, secondCount, ERROR_2); - int firstErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1) - .getErrors().get(ERROR_1); - int secondErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1) - .getErrors().get(ERROR_2); + int firstErrorTypeCount = + getResultForDataType(mLogger, DATA_TYPE_1).getErrors().get(ERROR_1); + int secondErrorTypeCount = + getResultForDataType(mLogger, DATA_TYPE_1).getErrors().get(ERROR_2); assertThat(firstErrorTypeCount).isEqualTo(firstCount); assertThat(secondErrorTypeCount).isEqualTo(secondCount); } - private static DataTypeResult getResultForDataType(BackupRestoreEventLogger logger, - @BackupRestoreDataType String dataType) { + @Test + public void testGetLoggingResults_resultsParceledAndUnparceled_recreatedCorrectly() { + mLogger = new BackupRestoreEventLogger(RESTORE); + int firstTypeSuccessCount = 1; + int firstTypeErrorOneCount = 2; + int firstTypeErrorTwoCount = 3; + mLogger.logItemsRestored(DATA_TYPE_1, firstTypeSuccessCount); + mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstTypeErrorOneCount, ERROR_1); + mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstTypeErrorTwoCount, ERROR_2); + mLogger.logRestoreMetadata(DATA_TYPE_1, METADATA_1); + int secondTypeSuccessCount = 4; + int secondTypeErrorOneCount = 5; + mLogger.logItemsRestored(DATA_TYPE_2, secondTypeSuccessCount); + mLogger.logItemsRestoreFailed(DATA_TYPE_2, secondTypeErrorOneCount, ERROR_1); + + List<DataTypeResult> resultsList = mLogger.getLoggingResults(); + Parcel parcel = Parcel.obtain(); + + parcel.writeParcelableList(resultsList, /* flags= */ 0); + + parcel.setDataPosition(0); + List<DataTypeResult> recreatedList = new ArrayList<>(); + parcel.readParcelableList( + recreatedList, DataTypeResult.class.getClassLoader(), DataTypeResult.class); + + assertThat(recreatedList.get(0).getDataType()).isEqualTo(DATA_TYPE_1); + assertThat(recreatedList.get(0).getSuccessCount()).isEqualTo(firstTypeSuccessCount); + assertThat(recreatedList.get(0).getFailCount()) + .isEqualTo(firstTypeErrorOneCount + firstTypeErrorTwoCount); + assertThat(recreatedList.get(0).getErrors().get(ERROR_1)).isEqualTo(firstTypeErrorOneCount); + assertThat(recreatedList.get(0).getErrors().get(ERROR_2)).isEqualTo(firstTypeErrorTwoCount); + assertThat(recreatedList.get(1).getDataType()).isEqualTo(DATA_TYPE_2); + assertThat(recreatedList.get(1).getSuccessCount()).isEqualTo(secondTypeSuccessCount); + assertThat(recreatedList.get(1).getFailCount()).isEqualTo(secondTypeErrorOneCount); + assertThat(recreatedList.get(1).getErrors().get(ERROR_1)) + .isEqualTo(secondTypeErrorOneCount); + } + + private static DataTypeResult getResultForDataType( + BackupRestoreEventLogger logger, @BackupRestoreDataType String dataType) { Optional<DataTypeResult> result = getResultForDataTypeIfPresent(logger, dataType); if (result.isEmpty()) { fail("Failed to find result for data type: " + dataType); @@ -273,8 +313,9 @@ public class BackupRestoreEventLoggerTest { private static Optional<DataTypeResult> getResultForDataTypeIfPresent( BackupRestoreEventLogger logger, @BackupRestoreDataType String dataType) { List<DataTypeResult> resultList = logger.getLoggingResults(); - return resultList.stream().filter( - dataTypeResult -> dataTypeResult.getDataType().equals(dataType)).findAny(); + return resultList.stream() + .filter(dataTypeResult -> dataTypeResult.getDataType().equals(dataType)) + .findAny(); } private byte[] getMetaDataHash(String metaData) { diff --git a/core/tests/coretests/src/android/app/time/DetectorStatusTypesTest.java b/core/tests/coretests/src/android/app/time/DetectorStatusTypesTest.java new file mode 100644 index 000000000000..f57ee43b76c8 --- /dev/null +++ b/core/tests/coretests/src/android/app/time/DetectorStatusTypesTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_UNKNOWN; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_UNKNOWN; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus; +import android.app.time.DetectorStatusTypes.DetectorStatus; + +import org.junit.Test; + +public class DetectorStatusTypesTest { + + @Test + public void testRequireValidDetectionAlgorithmStatus() { + for (@DetectionAlgorithmStatus int status = DETECTION_ALGORITHM_STATUS_UNKNOWN; + status <= DETECTION_ALGORITHM_STATUS_RUNNING; status++) { + assertEquals(status, DetectorStatusTypes.requireValidDetectionAlgorithmStatus(status)); + } + + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.requireValidDetectionAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_UNKNOWN - 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.requireValidDetectionAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING + 1)); + } + + @Test + public void testFormatAndParseDetectionAlgorithmStatus() { + for (@DetectionAlgorithmStatus int status = DETECTION_ALGORITHM_STATUS_UNKNOWN; + status <= DETECTION_ALGORITHM_STATUS_RUNNING; status++) { + assertEquals(status, DetectorStatusTypes.detectionAlgorithmStatusFromString( + DetectorStatusTypes.detectionAlgorithmStatusToString(status))); + } + + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusToString( + DETECTION_ALGORITHM_STATUS_UNKNOWN - 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusToString( + DETECTION_ALGORITHM_STATUS_RUNNING + 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString(null)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString("")); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString("FOO")); + } + + @Test + public void testRequireValidDetectorStatus() { + for (@DetectorStatus int status = DETECTOR_STATUS_UNKNOWN; + status <= DETECTOR_STATUS_RUNNING; status++) { + assertEquals(status, DetectorStatusTypes.requireValidDetectorStatus(status)); + } + + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.requireValidDetectorStatus(DETECTOR_STATUS_UNKNOWN - 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.requireValidDetectorStatus(DETECTOR_STATUS_RUNNING + 1)); + } + + @Test + public void testFormatAndParseDetectorStatus() { + for (@DetectorStatus int status = DETECTOR_STATUS_UNKNOWN; + status <= DETECTOR_STATUS_RUNNING; status++) { + assertEquals(status, DetectorStatusTypes.detectorStatusFromString( + DetectorStatusTypes.detectorStatusToString(status))); + } + + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusToString(DETECTOR_STATUS_UNKNOWN - 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusToString(DETECTOR_STATUS_RUNNING + 1)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString(null)); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString("")); + assertThrows(IllegalArgumentException.class, + () -> DetectorStatusTypes.detectorStatusFromString("FOO")); + } +} diff --git a/core/tests/coretests/src/android/app/time/LocationTimeZoneAlgorithmStatusTest.java b/core/tests/coretests/src/android/app/time/LocationTimeZoneAlgorithmStatusTest.java new file mode 100644 index 000000000000..a648a885aea2 --- /dev/null +++ b/core/tests/coretests/src/android/app/time/LocationTimeZoneAlgorithmStatusTest.java @@ -0,0 +1,210 @@ +/* + * Copyright 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_UNCERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY; +import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +import android.app.time.LocationTimeZoneAlgorithmStatus.ProviderStatus; +import android.service.timezone.TimeZoneProviderStatus; + +import org.junit.Test; + +public class LocationTimeZoneAlgorithmStatusTest { + + private static final TimeZoneProviderStatus ARBITRARY_PROVIDER_RUNNING_STATUS = + new TimeZoneProviderStatus.Builder() + .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK) + .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK) + .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK) + .build(); + + @Test + public void testConstructorValidation() { + // Sample some invalid cases + + // There can't be a reported provider status if the algorithm isn't running. + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS)); + + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null)); + + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS)); + + // No reported provider status expected if the associated provider isn't ready / present. + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_NOT_PRESENT, null); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null)); + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_READY, null, + PROVIDER_STATUS_NOT_PRESENT, null); + assertThrows(IllegalArgumentException.class, + () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_READY, null, + PROVIDER_STATUS_NOT_PRESENT, ARBITRARY_PROVIDER_RUNNING_STATUS)); + } + + @Test + public void testEquals() { + LocationTimeZoneAlgorithmStatus one = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null); + assertEqualsAndHashCode(one, one); + + { + LocationTimeZoneAlgorithmStatus two = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null); + assertEqualsAndHashCode(one, two); + } + + { + LocationTimeZoneAlgorithmStatus three = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_NOT_READY, null, + PROVIDER_STATUS_NOT_PRESENT, null); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + } + + @Test + public void testParcelable() { + // Primary provider only. + { + LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null); + assertRoundTripParcelable(locationAlgorithmStatus); + } + + // Secondary provider only + { + LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS); + assertRoundTripParcelable(locationAlgorithmStatus); + } + + // Algorithm not running. + { + LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_NOT_PRESENT, null); + assertRoundTripParcelable(locationAlgorithmStatus); + } + } + + @Test + public void testRequireValidProviderStatus() { + for (@ProviderStatus int status = PROVIDER_STATUS_NOT_PRESENT; + status <= PROVIDER_STATUS_IS_UNCERTAIN; status++) { + assertEquals(status, + LocationTimeZoneAlgorithmStatus.requireValidProviderStatus(status)); + } + + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.requireValidProviderStatus( + PROVIDER_STATUS_NOT_PRESENT - 1)); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.requireValidProviderStatus( + PROVIDER_STATUS_IS_UNCERTAIN + 1)); + } + + @Test + public void testFormatAndParseProviderStatus() { + for (@ProviderStatus int status = PROVIDER_STATUS_NOT_PRESENT; + status <= PROVIDER_STATUS_IS_UNCERTAIN; status++) { + assertEquals(status, LocationTimeZoneAlgorithmStatus.providerStatusFromString( + LocationTimeZoneAlgorithmStatus.providerStatusToString(status))); + } + + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusToString( + PROVIDER_STATUS_NOT_PRESENT - 1)); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusToString( + PROVIDER_STATUS_IS_UNCERTAIN + 1)); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusFromString(null)); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusFromString("")); + assertThrows(IllegalArgumentException.class, + () -> LocationTimeZoneAlgorithmStatus.providerStatusFromString("FOO")); + } + + @Test + public void testParseCommandlineArg_noNullReportedStatuses() { + LocationTimeZoneAlgorithmStatus status = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS, + PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS); + assertEquals(status, + LocationTimeZoneAlgorithmStatus.parseCommandlineArg(status.toString())); + } + + @Test + public void testParseCommandlineArg_withNullReportedStatuses() { + LocationTimeZoneAlgorithmStatus status = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, null, + PROVIDER_STATUS_IS_UNCERTAIN, null); + assertEquals(status, + LocationTimeZoneAlgorithmStatus.parseCommandlineArg(status.toString())); + } +} diff --git a/core/tests/coretests/src/android/app/time/TelephonyTimeZoneAlgorithmStatusTest.java b/core/tests/coretests/src/android/app/time/TelephonyTimeZoneAlgorithmStatusTest.java new file mode 100644 index 000000000000..b90c485bbbb6 --- /dev/null +++ b/core/tests/coretests/src/android/app/time/TelephonyTimeZoneAlgorithmStatusTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; + +import static org.junit.Assert.assertNotEquals; + +import org.junit.Test; + +public class TelephonyTimeZoneAlgorithmStatusTest { + + @Test + public void testEquals() { + TelephonyTimeZoneAlgorithmStatus one = new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING); + assertEqualsAndHashCode(one, one); + + { + TelephonyTimeZoneAlgorithmStatus two = new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING); + assertEqualsAndHashCode(one, two); + } + + { + TelephonyTimeZoneAlgorithmStatus three = new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + } + + @Test + public void testParcelable() { + // Algorithm running. + { + TelephonyTimeZoneAlgorithmStatus locationAlgorithmStatus = + new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING); + assertRoundTripParcelable(locationAlgorithmStatus); + } + + // Algorithm not running. + { + TelephonyTimeZoneAlgorithmStatus locationAlgorithmStatus = + new TelephonyTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); + assertRoundTripParcelable(locationAlgorithmStatus); + } + } +} diff --git a/core/tests/coretests/src/android/app/time/TimeZoneDetectorStatusTest.java b/core/tests/coretests/src/android/app/time/TimeZoneDetectorStatusTest.java new file mode 100644 index 000000000000..dfff7ecdf989 --- /dev/null +++ b/core/tests/coretests/src/android/app/time/TimeZoneDetectorStatusTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 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 android.app.time; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING; +import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; + +import static org.junit.Assert.assertNotEquals; + +import org.junit.Test; + +public class TimeZoneDetectorStatusTest { + + private static final TelephonyTimeZoneAlgorithmStatus ARBITRARY_TELEPHONY_ALGORITHM_STATUS = + new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING); + + private static final LocationTimeZoneAlgorithmStatus ARBITRARY_LOCATION_ALGORITHM_STATUS = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null, + LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT, null); + + @Test + public void testEquals() { + TimeZoneDetectorStatus one = new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertEqualsAndHashCode(one, one); + + { + TimeZoneDetectorStatus two = new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertEqualsAndHashCode(one, two); + } + + { + TimeZoneDetectorStatus three = new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + + { + TelephonyTimeZoneAlgorithmStatus telephonyAlgorithmStatus = + new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING); + assertNotEquals(telephonyAlgorithmStatus, ARBITRARY_TELEPHONY_ALGORITHM_STATUS); + + TimeZoneDetectorStatus three = new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING, + telephonyAlgorithmStatus, ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + + { + LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = + new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING, + LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null, + LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null); + assertNotEquals(locationAlgorithmStatus, ARBITRARY_LOCATION_ALGORITHM_STATUS); + + TimeZoneDetectorStatus three = new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, locationAlgorithmStatus); + assertNotEquals(one, three); + assertNotEquals(three, one); + } + } + + @Test + public void testParcelable() { + // Detector running. + { + TimeZoneDetectorStatus locationAlgorithmStatus = new TimeZoneDetectorStatus( + DETECTOR_STATUS_RUNNING, ARBITRARY_TELEPHONY_ALGORITHM_STATUS, + ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertRoundTripParcelable(locationAlgorithmStatus); + } + + // Detector not running. + { + TimeZoneDetectorStatus locationAlgorithmStatus = + new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING, + ARBITRARY_TELEPHONY_ALGORITHM_STATUS, + ARBITRARY_LOCATION_ALGORITHM_STATUS); + assertRoundTripParcelable(locationAlgorithmStatus); + } + } +} diff --git a/core/tests/coretests/src/android/text/format/DateFormatTest.java b/core/tests/coretests/src/android/text/format/DateFormatTest.java index 212cc44eefab..8459330cc07b 100644 --- a/core/tests/coretests/src/android/text/format/DateFormatTest.java +++ b/core/tests/coretests/src/android/text/format/DateFormatTest.java @@ -156,8 +156,8 @@ public class DateFormatTest { @DisableCompatChanges({DateFormat.DISALLOW_DUPLICATE_FIELD_IN_SKELETON}) public void testGetBestDateTimePattern_enableDuplicateField() { // en-US uses 12-hour format by default. - assertEquals("h:mm a", DateFormat.getBestDateTimePattern(Locale.US, "jmma")); - assertEquals("h:mm a", DateFormat.getBestDateTimePattern(Locale.US, "ahmma")); + assertEquals("h:mm\u202fa", DateFormat.getBestDateTimePattern(Locale.US, "jmma")); + assertEquals("h:mm\u202fa", DateFormat.getBestDateTimePattern(Locale.US, "ahmma")); } private static void assertIllegalArgumentException(Locale l, String skeleton) { diff --git a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java index 9c063954c0ad..de7244d49834 100644 --- a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java +++ b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java @@ -93,7 +93,8 @@ public class DateIntervalFormatTest { assertEquals("January 19", formatDateRange(en_US, tz, timeWithCurrentYear, timeWithCurrentYear + HOUR, FORMAT_SHOW_DATE)); - assertEquals("3:30 AM", formatDateRange(en_US, tz, fixedTime, fixedTime, FORMAT_SHOW_TIME)); + assertEquals("3:30\u202fAM", formatDateRange(en_US, tz, fixedTime, fixedTime, + FORMAT_SHOW_TIME)); assertEquals("January 19, 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + HOUR, FORMAT_SHOW_YEAR)); assertEquals("January 19", @@ -101,27 +102,27 @@ public class DateIntervalFormatTest { assertEquals("January", formatDateRange(en_US, tz, timeWithCurrentYear, timeWithCurrentYear + HOUR, FORMAT_NO_MONTH_DAY)); - assertEquals("3:30 AM", + assertEquals("3:30\u202fAM", formatDateRange(en_US, tz, fixedTime, fixedTime, FORMAT_12HOUR | FORMAT_SHOW_TIME)); assertEquals("03:30", formatDateRange(en_US, tz, fixedTime, fixedTime, FORMAT_24HOUR | FORMAT_SHOW_TIME)); - assertEquals("3:30 AM", formatDateRange(en_US, tz, fixedTime, fixedTime, + assertEquals("3:30\u202fAM", formatDateRange(en_US, tz, fixedTime, fixedTime, FORMAT_12HOUR /*| FORMAT_CAP_AMPM*/ | FORMAT_SHOW_TIME)); - assertEquals("12:00 PM", + assertEquals("12:00\u202fPM", formatDateRange(en_US, tz, fixedTime + noonDuration, fixedTime + noonDuration, FORMAT_12HOUR | FORMAT_SHOW_TIME)); - assertEquals("12:00 PM", + assertEquals("12:00\u202fPM", formatDateRange(en_US, tz, fixedTime + noonDuration, fixedTime + noonDuration, FORMAT_12HOUR | FORMAT_SHOW_TIME /*| FORMAT_CAP_NOON*/)); - assertEquals("12:00 PM", + assertEquals("12:00\u202fPM", formatDateRange(en_US, tz, fixedTime + noonDuration, fixedTime + noonDuration, FORMAT_12HOUR /*| FORMAT_NO_NOON*/ | FORMAT_SHOW_TIME)); - assertEquals("12:00 AM", formatDateRange(en_US, tz, fixedTime - midnightDuration, + assertEquals("12:00\u202fAM", formatDateRange(en_US, tz, fixedTime - midnightDuration, fixedTime - midnightDuration, FORMAT_12HOUR | FORMAT_SHOW_TIME /*| FORMAT_NO_MIDNIGHT*/)); - assertEquals("3:30 AM", + assertEquals("3:30\u202fAM", formatDateRange(en_US, tz, fixedTime, fixedTime, FORMAT_SHOW_TIME | FORMAT_UTC)); - assertEquals("3 AM", formatDateRange(en_US, tz, onTheHour, onTheHour, + assertEquals("3\u202fAM", formatDateRange(en_US, tz, onTheHour, onTheHour, FORMAT_SHOW_TIME | FORMAT_ABBREV_TIME)); assertEquals("Mon", formatDateRange(en_US, tz, fixedTime, fixedTime + HOUR, FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_WEEKDAY)); @@ -134,13 +135,13 @@ public class DateIntervalFormatTest { assertEquals("1/19/2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * HOUR, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); - assertEquals("1/19/2009 – 1/22/2009", + assertEquals("1/19/2009\u2009\u2013\u20091/22/2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); - assertEquals("1/19/2009 – 4/22/2009", + assertEquals("1/19/2009\u2009\u2013\u20094/22/2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); - assertEquals("1/19/2009 – 2/9/2012", + assertEquals("1/19/2009\u2009\u2013\u20092/9/2012", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); @@ -151,7 +152,7 @@ public class DateIntervalFormatTest { assertEquals("19.01. – 22.04.2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); - assertEquals("19.01.2009 – 09.02.2012", + assertEquals("19.01.2009\u2009\u2013\u200909.02.2012", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); @@ -169,48 +170,48 @@ public class DateIntervalFormatTest { assertEquals("19/1/2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + HOUR, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); - assertEquals("19/1/2009 – 22/1/2009", + assertEquals("19/1/2009\u2009\u2013\u200922/1/2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); - assertEquals("19/1/2009 – 22/4/2009", + assertEquals("19/1/2009\u2009\u2013\u200922/4/2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); - assertEquals("19/1/2009 – 9/2/2012", + assertEquals("19/1/2009\u2009\u2013\u20099/2/2012", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); // These are some random other test cases I came up with. - assertEquals("January 19 – 22, 2009", + assertEquals("January 19\u2009\u2013\u200922, 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY, 0)); - assertEquals("Jan 19 – 22, 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY, - FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("Mon, Jan 19 – Thu, Jan 22, 2009", + assertEquals("Jan 19\u2009\u2013\u200922, 2009", formatDateRange(en_US, tz, fixedTime, + fixedTime + 3 * DAY, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); + assertEquals("Mon, Jan 19\u2009\u2013\u2009Thu, Jan 22, 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL)); - assertEquals("Monday, January 19 – Thursday, January 22, 2009", + assertEquals("Monday, January 19\u2009\u2013\u2009Thursday, January 22, 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY)); - assertEquals("January 19 – April 22, 2009", + assertEquals("January 19\u2009\u2013\u2009April 22, 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH, 0)); - assertEquals("Jan 19 – Apr 22, 2009", + assertEquals("Jan 19\u2009\u2013\u2009Apr 22, 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("Mon, Jan 19 – Wed, Apr 22, 2009", + assertEquals("Mon, Jan 19\u2009\u2013\u2009Wed, Apr 22, 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL)); - assertEquals("January – April 2009", + assertEquals("January\u2009\u2013\u2009April 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_NO_MONTH_DAY)); - assertEquals("Jan 19, 2009 – Feb 9, 2012", + assertEquals("Jan 19, 2009\u2009\u2013\u2009Feb 9, 2012", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("Jan 2009 – Feb 2012", + assertEquals("Jan 2009\u2009\u2013\u2009Feb 2012", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_NO_MONTH_DAY | FORMAT_ABBREV_ALL)); - assertEquals("January 19, 2009 – February 9, 2012", + assertEquals("January 19, 2009\u2009\u2013\u2009February 9, 2012", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR, 0)); - assertEquals("Monday, January 19, 2009 – Thursday, February 9, 2012", + assertEquals("Monday, January 19, 2009\u2009\u2013\u2009Thursday, February 9, 2012", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_WEEKDAY)); // The same tests but for de_DE. @@ -225,26 +226,26 @@ public class DateIntervalFormatTest { assertEquals("Montag, 19. – Donnerstag, 22. Januar 2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY)); - assertEquals("19. Januar – 22. April 2009", + assertEquals("19. Januar\u2009\u2013\u200922. April 2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH, 0)); - assertEquals("19. Jan. – 22. Apr. 2009", + assertEquals("19. Jan.\u2009\u2013\u200922. Apr. 2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("Mo., 19. Jan. – Mi., 22. Apr. 2009", + assertEquals("Mo., 19. Jan.\u2009\u2013\u2009Mi., 22. Apr. 2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL)); assertEquals("Januar–April 2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_NO_MONTH_DAY)); - assertEquals("19. Jan. 2009 – 9. Feb. 2012", + assertEquals("19. Jan. 2009\u2009\u2013\u20099. Feb. 2012", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("Jan. 2009 – Feb. 2012", + assertEquals("Jan. 2009\u2009\u2013\u2009Feb. 2012", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_NO_MONTH_DAY | FORMAT_ABBREV_ALL)); - assertEquals("19. Januar 2009 – 9. Februar 2012", + assertEquals("19. Januar 2009\u2009\u2013\u20099. Februar 2012", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR, 0)); - assertEquals("Montag, 19. Januar 2009 – Donnerstag, 9. Februar 2012", + assertEquals("Montag, 19. Januar 2009\u2009\u2013\u2009Donnerstag, 9. Februar 2012", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_WEEKDAY)); // The same tests but for es_US. @@ -254,32 +255,32 @@ public class DateIntervalFormatTest { assertEquals("19–22 de ene de 2009", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("lun, 19 de ene – jue, 22 de ene de 2009", + assertEquals("lun, 19 de ene\u2009\u2013\u2009jue, 22 de ene de 2009", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL)); - assertEquals("lunes, 19 de enero – jueves, 22 de enero de 2009", + assertEquals("lunes, 19 de enero\u2009\u2013\u2009jueves, 22 de enero de 2009", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY)); - assertEquals("19 de enero – 22 de abril de 2009", + assertEquals("19 de enero\u2009\u2013\u200922 de abril de 2009", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * MONTH, 0)); - assertEquals("19 de ene – 22 de abr 2009", + assertEquals("19 de ene\u2009\u2013\u200922 de abr 2009", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("lun, 19 de ene – mié, 22 de abr de 2009", + assertEquals("lun, 19 de ene\u2009\u2013\u2009mié, 22 de abr de 2009", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL)); assertEquals("enero–abril de 2009", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_NO_MONTH_DAY)); - assertEquals("19 de ene de 2009 – 9 de feb de 2012", + assertEquals("19 de ene de 2009\u2009\u2013\u20099 de feb de 2012", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("ene de 2009 – feb de 2012", + assertEquals("ene de 2009\u2009\u2013\u2009feb de 2012", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_NO_MONTH_DAY | FORMAT_ABBREV_ALL)); - assertEquals("19 de enero de 2009 – 9 de febrero de 2012", + assertEquals("19 de enero de 2009\u2009\u2013\u20099 de febrero de 2012", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * YEAR, 0)); - assertEquals("lunes, 19 de enero de 2009 – jueves, 9 de febrero de 2012", + assertEquals("lunes, 19 de enero de 2009\u2009\u2013\u2009jueves, 9 de febrero de 2012", formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_WEEKDAY)); // The same tests but for es_ES. @@ -288,32 +289,32 @@ public class DateIntervalFormatTest { formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY, 0)); assertEquals("19–22 ene 2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("lun, 19 ene – jue, 22 ene 2009", + assertEquals("lun, 19 ene\u2009\u2013\u2009jue, 22 ene 2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL)); - assertEquals("lunes, 19 de enero – jueves, 22 de enero de 2009", + assertEquals("lunes, 19 de enero\u2009\u2013\u2009jueves, 22 de enero de 2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY)); - assertEquals("19 de enero – 22 de abril de 2009", + assertEquals("19 de enero\u2009\u2013\u200922 de abril de 2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH, 0)); - assertEquals("19 ene – 22 abr 2009", + assertEquals("19 ene\u2009\u2013\u200922 abr 2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("lun, 19 ene – mié, 22 abr 2009", + assertEquals("lun, 19 ene\u2009\u2013\u2009mié, 22 abr 2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL)); assertEquals("enero–abril de 2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_NO_MONTH_DAY)); - assertEquals("19 ene 2009 – 9 feb 2012", + assertEquals("19 ene 2009\u2009\u2013\u20099 feb 2012", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("ene 2009 – feb 2012", + assertEquals("ene 2009\u2009\u2013\u2009feb 2012", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_NO_MONTH_DAY | FORMAT_ABBREV_ALL)); - assertEquals("19 de enero de 2009 – 9 de febrero de 2012", + assertEquals("19 de enero de 2009\u2009\u2013\u20099 de febrero de 2012", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR, 0)); - assertEquals("lunes, 19 de enero de 2009 – jueves, 9 de febrero de 2012", + assertEquals("lunes, 19 de enero de 2009\u2009\u2013\u2009jueves, 9 de febrero de 2012", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_WEEKDAY)); } @@ -330,7 +331,7 @@ public class DateIntervalFormatTest { c.set(2046, Calendar.OCTOBER, 4, 3, 30); long oct_4_2046 = c.getTimeInMillis(); int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL; - assertEquals("Jan 19, 2042 – Oct 4, 2046", + assertEquals("Jan 19, 2042\u2009\u2013\u2009Oct 4, 2046", formatDateRange(l, tz, jan_19_2042, oct_4_2046, flags)); } @@ -343,15 +344,15 @@ public class DateIntervalFormatTest { int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL | FORMAT_SHOW_TIME | FORMAT_24HOUR; // The Unix epoch is UTC, so 0 is 1970-01-01T00:00Z... - assertEquals("Jan 1, 1970, 00:00 – Jan 2, 1970, 00:00", + assertEquals("Jan 1, 1970, 00:00\u2009\u2013\u2009Jan 2, 1970, 00:00", formatDateRange(l, utc, 0, DAY + 1, flags)); // But MTV is hours behind, so 0 was still the afternoon of the previous day... - assertEquals("Dec 31, 1969, 16:00 – Jan 1, 1970, 16:00", + assertEquals("Dec 31, 1969, 16:00\u2009\u2013\u2009Jan 1, 1970, 16:00", formatDateRange(l, pacific, 0, DAY, flags)); } // http://b/10318326 - we can drop the minutes in a 12-hour time if they're zero, - // but not if we're using the 24-hour clock. That is: "4 PM" is reasonable, "16" is not. + // but not if we're using the 24-hour clock. That is: "4\u202fPM" is reasonable, "16" is not. @Test public void test10318326() throws Exception { long midnight = 0; @@ -367,23 +368,26 @@ public class DateIntervalFormatTest { // Full length on-the-hour times. assertEquals("00:00", formatDateRange(l, utc, midnight, midnight, time24)); - assertEquals("12:00 AM", formatDateRange(l, utc, midnight, midnight, time12)); + assertEquals("12:00\u202fAM", formatDateRange(l, utc, midnight, midnight, time12)); assertEquals("16:00", formatDateRange(l, utc, teaTime, teaTime, time24)); - assertEquals("4:00 PM", formatDateRange(l, utc, teaTime, teaTime, time12)); + assertEquals("4:00\u202fPM", formatDateRange(l, utc, teaTime, teaTime, time12)); // Abbreviated on-the-hour times. assertEquals("00:00", formatDateRange(l, utc, midnight, midnight, abbr24)); - assertEquals("12 AM", formatDateRange(l, utc, midnight, midnight, abbr12)); + assertEquals("12\u202fAM", formatDateRange(l, utc, midnight, midnight, abbr12)); assertEquals("16:00", formatDateRange(l, utc, teaTime, teaTime, abbr24)); - assertEquals("4 PM", formatDateRange(l, utc, teaTime, teaTime, abbr12)); + assertEquals("4\u202fPM", formatDateRange(l, utc, teaTime, teaTime, abbr12)); // Abbreviated on-the-hour ranges. - assertEquals("00:00 – 16:00", formatDateRange(l, utc, midnight, teaTime, abbr24)); - assertEquals("12 AM – 4 PM", formatDateRange(l, utc, midnight, teaTime, abbr12)); + assertEquals("00:00\u2009\u2013\u200916:00", formatDateRange(l, utc, midnight, teaTime, + abbr24)); + assertEquals("12\u202fAM\u2009\u2013\u20094\u202fPM", formatDateRange(l, utc, midnight, + teaTime, abbr12)); // Abbreviated mixed ranges. - assertEquals("00:00 – 16:01", formatDateRange(l, utc, midnight, teaTime + MINUTE, abbr24)); - assertEquals("12:00 AM – 4:01 PM", + assertEquals("00:00\u2009\u2013\u200916:01", formatDateRange(l, utc, midnight, + teaTime + MINUTE, abbr24)); + assertEquals("12:00\u202fAM\u2009\u2013\u20094:01\u202fPM", formatDateRange(l, utc, midnight, teaTime + MINUTE, abbr12)); } @@ -406,12 +410,12 @@ public class DateIntervalFormatTest { // Run one millisecond over, though, and you're into the next day. long nextMorning = 1 * DAY + 1; - assertEquals("Thursday, January 1 – Friday, January 2, 1970", + assertEquals("Thursday, January 1\u2009\u2013\u2009Friday, January 2, 1970", formatDateRange(l, utc, midnight, nextMorning, flags)); // But the same reasoning applies for that day. long nextMidnight = 2 * DAY; - assertEquals("Thursday, January 1 – Friday, January 2, 1970", + assertEquals("Thursday, January 1\u2009\u2013\u2009Friday, January 2, 1970", formatDateRange(l, utc, midnight, nextMidnight, flags)); } @@ -424,9 +428,9 @@ public class DateIntervalFormatTest { int flags = FORMAT_SHOW_TIME | FORMAT_24HOUR | FORMAT_SHOW_DATE; - assertEquals("January 1, 1970, 22:00 – 00:00", + assertEquals("January 1, 1970, 22:00\u2009\u2013\u200900:00", formatDateRange(l, utc, 22 * HOUR, 24 * HOUR, flags)); - assertEquals("January 1, 1970 at 22:00 – January 2, 1970 at 00:30", + assertEquals("January 1, 1970 at 22:00\u2009\u2013\u2009January 2, 1970 at 00:30", formatDateRange(l, utc, 22 * HOUR, 24 * HOUR + 30 * MINUTE, flags)); } @@ -443,9 +447,9 @@ public class DateIntervalFormatTest { c.clear(); c.set(1980, Calendar.JANUARY, 1, 0, 0); long jan_1_1980 = c.getTimeInMillis(); - assertEquals("January 1, 1980, 22:00 – 00:00", + assertEquals("January 1, 1980, 22:00\u2009\u2013\u200900:00", formatDateRange(l, utc, jan_1_1980 + 22 * HOUR, jan_1_1980 + 24 * HOUR, flags)); - assertEquals("January 1, 1980 at 22:00 – January 2, 1980 at 00:30", + assertEquals("January 1, 1980 at 22:00\u2009\u2013\u2009January 2, 1980 at 00:30", formatDateRange(l, utc, jan_1_1980 + 22 * HOUR, jan_1_1980 + 24 * HOUR + 30 * MINUTE, flags)); } @@ -463,12 +467,12 @@ public class DateIntervalFormatTest { c.clear(); c.set(1980, Calendar.JANUARY, 1, 0, 0); long jan_1_1980 = c.getTimeInMillis(); - assertEquals("January 1, 1980, 22:00 – 00:00", + assertEquals("January 1, 1980, 22:00\u2009\u2013\u200900:00", formatDateRange(l, pacific, jan_1_1980 + 22 * HOUR, jan_1_1980 + 24 * HOUR, flags)); c.set(1980, Calendar.JULY, 1, 0, 0); long jul_1_1980 = c.getTimeInMillis(); - assertEquals("July 1, 1980, 22:00 – 00:00", + assertEquals("July 1, 1980, 22:00\u2009\u2013\u200900:00", formatDateRange(l, pacific, jul_1_1980 + 22 * HOUR, jul_1_1980 + 24 * HOUR, flags)); } @@ -531,11 +535,13 @@ public class DateIntervalFormatTest { formatDateRange(l, utc, oldYear, oldYear, FORMAT_SHOW_DATE | FORMAT_NO_YEAR)); // ...or the start and end years aren't the same... - assertEquals(String.format("February 10, 1980 – February 10, %d", c.get(Calendar.YEAR)), + assertEquals(String.format("February 10, 1980\u2009\u2013\u2009February 10, %d", + c.get(Calendar.YEAR)), formatDateRange(l, utc, oldYear, thisYear, FORMAT_SHOW_DATE)); // (And you can't avoid that --- icu4c steps in and overrides you.) - assertEquals(String.format("February 10, 1980 – February 10, %d", c.get(Calendar.YEAR)), + assertEquals(String.format("February 10, 1980\u2009\u2013\u2009February 10, %d", + c.get(Calendar.YEAR)), formatDateRange(l, utc, oldYear, thisYear, FORMAT_SHOW_DATE | FORMAT_NO_YEAR)); } @@ -595,7 +601,7 @@ public class DateIntervalFormatTest { formatDateRange(new ULocale("fa"), utc, thisYear, thisYear, flags)); assertEquals("يونۍ د ۱۹۸۰ د فبروري ۱۰", formatDateRange(new ULocale("ps"), utc, thisYear, thisYear, flags)); - assertEquals("วันอาทิตย์ที่ 10 กุมภาพันธ์ ค.ศ. 1980", + assertEquals("วันอาทิตย์ที่ 10 กุมภาพันธ์ 1980", formatDateRange(new ULocale("th"), utc, thisYear, thisYear, flags)); } @@ -607,9 +613,12 @@ public class DateIntervalFormatTest { int flags = FORMAT_SHOW_TIME | FORMAT_ABBREV_ALL | FORMAT_12HOUR; - assertEquals("10 – 11 AM", formatDateRange(l, utc, 10 * HOUR, 11 * HOUR, flags)); - assertEquals("11 AM – 1 PM", formatDateRange(l, utc, 11 * HOUR, 13 * HOUR, flags)); - assertEquals("2 – 3 PM", formatDateRange(l, utc, 14 * HOUR, 15 * HOUR, flags)); + assertEquals("10\u2009\u2013\u200911\u202fAM", formatDateRange(l, utc, + 10 * HOUR, 11 * HOUR, flags)); + assertEquals("11\u202fAM\u2009\u2013\u20091\u202fPM", formatDateRange(l, utc, + 11 * HOUR, 13 * HOUR, flags)); + assertEquals("2\u2009\u2013\u20093\u202fPM", formatDateRange(l, utc, + 14 * HOUR, 15 * HOUR, flags)); } // http://b/20708022 @@ -618,8 +627,8 @@ public class DateIntervalFormatTest { final ULocale locale = new ULocale("en"); final TimeZone timeZone = TimeZone.getTimeZone("UTC"); - assertEquals("11:00 PM – 12:00 AM", formatDateRange(locale, timeZone, - 1430434800000L, 1430438400000L, FORMAT_SHOW_TIME)); + assertEquals("11:00\u202fPM\u2009\u2013\u200912:00\u202fAM", formatDateRange(locale, + timeZone, 1430434800000L, 1430438400000L, FORMAT_SHOW_TIME)); } // http://b/68847519 @@ -629,23 +638,25 @@ public class DateIntervalFormatTest { ENGLISH, GMT_ZONE, from, to, FORMAT_SHOW_DATE | FORMAT_SHOW_TIME | FORMAT_24HOUR); // If we're showing times and the end-point is midnight the following day, we want the // behaviour of suppressing the date for the end... - assertEquals("February 27, 2007, 04:00 – 00:00", fmt.apply(1172548800000L, 1172620800000L)); + assertEquals("February 27, 2007, 04:00\u2009\u2013\u200900:00", fmt.apply(1172548800000L, + 1172620800000L)); // ...unless the start-point is also midnight, in which case we need dates to disambiguate. - assertEquals("February 27, 2007 at 00:00 – February 28, 2007 at 00:00", + assertEquals("February 27, 2007 at 00:00\u2009\u2013\u2009February 28, 2007 at 00:00", fmt.apply(1172534400000L, 1172620800000L)); // We want to show the date if the end-point is a millisecond after midnight the following // day, or if it is exactly midnight the day after that. - assertEquals("February 27, 2007 at 04:00 – February 28, 2007 at 00:00", + assertEquals("February 27, 2007 at 04:00\u2009\u2013\u2009February 28, 2007 at 00:00", fmt.apply(1172548800000L, 1172620800001L)); - assertEquals("February 27, 2007 at 04:00 – March 1, 2007 at 00:00", + assertEquals("February 27, 2007 at 04:00\u2009\u2013\u2009March 1, 2007 at 00:00", fmt.apply(1172548800000L, 1172707200000L)); // We want to show the date if the start-point is anything less than a minute after // midnight, // since that gets displayed as midnight... - assertEquals("February 27, 2007 at 00:00 – February 28, 2007 at 00:00", + assertEquals("February 27, 2007 at 00:00\u2009\u2013\u2009February 28, 2007 at 00:00", fmt.apply(1172534459999L, 1172620800000L)); // ...but not if it is exactly one minute after midnight. - assertEquals("February 27, 2007, 00:01 – 00:00", fmt.apply(1172534460000L, 1172620800000L)); + assertEquals("February 27, 2007, 00:01\u2009\u2013\u200900:00", fmt.apply(1172534460000L, + 1172620800000L)); } // http://b/68847519 @@ -656,16 +667,20 @@ public class DateIntervalFormatTest { // If we're only showing dates and the end-point is midnight of any day, we want the // behaviour of showing an end date one earlier. So if the end-point is March 2, 2007 00:00, // show March 1, 2007 instead (whether the start-point is midnight or not). - assertEquals("February 27 – March 1, 2007", fmt.apply(1172534400000L, 1172793600000L)); - assertEquals("February 27 – March 1, 2007", fmt.apply(1172548800000L, 1172793600000L)); + assertEquals("February 27\u2009\u2013\u2009March 1, 2007", + fmt.apply(1172534400000L, 1172793600000L)); + assertEquals("February 27\u2009\u2013\u2009March 1, 2007", + fmt.apply(1172548800000L, 1172793600000L)); // We want to show the true date if the end-point is a millisecond after midnight. - assertEquals("February 27 – March 2, 2007", fmt.apply(1172534400000L, 1172793600001L)); + assertEquals("February 27\u2009\u2013\u2009March 2, 2007", + fmt.apply(1172534400000L, 1172793600001L)); // 2006-02-27 00:00:00.000 GMT - 2007-03-02 00:00:00.000 GMT - assertEquals("February 27, 2006 – March 1, 2007", + assertEquals("February 27, 2006\u2009\u2013\u2009March 1, 2007", fmt.apply(1140998400000L, 1172793600000L)); // Spans a leap year's Feb 29th. - assertEquals("February 27 – March 1, 2004", fmt.apply(1077840000000L, 1078185600000L)); + assertEquals("February 27\u2009\u2013\u2009March 1, 2004", + fmt.apply(1077840000000L, 1078185600000L)); } } diff --git a/core/tests/coretests/src/android/text/format/DateUtilsTest.java b/core/tests/coretests/src/android/text/format/DateUtilsTest.java index 381c0512c532..39ed82ef40f3 100644 --- a/core/tests/coretests/src/android/text/format/DateUtilsTest.java +++ b/core/tests/coretests/src/android/text/format/DateUtilsTest.java @@ -139,16 +139,16 @@ public class DateUtilsTest { fixedTime, java.text.DateFormat.SHORT, java.text.DateFormat.FULL)); final long hourDuration = 2 * 60 * 60 * 1000; - assertEquals("5:30:15 AM Greenwich Mean Time", DateUtils.formatSameDayTime( + assertEquals("5:30:15\u202fAM Greenwich Mean Time", DateUtils.formatSameDayTime( fixedTime + hourDuration, fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.FULL)); - assertEquals("5:30:15 AM", DateUtils.formatSameDayTime(fixedTime + hourDuration, + assertEquals("5:30:15\u202fAM", DateUtils.formatSameDayTime(fixedTime + hourDuration, fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.DEFAULT)); - assertEquals("5:30:15 AM GMT", DateUtils.formatSameDayTime(fixedTime + hourDuration, + assertEquals("5:30:15\u202fAM GMT", DateUtils.formatSameDayTime(fixedTime + hourDuration, fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.LONG)); - assertEquals("5:30:15 AM", DateUtils.formatSameDayTime(fixedTime + hourDuration, + assertEquals("5:30:15\u202fAM", DateUtils.formatSameDayTime(fixedTime + hourDuration, fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.MEDIUM)); - assertEquals("5:30 AM", DateUtils.formatSameDayTime(fixedTime + hourDuration, + assertEquals("5:30\u202fAM", DateUtils.formatSameDayTime(fixedTime + hourDuration, fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.SHORT)); } diff --git a/core/tests/coretests/src/android/text/format/RelativeDateTimeFormatterTest.java b/core/tests/coretests/src/android/text/format/RelativeDateTimeFormatterTest.java index b3425162f48f..2337802db71f 100644 --- a/core/tests/coretests/src/android/text/format/RelativeDateTimeFormatterTest.java +++ b/core/tests/coretests/src/android/text/format/RelativeDateTimeFormatterTest.java @@ -468,37 +468,37 @@ public class RelativeDateTimeFormatterTest { cal.set(2015, Calendar.FEBRUARY, 5, 10, 50, 0); final long base = cal.getTimeInMillis(); - assertEquals("5 seconds ago, 10:49 AM", + assertEquals("5 seconds ago, 10:49\u202fAM", getRelativeDateTimeString(en_US, tz, base - 5 * SECOND_IN_MILLIS, base, 0, MINUTE_IN_MILLIS, 0)); - assertEquals("5 min. ago, 10:45 AM", + assertEquals("5 min. ago, 10:45\u202fAM", getRelativeDateTimeString(en_US, tz, base - 5 * MINUTE_IN_MILLIS, base, 0, HOUR_IN_MILLIS, FORMAT_ABBREV_RELATIVE)); - assertEquals("0 hr. ago, 10:45 AM", + assertEquals("0 hr. ago, 10:45\u202fAM", getRelativeDateTimeString(en_US, tz, base - 5 * MINUTE_IN_MILLIS, base, HOUR_IN_MILLIS, DAY_IN_MILLIS, FORMAT_ABBREV_RELATIVE)); - assertEquals("5 hours ago, 5:50 AM", + assertEquals("5 hours ago, 5:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 5 * HOUR_IN_MILLIS, base, HOUR_IN_MILLIS, DAY_IN_MILLIS, 0)); - assertEquals("Yesterday, 7:50 PM", + assertEquals("Yesterday, 7:50\u202fPM", getRelativeDateTimeString(en_US, tz, base - 15 * HOUR_IN_MILLIS, base, 0, WEEK_IN_MILLIS, FORMAT_ABBREV_RELATIVE)); - assertEquals("5 days ago, 10:50 AM", + assertEquals("5 days ago, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 5 * DAY_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); - assertEquals("Jan 29, 10:50 AM", + assertEquals("Jan 29, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 7 * DAY_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); - assertEquals("11/27/2014, 10:50 AM", + assertEquals("11/27/2014, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 10 * WEEK_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); - assertEquals("11/27/2014, 10:50 AM", + assertEquals("11/27/2014, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 10 * WEEK_IN_MILLIS, base, 0, YEAR_IN_MILLIS, 0)); // User-supplied flags should be ignored when formatting the date clause. final int FORMAT_SHOW_WEEKDAY = 0x00002; - assertEquals("11/27/2014, 10:50 AM", + assertEquals("11/27/2014, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 10 * WEEK_IN_MILLIS, base, 0, WEEK_IN_MILLIS, FORMAT_ABBREV_ALL | FORMAT_SHOW_WEEKDAY)); @@ -514,14 +514,14 @@ public class RelativeDateTimeFormatterTest { // So 5 hours before 3:15 AM should be formatted as 'Yesterday, 9:15 PM'. cal.set(2014, Calendar.MARCH, 9, 3, 15, 0); long base = cal.getTimeInMillis(); - assertEquals("Yesterday, 9:15 PM", + assertEquals("Yesterday, 9:15\u202fPM", getRelativeDateTimeString(en_US, tz, base - 5 * HOUR_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); // 1 hour after 2:00 AM should be formatted as 'In 1 hour, 4:00 AM'. cal.set(2014, Calendar.MARCH, 9, 2, 0, 0); base = cal.getTimeInMillis(); - assertEquals("In 1 hour, 4:00 AM", + assertEquals("In 1 hour, 4:00\u202fAM", getRelativeDateTimeString(en_US, tz, base + 1 * HOUR_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); @@ -529,22 +529,22 @@ public class RelativeDateTimeFormatterTest { // 1:00 AM. 8 hours before 5:20 AM should be 'Yesterday, 10:20 PM'. cal.set(2014, Calendar.NOVEMBER, 2, 5, 20, 0); base = cal.getTimeInMillis(); - assertEquals("Yesterday, 10:20 PM", + assertEquals("Yesterday, 10:20\u202fPM", getRelativeDateTimeString(en_US, tz, base - 8 * HOUR_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); cal.set(2014, Calendar.NOVEMBER, 2, 0, 45, 0); base = cal.getTimeInMillis(); // 45 minutes after 0:45 AM should be 'In 45 minutes, 1:30 AM'. - assertEquals("In 45 minutes, 1:30 AM", + assertEquals("In 45 minutes, 1:30\u202fAM", getRelativeDateTimeString(en_US, tz, base + 45 * MINUTE_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); // 45 minutes later, it should be 'In 45 minutes, 1:15 AM'. - assertEquals("In 45 minutes, 1:15 AM", + assertEquals("In 45 minutes, 1:15\u202fAM", getRelativeDateTimeString(en_US, tz, base + 90 * MINUTE_IN_MILLIS, base + 45 * MINUTE_IN_MILLIS, 0, WEEK_IN_MILLIS, 0)); // Another 45 minutes later, it should be 'In 45 minutes, 2:00 AM'. - assertEquals("In 45 minutes, 2:00 AM", + assertEquals("In 45 minutes, 2:00\u202fAM", getRelativeDateTimeString(en_US, tz, base + 135 * MINUTE_IN_MILLIS, base + 90 * MINUTE_IN_MILLIS, 0, WEEK_IN_MILLIS, 0)); } @@ -593,7 +593,7 @@ public class RelativeDateTimeFormatterTest { Calendar yesterdayCalendar1 = Calendar.getInstance(tz, en_US); yesterdayCalendar1.set(2011, Calendar.SEPTEMBER, 1, 10, 24, 0); long yesterday1 = yesterdayCalendar1.getTimeInMillis(); - assertEquals("Yesterday, 10:24 AM", + assertEquals("Yesterday, 10:24\u202fAM", getRelativeDateTimeString(en_US, tz, yesterday1, now, MINUTE_IN_MILLIS, WEEK_IN_MILLIS, 0)); @@ -601,7 +601,7 @@ public class RelativeDateTimeFormatterTest { Calendar yesterdayCalendar2 = Calendar.getInstance(tz, en_US); yesterdayCalendar2.set(2011, Calendar.SEPTEMBER, 1, 10, 22, 0); long yesterday2 = yesterdayCalendar2.getTimeInMillis(); - assertEquals("Yesterday, 10:22 AM", + assertEquals("Yesterday, 10:22\u202fAM", getRelativeDateTimeString(en_US, tz, yesterday2, now, MINUTE_IN_MILLIS, WEEK_IN_MILLIS, 0)); @@ -609,7 +609,7 @@ public class RelativeDateTimeFormatterTest { Calendar twoDaysAgoCalendar1 = Calendar.getInstance(tz, en_US); twoDaysAgoCalendar1.set(2011, Calendar.AUGUST, 31, 10, 24, 0); long twoDaysAgo1 = twoDaysAgoCalendar1.getTimeInMillis(); - assertEquals("2 days ago, 10:24 AM", + assertEquals("2 days ago, 10:24\u202fAM", getRelativeDateTimeString(en_US, tz, twoDaysAgo1, now, MINUTE_IN_MILLIS, WEEK_IN_MILLIS, 0)); @@ -617,7 +617,7 @@ public class RelativeDateTimeFormatterTest { Calendar twoDaysAgoCalendar2 = Calendar.getInstance(tz, en_US); twoDaysAgoCalendar2.set(2011, Calendar.AUGUST, 31, 10, 22, 0); long twoDaysAgo2 = twoDaysAgoCalendar2.getTimeInMillis(); - assertEquals("2 days ago, 10:22 AM", + assertEquals("2 days ago, 10:22\u202fAM", getRelativeDateTimeString(en_US, tz, twoDaysAgo2, now, MINUTE_IN_MILLIS, WEEK_IN_MILLIS, 0)); @@ -625,7 +625,7 @@ public class RelativeDateTimeFormatterTest { Calendar tomorrowCalendar1 = Calendar.getInstance(tz, en_US); tomorrowCalendar1.set(2011, Calendar.SEPTEMBER, 3, 10, 22, 0); long tomorrow1 = tomorrowCalendar1.getTimeInMillis(); - assertEquals("Tomorrow, 10:22 AM", + assertEquals("Tomorrow, 10:22\u202fAM", getRelativeDateTimeString(en_US, tz, tomorrow1, now, MINUTE_IN_MILLIS, WEEK_IN_MILLIS, 0)); @@ -633,7 +633,7 @@ public class RelativeDateTimeFormatterTest { Calendar tomorrowCalendar2 = Calendar.getInstance(tz, en_US); tomorrowCalendar2.set(2011, Calendar.SEPTEMBER, 3, 10, 24, 0); long tomorrow2 = tomorrowCalendar2.getTimeInMillis(); - assertEquals("Tomorrow, 10:24 AM", + assertEquals("Tomorrow, 10:24\u202fAM", getRelativeDateTimeString(en_US, tz, tomorrow2, now, MINUTE_IN_MILLIS, WEEK_IN_MILLIS, 0)); @@ -641,7 +641,7 @@ public class RelativeDateTimeFormatterTest { Calendar twoDaysLaterCalendar1 = Calendar.getInstance(tz, en_US); twoDaysLaterCalendar1.set(2011, Calendar.SEPTEMBER, 4, 10, 22, 0); long twoDaysLater1 = twoDaysLaterCalendar1.getTimeInMillis(); - assertEquals("In 2 days, 10:22 AM", + assertEquals("In 2 days, 10:22\u202fAM", getRelativeDateTimeString(en_US, tz, twoDaysLater1, now, MINUTE_IN_MILLIS, WEEK_IN_MILLIS, 0)); @@ -649,7 +649,7 @@ public class RelativeDateTimeFormatterTest { Calendar twoDaysLaterCalendar2 = Calendar.getInstance(tz, en_US); twoDaysLaterCalendar2.set(2011, Calendar.SEPTEMBER, 4, 10, 24, 0); long twoDaysLater2 = twoDaysLaterCalendar2.getTimeInMillis(); - assertEquals("In 2 days, 10:24 AM", + assertEquals("In 2 days, 10:24\u202fAM", getRelativeDateTimeString(en_US, tz, twoDaysLater2, now, MINUTE_IN_MILLIS, WEEK_IN_MILLIS, 0)); } @@ -664,11 +664,11 @@ public class RelativeDateTimeFormatterTest { cal.set(2012, Calendar.FEBRUARY, 5, 10, 50, 0); long base = cal.getTimeInMillis(); - assertEquals("Feb 5, 5:50 AM", getRelativeDateTimeString(en_US, tz, + assertEquals("Feb 5, 5:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 5 * HOUR_IN_MILLIS, base, 0, MINUTE_IN_MILLIS, 0)); - assertEquals("Jan 29, 10:50 AM", getRelativeDateTimeString(en_US, tz, + assertEquals("Jan 29, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 7 * DAY_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); - assertEquals("11/27/2011, 10:50 AM", getRelativeDateTimeString(en_US, tz, + assertEquals("11/27/2011, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 10 * WEEK_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); assertEquals("January 6", getRelativeTimeSpanString(en_US, tz, @@ -687,11 +687,11 @@ public class RelativeDateTimeFormatterTest { // Feb 5, 2018 at 10:50 PST cal.set(2018, Calendar.FEBRUARY, 5, 10, 50, 0); base = cal.getTimeInMillis(); - assertEquals("Feb 5, 5:50 AM", getRelativeDateTimeString(en_US, tz, + assertEquals("Feb 5, 5:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 5 * HOUR_IN_MILLIS, base, 0, MINUTE_IN_MILLIS, 0)); - assertEquals("Jan 29, 10:50 AM", getRelativeDateTimeString(en_US, tz, + assertEquals("Jan 29, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 7 * DAY_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); - assertEquals("11/27/2017, 10:50 AM", getRelativeDateTimeString(en_US, tz, + assertEquals("11/27/2017, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz, base - 10 * WEEK_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0)); assertEquals("January 6", getRelativeTimeSpanString(en_US, tz, diff --git a/core/tests/coretests/src/com/android/internal/os/ProcLocksReaderTest.java b/core/tests/coretests/src/com/android/internal/os/ProcLocksReaderTest.java index b34554cd113d..c3d40eb09237 100644 --- a/core/tests/coretests/src/com/android/internal/os/ProcLocksReaderTest.java +++ b/core/tests/coretests/src/com/android/internal/os/ProcLocksReaderTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertTrue; import android.content.Context; import android.os.FileUtils; +import android.util.IntArray; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -33,13 +34,15 @@ import org.junit.runner.RunWith; import java.io.File; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Arrays; @SmallTest @RunWith(AndroidJUnit4.class) public class ProcLocksReaderTest implements ProcLocksReader.ProcLocksReaderCallback { private File mProcDirectory; - private ArrayList<Integer> mPids = new ArrayList<>(); + + private ArrayList<int[]> mPids = new ArrayList<>(); @Before public void setUp() { @@ -54,41 +57,51 @@ public class ProcLocksReaderTest implements @Test public void testRunSimpleLocks() throws Exception { - String simpleLocks = - "1: POSIX ADVISORY READ 18403 fd:09:9070 1073741826 1073742335\n" + - "2: POSIX ADVISORY WRITE 18292 fd:09:34062 0 EOF\n"; + String simpleLocks = "1: POSIX ADVISORY READ 18403 fd:09:9070 1073741826 1073742335\n" + + "2: POSIX ADVISORY WRITE 18292 fd:09:34062 0 EOF\n"; runHandleBlockingFileLocks(simpleLocks); assertTrue(mPids.isEmpty()); } @Test public void testRunBlockingLocks() throws Exception { - String blockedLocks = - "1: POSIX ADVISORY READ 18403 fd:09:9070 1073741826 1073742335\n" + - "2: POSIX ADVISORY WRITE 18292 fd:09:34062 0 EOF\n" + - "2: -> POSIX ADVISORY WRITE 18291 fd:09:34062 0 EOF\n" + - "2: -> POSIX ADVISORY WRITE 18293 fd:09:34062 0 EOF\n" + - "3: POSIX ADVISORY READ 3888 fd:09:13992 128 128\n" + - "4: POSIX ADVISORY READ 3888 fd:09:14230 1073741826 1073742335\n"; + String blockedLocks = "1: POSIX ADVISORY READ 18403 fd:09:9070 1073741826 1073742335\n" + + "2: POSIX ADVISORY WRITE 18292 fd:09:34062 0 EOF\n" + + "2: -> POSIX ADVISORY WRITE 18291 fd:09:34062 0 EOF\n" + + "2: -> POSIX ADVISORY WRITE 18293 fd:09:34062 0 EOF\n" + + "3: POSIX ADVISORY READ 3888 fd:09:13992 128 128\n" + + "4: POSIX ADVISORY READ 3888 fd:09:14230 1073741826 1073742335\n"; + runHandleBlockingFileLocks(blockedLocks); + assertTrue(Arrays.equals(mPids.remove(0), new int[]{18292, 18291, 18293})); + assertTrue(mPids.isEmpty()); + } + + @Test + public void testRunLastBlockingLocks() throws Exception { + String blockedLocks = "1: POSIX ADVISORY READ 18403 fd:09:9070 1073741826 1073742335\n" + + "2: POSIX ADVISORY WRITE 18292 fd:09:34062 0 EOF\n" + + "2: -> POSIX ADVISORY WRITE 18291 fd:09:34062 0 EOF\n" + + "2: -> POSIX ADVISORY WRITE 18293 fd:09:34062 0 EOF\n"; runHandleBlockingFileLocks(blockedLocks); - assertTrue(mPids.remove(0).equals(18292)); + assertTrue(Arrays.equals(mPids.remove(0), new int[]{18292, 18291, 18293})); assertTrue(mPids.isEmpty()); } @Test public void testRunMultipleBlockingLocks() throws Exception { - String blockedLocks = - "1: POSIX ADVISORY READ 18403 fd:09:9070 1073741826 1073742335\n" + - "2: POSIX ADVISORY WRITE 18292 fd:09:34062 0 EOF\n" + - "2: -> POSIX ADVISORY WRITE 18291 fd:09:34062 0 EOF\n" + - "2: -> POSIX ADVISORY WRITE 18293 fd:09:34062 0 EOF\n" + - "3: POSIX ADVISORY READ 3888 fd:09:13992 128 128\n" + - "4: FLOCK ADVISORY WRITE 3840 fe:01:5111809 0 EOF\n" + - "4: -> FLOCK ADVISORY WRITE 3841 fe:01:5111809 0 EOF\n" + - "5: POSIX ADVISORY READ 3888 fd:09:14230 1073741826 1073742335\n"; + String blockedLocks = "1: POSIX ADVISORY READ 18403 fd:09:9070 1073741826 1073742335\n" + + "2: POSIX ADVISORY WRITE 18292 fd:09:34062 0 EOF\n" + + "2: -> POSIX ADVISORY WRITE 18291 fd:09:34062 0 EOF\n" + + "2: -> POSIX ADVISORY WRITE 18293 fd:09:34062 0 EOF\n" + + "3: POSIX ADVISORY READ 3888 fd:09:13992 128 128\n" + + "4: FLOCK ADVISORY WRITE 3840 fe:01:5111809 0 EOF\n" + + "4: -> FLOCK ADVISORY WRITE 3841 fe:01:5111809 0 EOF\n" + + "5: FLOCK ADVISORY READ 3888 fd:09:14230 0 EOF\n" + + "5: -> FLOCK ADVISORY READ 3887 fd:09:14230 0 EOF\n"; runHandleBlockingFileLocks(blockedLocks); - assertTrue(mPids.remove(0).equals(18292)); - assertTrue(mPids.remove(0).equals(3840)); + assertTrue(Arrays.equals(mPids.remove(0), new int[]{18292, 18291, 18293})); + assertTrue(Arrays.equals(mPids.remove(0), new int[]{3840, 3841})); + assertTrue(Arrays.equals(mPids.remove(0), new int[]{3888, 3887})); assertTrue(mPids.isEmpty()); } @@ -102,11 +115,12 @@ public class ProcLocksReaderTest implements /** * Call the callback function of handleBlockingFileLocks(). - * - * @param pid Each process that hold file locks blocking other processes. + * @param pids Each process that hold file locks blocking other processes. + * pids[0] is the process blocking others + * pids[1..n-1] are the processes being blocked */ @Override - public void onBlockingFileLock(int pid) { - mPids.add(pid); + public void onBlockingFileLock(IntArray pids) { + mPids.add(pids.toArray()); } } diff --git a/core/tests/coretests/src/com/android/internal/os/SafeZipPathValidatorCallbackTest.java b/core/tests/coretests/src/com/android/internal/os/SafeZipPathValidatorCallbackTest.java new file mode 100644 index 000000000000..c540a150bf35 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/os/SafeZipPathValidatorCallbackTest.java @@ -0,0 +1,244 @@ +/* + * 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.internal.os; + +import static org.junit.Assert.assertThrows; + +import android.compat.testing.PlatformCompatChangeRule; + +import androidx.test.runner.AndroidJUnit4; + +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * Test SafeZipPathCallback. + */ +@RunWith(AndroidJUnit4.class) +public class SafeZipPathValidatorCallbackTest { + @Rule + public TestRule mCompatChangeRule = new PlatformCompatChangeRule(); + + @Before + public void setUp() { + RuntimeInit.initZipPathValidatorCallback(); + } + + @Test + @EnableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL}) + public void testNewZipFile_whenZipFileHasDangerousEntriesAndChangeEnabled_throws() + throws Exception { + final String[] dangerousEntryNames = { + "../foo.bar", + "foo/../bar.baz", + "foo/../../bar.baz", + "foo.bar/..", + "foo.bar/../", + "..", + "../", + "/foo", + }; + for (String entryName : dangerousEntryNames) { + final File tempFile = File.createTempFile("smdc", "zip"); + try { + writeZipFileOutputStreamWithEmptyEntry(tempFile, entryName); + + assertThrows( + "ZipException expected for entry: " + entryName, + ZipException.class, + () -> { + new ZipFile(tempFile); + }); + } finally { + tempFile.delete(); + } + } + } + + @Test + @EnableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL}) + public void + testZipInputStreamGetNextEntry_whenZipFileHasDangerousEntriesAndChangeEnabled_throws() + throws Exception { + final String[] dangerousEntryNames = { + "../foo.bar", + "foo/../bar.baz", + "foo/../../bar.baz", + "foo.bar/..", + "foo.bar/../", + "..", + "../", + "/foo", + }; + for (String entryName : dangerousEntryNames) { + byte[] badZipBytes = getZipBytesFromZipOutputStreamWithEmptyEntry(entryName); + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(badZipBytes))) { + assertThrows( + "ZipException expected for entry: " + entryName, + ZipException.class, + () -> { + zis.getNextEntry(); + }); + } + } + } + + @Test + @EnableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL}) + public void testNewZipFile_whenZipFileHasNormalEntriesAndChangeEnabled_doesNotThrow() + throws Exception { + final String[] normalEntryNames = { + "foo", "foo.bar", "foo..bar", + }; + for (String entryName : normalEntryNames) { + final File tempFile = File.createTempFile("smdc", "zip"); + try { + writeZipFileOutputStreamWithEmptyEntry(tempFile, entryName); + try { + new ZipFile((tempFile)); + } catch (ZipException e) { + throw new AssertionError("ZipException not expected for entry: " + entryName); + } + } finally { + tempFile.delete(); + } + } + } + + @Test + @DisableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL}) + public void + testZipInputStreamGetNextEntry_whenZipFileHasNormalEntriesAndChangeEnabled_doesNotThrow() + throws Exception { + final String[] normalEntryNames = { + "foo", "foo.bar", "foo..bar", + }; + for (String entryName : normalEntryNames) { + byte[] zipBytes = getZipBytesFromZipOutputStreamWithEmptyEntry(entryName); + try { + ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes)); + zis.getNextEntry(); + } catch (ZipException e) { + throw new AssertionError("ZipException not expected for entry: " + entryName); + } + } + } + + @Test + @DisableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL}) + public void + testNewZipFile_whenZipFileHasNormalAndDangerousEntriesAndChangeDisabled_doesNotThrow() + throws Exception { + final String[] entryNames = { + "../foo.bar", + "foo/../bar.baz", + "foo/../../bar.baz", + "foo.bar/..", + "foo.bar/../", + "..", + "../", + "/foo", + "foo", + "foo.bar", + "foo..bar", + }; + for (String entryName : entryNames) { + final File tempFile = File.createTempFile("smdc", "zip"); + try { + writeZipFileOutputStreamWithEmptyEntry(tempFile, entryName); + try { + new ZipFile((tempFile)); + } catch (ZipException e) { + throw new AssertionError("ZipException not expected for entry: " + entryName); + } + } finally { + tempFile.delete(); + } + } + } + + @Test + @DisableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL}) + public void + testZipInputStreamGetNextEntry_whenZipFileHasNormalAndDangerousEntriesAndChangeDisabled_doesNotThrow() + throws Exception { + final String[] entryNames = { + "../foo.bar", + "foo/../bar.baz", + "foo/../../bar.baz", + "foo.bar/..", + "foo.bar/../", + "..", + "../", + "/foo", + "foo", + "foo.bar", + "foo..bar", + }; + for (String entryName : entryNames) { + byte[] zipBytes = getZipBytesFromZipOutputStreamWithEmptyEntry(entryName); + try { + ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes)); + zis.getNextEntry(); + } catch (ZipException e) { + throw new AssertionError("ZipException not expected for entry: " + entryName); + } + } + } + + private void writeZipFileOutputStreamWithEmptyEntry(File tempFile, String entryName) + throws IOException { + FileOutputStream tempFileStream = new FileOutputStream(tempFile); + writeZipOutputStreamWithEmptyEntry(tempFileStream, entryName); + tempFileStream.close(); + } + + private byte[] getZipBytesFromZipOutputStreamWithEmptyEntry(String entryName) + throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + writeZipOutputStreamWithEmptyEntry(bos, entryName); + return bos.toByteArray(); + } + + private void writeZipOutputStreamWithEmptyEntry(OutputStream os, String entryName) + throws IOException { + ZipOutputStream zos = new ZipOutputStream(os); + ZipEntry entry = new ZipEntry(entryName); + zos.putNextEntry(entry); + zos.write(new byte[2]); + zos.closeEntry(); + zos.close(); + } +} diff --git a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java index 34659897e588..2da9a2ebbdb6 100644 --- a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java +++ b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java @@ -18,7 +18,6 @@ package com.android.overlaytest; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertThrows; @@ -45,7 +44,6 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.Collections; -import java.util.List; import java.util.concurrent.TimeoutException; @RunWith(JUnit4.class) @@ -221,11 +219,56 @@ public class FabricatedOverlaysTest { } @Test + public void setResourceValue_withNullResourceName() throws Exception { + final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder( + "android", TEST_OVERLAY_NAME, mContext.getPackageName()); + + assertThrows(NullPointerException.class, + () -> builder.setResourceValue(null, TypedValue.TYPE_INT_DEC, 1)); + } + + @Test + public void setResourceValue_withEmptyResourceName() throws Exception { + final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder( + "android", TEST_OVERLAY_NAME, mContext.getPackageName()); + + assertThrows(IllegalArgumentException.class, + () -> builder.setResourceValue("", TypedValue.TYPE_INT_DEC, 1)); + } + + @Test + public void setResourceValue_withEmptyPackageName() throws Exception { + final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder( + "android", TEST_OVERLAY_NAME, mContext.getPackageName()); + + assertThrows(IllegalArgumentException.class, + () -> builder.setResourceValue(":color/mycolor", TypedValue.TYPE_INT_DEC, 1)); + } + + @Test + public void setResourceValue_withInvalidTypeName() throws Exception { + final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder( + "android", TEST_OVERLAY_NAME, mContext.getPackageName()); + + assertThrows(IllegalArgumentException.class, + () -> builder.setResourceValue("c/mycolor", TypedValue.TYPE_INT_DEC, 1)); + } + + @Test + public void setResourceValue_withEmptyTypeName() throws Exception { + final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder( + "android", TEST_OVERLAY_NAME, mContext.getPackageName()); + + assertThrows(IllegalArgumentException.class, + () -> builder.setResourceValue("/mycolor", TypedValue.TYPE_INT_DEC, 1)); + } + + @Test public void testInvalidResourceValues() throws Exception { final FabricatedOverlay overlay = new FabricatedOverlay.Builder( "android", TEST_OVERLAY_NAME, mContext.getPackageName()) .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1) - .setResourceValue("something", TypedValue.TYPE_INT_DEC, 1) + .setResourceValue("color/something", TypedValue.TYPE_INT_DEC, 1) .build(); waitForResourceValue(0); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 74303e2fab7c..9d841ea2e55d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -18,6 +18,12 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; +import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; +import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; +import static androidx.window.extensions.embedding.SplitContainer.shouldFinishPrimaryWithSecondary; +import static androidx.window.extensions.embedding.SplitContainer.shouldFinishSecondaryWithPrimary; + import android.app.Activity; import android.app.WindowConfiguration.WindowingMode; import android.content.Intent; @@ -140,6 +146,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, launchingFragmentToken, secondaryFragmentToken, rule); + setCompanionTaskFragment(wct, launchingFragmentToken, secondaryFragmentToken, rule, + false /* isStacked */); } /** @@ -215,6 +223,28 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.setAdjacentTaskFragments(primary, secondary, adjacentParams); } + void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule, + boolean isStacked) { + final boolean finishPrimaryWithSecondary; + if (isStacked) { + finishPrimaryWithSecondary = shouldFinishAssociatedContainerWhenStacked( + getFinishPrimaryWithSecondaryBehavior(splitRule)); + } else { + finishPrimaryWithSecondary = shouldFinishPrimaryWithSecondary(splitRule); + } + wct.setCompanionTaskFragment(primary, finishPrimaryWithSecondary ? secondary : null); + + final boolean finishSecondaryWithPrimary; + if (isStacked) { + finishSecondaryWithPrimary = shouldFinishAssociatedContainerWhenStacked( + getFinishSecondaryWithPrimaryBehavior(splitRule)); + } else { + finishSecondaryWithPrimary = shouldFinishSecondaryWithPrimary(splitRule); + } + wct.setCompanionTaskFragment(secondary, finishSecondaryWithPrimary ? primary : null); + } + TaskFragmentCreationParams createFragmentOptions(@NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { if (mFragmentInfos.containsKey(fragmentToken)) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 16760e26b3f1..d52caaf4c3e8 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -389,6 +389,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // launching activity in the Task. mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); + } else if (taskFragmentInfo.isClearedForReorderActivityToFront()) { + // Do not finish the dependents if this TaskFragment was cleared to reorder + // the launching Activity to front of the Task. + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } else if (!container.isWaitingActivityAppear()) { // Do not finish the container before the expected activity appear until // timeout. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 362f1fa096cc..cb470bac5c9a 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -371,13 +371,16 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull SplitAttributes splitAttributes) { // Clear adjacent TaskFragments if the container is shown in fullscreen, or the // secondaryContainer could not be finished. - if (!shouldShowSplit(splitAttributes)) { + boolean isStacked = !shouldShowSplit(splitAttributes); + if (isStacked) { setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), null /* secondary */, null /* splitRule */); } else { setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), secondaryContainer.getTaskFragmentToken(), splitRule); } + setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(), + secondaryContainer.getTaskFragmentToken(), splitRule, isStacked); } /** @@ -489,8 +492,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { || splitContainer.getSecondaryContainer().getInfo() == null) { return RESULT_EXPAND_FAILED_NO_TF_INFO; } - expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken()); - expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken()); + final IBinder primaryToken = + splitContainer.getPrimaryContainer().getTaskFragmentToken(); + final IBinder secondaryToken = + splitContainer.getSecondaryContainer().getTaskFragmentToken(); + expandTaskFragment(wct, primaryToken); + expandTaskFragment(wct, secondaryToken); + // Set the companion TaskFragment when the two containers stacked. + setCompanionTaskFragment(wct, primaryToken, secondaryToken, + splitContainer.getSplitRule(), true /* isStacked */); return RESULT_EXPANDED; } return RESULT_NOT_EXPANDED; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index b516e1407b11..2192b5ca219c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -36,6 +36,7 @@ import android.os.Bundle; import android.os.IBinder; import android.util.ArrayMap; import android.window.WindowContext; +import android.window.WindowProvider; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -71,7 +72,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private final List<CommonFoldingFeature> mLastReportedFoldingFeatures = new ArrayList<>(); - private final Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners = + private final Map<IBinder, ConfigurationChangeListener> mConfigurationChangeListeners = new ArrayMap<>(); public WindowLayoutComponentImpl(@NonNull Context context) { @@ -121,21 +122,21 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } if (!context.isUiContext()) { throw new IllegalArgumentException("Context must be a UI Context, which should be" - + " an Activity or a WindowContext"); + + " an Activity, WindowContext or InputMethodService"); } mFoldingFeatureProducer.getData((features) -> { - // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer. WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features); consumer.accept(newWindowLayout); }); mWindowLayoutChangeListeners.put(context, consumer); - if (context instanceof WindowContext) { + // TODO(b/258065175) Further extend this to ContextWrappers. + if (context instanceof WindowProvider) { final IBinder windowContextToken = context.getWindowContextToken(); - final WindowContextConfigListener listener = - new WindowContextConfigListener(windowContextToken); + final ConfigurationChangeListener listener = + new ConfigurationChangeListener(windowContextToken); context.registerComponentCallbacks(listener); - mWindowContextConfigListeners.put(windowContextToken, listener); + mConfigurationChangeListeners.put(windowContextToken, listener); } } @@ -150,10 +151,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) { continue; } - if (context instanceof WindowContext) { + if (context instanceof WindowProvider) { final IBinder token = context.getWindowContextToken(); - context.unregisterComponentCallbacks(mWindowContextConfigListeners.get(token)); - mWindowContextConfigListeners.remove(token); + context.unregisterComponentCallbacks(mConfigurationChangeListeners.get(token)); + mConfigurationChangeListeners.remove(token); } break; } @@ -349,10 +350,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } } - private final class WindowContextConfigListener implements ComponentCallbacks { + private final class ConfigurationChangeListener implements ComponentCallbacks { final IBinder mToken; - WindowContextConfigListener(IBinder token) { + ConfigurationChangeListener(IBinder token) { mToken = token; } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index 40f7a273980a..92011af27619 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -169,6 +169,7 @@ public class EmbeddingTestUtils { new Point(), false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, + false /* isClearedForReorderActivityToFront */, new Point()); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 957a24873998..79813c7d064e 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -144,6 +144,6 @@ public class JetpackTaskFragmentOrganizerTest { mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */, false /* isVisible */, new ArrayList<>(), new Point(), false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, - new Point()); + false /* isClearedForReorderActivityToFront */, new Point()); } } 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 07cd7d6f60b7..f811940fd304 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 @@ -116,7 +116,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final TouchTracker mTouchTracker = new TouchTracker(); private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>(); - + @Nullable private IOnBackInvokedCallback mActiveCallback; @VisibleForTesting @@ -180,6 +180,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void initBackAnimationRunners() { + if (!IS_U_ANIMATION_ENABLED) { + return; + } + final CrossTaskBackAnimation crossTaskAnimation = new CrossTaskBackAnimation(mContext); mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK, new BackAnimationRunner(crossTaskAnimation.mCallback, crossTaskAnimation.mRunner)); @@ -207,7 +211,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void updateEnableAnimationFromSetting() { int settingValue = Global.getInt(mContext.getContentResolver(), Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF); - boolean isEnabled = settingValue == SETTING_VALUE_ON && IS_U_ANIMATION_ENABLED; + boolean isEnabled = settingValue == SETTING_VALUE_ON; mEnableAnimations.set(isEnabled); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled); } @@ -350,10 +354,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } final int backType = backNavigationInfo.getType(); - final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType); + final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(); if (shouldDispatchToAnimator) { - mActiveCallback = mAnimationDefinition.get(backType).getCallback(); - mAnimationDefinition.get(backType).startGesture(); + if (mAnimationDefinition.contains(backType)) { + mActiveCallback = mAnimationDefinition.get(backType).getCallback(); + mAnimationDefinition.get(backType).startGesture(); + } else { + mActiveCallback = null; + } } else { mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback(); dispatchOnBackStarted(mActiveCallback, mTouchTracker.createStartEvent(null)); @@ -361,9 +369,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void onMove() { - if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get()) { + if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get() + || mActiveCallback == null) { return; } + final BackEvent backEvent = mTouchTracker.createProgressEvent(); dispatchOnBackProgressed(mActiveCallback, backEvent); } @@ -387,11 +397,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private boolean shouldDispatchToAnimator(int backType) { + private boolean shouldDispatchToAnimator() { return mEnableAnimations.get() && mBackNavigationInfo != null - && mBackNavigationInfo.isPrepareRemoteAnimation() - && mAnimationDefinition.contains(backType); + && mBackNavigationInfo.isPrepareRemoteAnimation(); } private void dispatchOnBackStarted(IOnBackInvokedCallback callback, @@ -461,6 +470,30 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTouchTracker.setProgressThreshold(progressThreshold); } + private void invokeOrCancelBack() { + // Make a synchronized call to core before dispatch back event to client side. + // If the close transition happens before the core receives onAnimationFinished, there will + // play a second close animation for that transition. + if (mBackAnimationFinishedCallback != null) { + try { + mBackAnimationFinishedCallback.onAnimationFinished(mTriggerBack); + } catch (RemoteException e) { + Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e); + } + mBackAnimationFinishedCallback = null; + } + + if (mBackNavigationInfo != null) { + final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); + if (mTriggerBack) { + dispatchOnBackInvoked(callback); + } else { + dispatchOnBackCancelled(callback); + } + } + finishBackNavigation(); + } + /** * Called when the gesture is released, then it could start the post commit animation. */ @@ -493,15 +526,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } final int backType = mBackNavigationInfo.getType(); - // Directly finish back navigation if no animator defined. - if (!shouldDispatchToAnimator(backType)) { - if (mTriggerBack) { - dispatchOnBackInvoked(mActiveCallback); - } else { - dispatchOnBackCancelled(mActiveCallback); - } - // Animation missing. Simply finish back navigation. - finishBackNavigation(); + // Simply trigger and finish back navigation when no animator defined. + if (!shouldDispatchToAnimator() || mActiveCallback == null) { + invokeOrCancelBack(); return; } @@ -549,16 +576,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()"); // Trigger the real back. - if (mBackNavigationInfo != null) { - IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); - if (mTriggerBack) { - dispatchOnBackInvoked(callback); - } else { - dispatchOnBackCancelled(callback); - } - } - - finishBackNavigation(); + invokeOrCancelBack(); } /** @@ -567,25 +585,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @VisibleForTesting void finishBackNavigation() { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()"); - BackNavigationInfo backNavigationInfo = mBackNavigationInfo; - boolean triggerBack = mTriggerBack; - mBackNavigationInfo = null; - mTriggerBack = false; mShouldStartOnNextMoveEvent = false; mTouchTracker.reset(); mActiveCallback = null; - if (backNavigationInfo == null) { - return; - } - if (mBackAnimationFinishedCallback != null) { - try { - mBackAnimationFinishedCallback.onAnimationFinished(triggerBack); - } catch (RemoteException e) { - Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e); - } - mBackAnimationFinishedCallback = null; + if (mBackNavigationInfo != null) { + mBackNavigationInfo.onBackNavigationFinished(mTriggerBack); + mBackNavigationInfo = null; } - backNavigationInfo.onBackNavigationFinished(triggerBack); + mTriggerBack = false; } private void createAdapter() { 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 5b7ed278e843..6e116b958ac9 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 @@ -163,7 +163,8 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Showing resizing hint. */ public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, - Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY) { + Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, + boolean immediately) { if (mResizingIconView == null) { return; } @@ -178,8 +179,8 @@ public class SplitDecorManager extends WindowlessWindowManager { final boolean show = newBounds.width() > mBounds.width() || newBounds.height() > mBounds.height(); - final boolean animate = show != mShown; - if (animate && mFadeAnimator != null && mFadeAnimator.isRunning()) { + final boolean update = show != mShown; + if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) { // If we need to animate and animator still running, cancel it before we ensure both // background and icon surfaces are non null for next animation. mFadeAnimator.cancel(); @@ -192,7 +193,7 @@ public class SplitDecorManager extends WindowlessWindowManager { .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); } - if (mGapBackgroundLeash == null) { + if (mGapBackgroundLeash == null && !immediately) { final boolean isLandscape = newBounds.height() == sideBounds.height(); final int left = isLandscape ? mBounds.width() : 0; final int top = isLandscape ? 0 : mBounds.height(); @@ -221,8 +222,13 @@ public class SplitDecorManager extends WindowlessWindowManager { newBounds.width() / 2 - mIconSize / 2, newBounds.height() / 2 - mIconSize / 2); - if (animate) { - startFadeAnimation(show, null /* finishedConsumer */); + if (update) { + if (immediately) { + t.setVisibility(mBackgroundLeash, show); + t.setVisibility(mIconLeash, show); + } else { + startFadeAnimation(show, null /* finishedConsumer */); + } mShown = show; } } @@ -319,10 +325,12 @@ public class SplitDecorManager extends WindowlessWindowManager { @Override public void onAnimationStart(@NonNull Animator animation) { if (show) { - animT.show(mBackgroundLeash).show(mIconLeash).show(mGapBackgroundLeash).apply(); - } else { - animT.hide(mGapBackgroundLeash).apply(); + animT.show(mBackgroundLeash).show(mIconLeash); + } + if (mGapBackgroundLeash != null) { + animT.setVisibility(mGapBackgroundLeash, show); } + animT.apply(); } @Override 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 3de1045bfbda..ec9e6f7573bf 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 @@ -83,8 +83,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private static final int FLING_RESIZE_DURATION = 250; private static final int FLING_SWITCH_DURATION = 350; - private static final int FLING_ENTER_DURATION = 350; - private static final int FLING_EXIT_DURATION = 350; + private static final int FLING_ENTER_DURATION = 450; + private static final int FLING_EXIT_DURATION = 450; private int mDividerWindowWidth; private int mDividerInsets; 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 5533ad56d17c..56aa742b8626 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 @@ -95,7 +95,7 @@ interface ISplitScreen { */ oneway void startShortcutAndTask(in ShortcutInfo shortcutInfo, in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio, - in RemoteTransition remoteTransition, in InstanceId instanceId) = 17; + in RemoteTransition remoteTransition, in InstanceId instanceId) = 17; /** * Version of startTasks using legacy transition system. @@ -119,6 +119,21 @@ interface ISplitScreen { in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15; /** + * Start a pair of intents using legacy transition system. + */ + oneway void startIntentsWithLegacyTransition(in PendingIntent pendingIntent1, + in Bundle options1, in PendingIntent pendingIntent2, 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 Bundle options1, + in PendingIntent pendingIntent2, 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 * recents (for example: the dividerBar). * @param appTargets apps that will be re-parented to display area @@ -132,4 +147,4 @@ interface ISplitScreen { */ RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; } -// Last id = 17 +// Last id = 19
\ No newline at end of file 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 cdc8cdd2c28d..1774dd5e3b6f 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 @@ -548,6 +548,24 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, options2, splitPosition, splitRatio, remoteTransition, instanceId); } + private void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, + @Nullable Bundle options1, PendingIntent pendingIntent2, + @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { + Intent fillInIntent1 = null; + Intent fillInIntent2 = null; + if (launchSameComponentAdjacently(pendingIntent1, pendingIntent2) + && supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) { + fillInIntent1 = new Intent(); + fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + fillInIntent2 = new Intent(); + fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } + mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1, options1, + pendingIntent2, fillInIntent2, options2, splitPosition, splitRatio, adapter, + instanceId); + } + @Override public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { @@ -621,6 +639,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return Objects.equals(launchingActivity, pairedActivity); } + private boolean launchSameComponentAdjacently(PendingIntent pendingIntent1, + PendingIntent pendingIntent2) { + return Objects.equals(pendingIntent1.getIntent().getComponent(), + pendingIntent2.getIntent().getComponent()); + } + @VisibleForTesting /** Returns {@code true} if the component supports multi-instances split. */ boolean supportMultiInstancesSplit(@Nullable ComponentName launching) { @@ -986,6 +1010,27 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override + public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, + @Nullable Bundle options1, PendingIntent pendingIntent2, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + executeRemoteCallWithTaskPermission(mController, "startIntentsWithLegacyTransition", + (controller) -> + controller.startIntentsWithLegacyTransition( + pendingIntent1, options1, pendingIntent2, options2, splitPosition, + splitRatio, adapter, instanceId) + ); + } + + @Override + public void startIntents(PendingIntent pendingIntent1, @Nullable Bundle options1, + PendingIntent pendingIntent2, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + // TODO(b/259368992): To be implemented. + } + + @Override public void startShortcut(String packageName, String shortcutId, int position, @Nullable Bundle options, UserHandle user, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startShortcut", 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 c2ab7ef7e7bf..acb71a80ee8a 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 @@ -169,6 +169,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private ValueAnimator mDividerFadeInAnimator; private boolean mDividerVisible; private boolean mKeyguardShowing; + private boolean mShowDecorImmediately; private final SyncTransactionQueue mSyncQueue; private final ShellTaskOrganizer mTaskOrganizer; private final Context mContext; @@ -588,8 +589,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Starts a pair of tasks using legacy transition. */ void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, - float splitRatio, RemoteAnimationAdapter adapter, - InstanceId instanceId) { + float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); addActivityOptions(options1, mSideStage); @@ -599,7 +599,20 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, instanceId); } - /** Starts a pair of intent and task using legacy transition. */ + /** Starts a pair of intents using legacy transition. */ + void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, Intent fillInIntent1, + @Nullable Bundle options1, PendingIntent pendingIntent2, Intent fillInIntent2, + @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (options1 == null) options1 = new Bundle(); + addActivityOptions(options1, mSideStage); + wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1); + + startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, options2, splitPosition, + splitRatio, adapter, instanceId); + } + void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, @@ -627,12 +640,29 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, instanceId); } + private void startWithLegacyTransition(WindowContainerTransaction wct, + @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, + @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { + startWithLegacyTransition(wct, INVALID_TASK_ID, mainPendingIntent, mainFillInIntent, + mainOptions, sidePosition, splitRatio, adapter, instanceId); + } + + private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId, + @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { + startWithLegacyTransition(wct, mainTaskId, null /* mainPendingIntent */, + null /* mainFillInIntent */, mainOptions, sidePosition, splitRatio, adapter, + instanceId); + } + /** * @param wct transaction to start the first task * @param instanceId if {@code null}, will not log. Otherwise it will be used in * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} */ private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId, + @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { // Init divider first to make divider leash for remote animation target. @@ -701,7 +731,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mainOptions == null) mainOptions = new Bundle(); addActivityOptions(mainOptions, mMainStage); updateWindowBounds(mSplitLayout, wct); - wct.startTask(mainTaskId, mainOptions); + if (mainTaskId == INVALID_TASK_ID) { + wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions); + } else { + wct.startTask(mainTaskId, mainOptions); + } wct.reorder(mRootTaskInfo.token, true); wct.setForceTranslucent(mRootTaskInfo.token, false); @@ -1556,6 +1590,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mLogger.isEnterRequestedByDrag()) { updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); } else { + mShowDecorImmediately = true; mSplitLayout.flingDividerToCenter(); } }); @@ -1631,14 +1666,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateSurfaceBounds(layout, t, true /* applyResizingOffset */); getMainStageBounds(mTempRect1); getSideStageBounds(mTempRect2); - mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY); - mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY); + mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately); + mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately); t.apply(); mTransactionPool.release(t); } @Override public void onLayoutSizeChanged(SplitLayout layout) { + // Reset this flag every time onLayoutSizeChanged. + mShowDecorImmediately = false; final WindowContainerTransaction wct = new WindowContainerTransaction(); updateWindowBounds(layout, wct); sendOnBoundsChanged(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index acad5d93eab4..bcf900b99c69 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -289,10 +289,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, - int offsetY) { + int offsetY, boolean immediately) { if (mSplitDecorManager != null && mRootTaskInfo != null) { mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t, offsetX, - offsetY); + offsetY, immediately); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 48c0cea150cc..d3f1332f6224 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -317,7 +317,7 @@ class DragResizeInputListener implements AutoCloseable { } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { - if (mDragging) { + if (mShouldHandleEvents && mDragging) { int dragPointerIndex = e.findPointerIndex(mDragPointerId); mCallback.onDragResizeEnd( e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index b603e0355e98..d75c36c99a4c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -38,6 +38,7 @@ import android.app.WindowConfiguration; import android.content.pm.ApplicationInfo; import android.graphics.Point; import android.graphics.Rect; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteCallback; @@ -56,6 +57,7 @@ import android.window.BackNavigationInfo; import android.window.IBackAnimationFinishedCallback; import android.window.IOnBackInvokedCallback; +import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -189,31 +191,23 @@ public class BackAnimationControllerTest extends ShellTestCase { } for (int type: testTypes) { - boolean[] backNavigationDone = new boolean[]{false}; - boolean[] triggerBack = new boolean[]{false}; - + final ResultListener result = new ResultListener(); createNavigationInfo(new BackNavigationInfo.Builder() .setType(type) .setOnBackInvokedCallback(mAppCallback) .setPrepareRemoteAnimation(true) - .setOnBackNavigationDone( - new RemoteCallback(result -> { - backNavigationDone[0] = true; - triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK); - }))); + .setOnBackNavigationDone(new RemoteCallback(result))); triggerBackGesture(); simulateRemoteAnimationStart(type); simulateRemoteAnimationFinished(); mShellExecutor.flushAll(); assertTrue("Navigation Done callback not called for " - + BackNavigationInfo.typeToString(type), backNavigationDone[0]); - assertTrue("TriggerBack should have been true", triggerBack[0]); + + BackNavigationInfo.typeToString(type), result.mBackNavigationDone); + assertTrue("TriggerBack should have been true", result.mTriggerBack); } } - - @Test public void backToHome_dispatchesEvents() throws RemoteException { registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); @@ -351,6 +345,65 @@ public class BackAnimationControllerTest extends ShellTestCase { verify(mAnimatorCallback, never()).onBackInvoked(); } + @Test + public void animationNotDefined() throws RemoteException { + final int[] testTypes = new int[] { + BackNavigationInfo.TYPE_RETURN_TO_HOME, + BackNavigationInfo.TYPE_CROSS_TASK, + BackNavigationInfo.TYPE_CROSS_ACTIVITY, + BackNavigationInfo.TYPE_DIALOG_CLOSE}; + + for (int type: testTypes) { + final ResultListener result = new ResultListener(); + createNavigationInfo(new BackNavigationInfo.Builder() + .setType(type) + .setOnBackInvokedCallback(mAppCallback) + .setPrepareRemoteAnimation(true) + .setOnBackNavigationDone(new RemoteCallback(result))); + triggerBackGesture(); + simulateRemoteAnimationStart(type); + mShellExecutor.flushAll(); + + assertTrue("Navigation Done callback not called for " + + BackNavigationInfo.typeToString(type), result.mBackNavigationDone); + assertTrue("TriggerBack should have been true", result.mTriggerBack); + } + + verify(mAppCallback, never()).onBackStarted(any()); + verify(mAppCallback, never()).onBackProgressed(any()); + verify(mAppCallback, times(testTypes.length)).onBackInvoked(); + + verify(mAnimatorCallback, never()).onBackStarted(any()); + verify(mAnimatorCallback, never()).onBackProgressed(any()); + verify(mAnimatorCallback, never()).onBackInvoked(); + } + + @Test + public void callbackShouldDeliverProgress() throws RemoteException { + registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); + + final int type = BackNavigationInfo.TYPE_CALLBACK; + final ResultListener result = new ResultListener(); + createNavigationInfo(new BackNavigationInfo.Builder() + .setType(type) + .setOnBackInvokedCallback(mAppCallback) + .setOnBackNavigationDone(new RemoteCallback(result))); + triggerBackGesture(); + mShellExecutor.flushAll(); + + assertTrue("Navigation Done callback not called for " + + BackNavigationInfo.typeToString(type), result.mBackNavigationDone); + assertTrue("TriggerBack should have been true", result.mTriggerBack); + + verify(mAppCallback, times(1)).onBackStarted(any()); + verify(mAppCallback, times(1)).onBackProgressed(any()); + verify(mAppCallback, times(1)).onBackInvoked(); + + verify(mAnimatorCallback, never()).onBackStarted(any()); + verify(mAnimatorCallback, never()).onBackProgressed(any()); + verify(mAnimatorCallback, never()).onBackInvoked(); + } + private void doMotionEvent(int actionDown, int coordinate) { mController.onMotionEvent( coordinate, coordinate, @@ -377,4 +430,14 @@ public class BackAnimationControllerTest extends ShellTestCase { mController.registerAnimation(type, new BackAnimationRunner(mAnimatorCallback, mBackAnimationRunner)); } + + private static class ResultListener implements RemoteCallback.OnResultListener { + boolean mBackNavigationDone = false; + boolean mTriggerBack = false; + @Override + public void onResult(@Nullable Bundle result) { + mBackNavigationDone = true; + mTriggerBack = result.getBoolean(KEY_TRIGGER_BACK); + } + }; } diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 1381bdd6a50d..06ffb72d183b 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -43,28 +43,19 @@ namespace { using EntryValue = std::variant<Res_value, incfs::verified_map_ptr<ResTable_map_entry>>; +/* NOTE: table_entry has been verified in LoadedPackage::GetEntryFromOffset(), + * and so access to ->value() and ->map_entry() are safe here + */ base::expected<EntryValue, IOError> GetEntryValue( incfs::verified_map_ptr<ResTable_entry> table_entry) { - const uint16_t entry_size = dtohs(table_entry->size); + const uint16_t entry_size = table_entry->size(); // Check if the entry represents a bag value. - if (entry_size >= sizeof(ResTable_map_entry) && - (dtohs(table_entry->flags) & ResTable_entry::FLAG_COMPLEX)) { - const auto map_entry = table_entry.convert<ResTable_map_entry>(); - if (!map_entry) { - return base::unexpected(IOError::PAGES_MISSING); - } - return map_entry.verified(); + if (entry_size >= sizeof(ResTable_map_entry) && table_entry->is_complex()) { + return table_entry.convert<ResTable_map_entry>().verified(); } - // The entry represents a non-bag value. - const auto entry_value = table_entry.offset(entry_size).convert<Res_value>(); - if (!entry_value) { - return base::unexpected(IOError::PAGES_MISSING); - } - Res_value value; - value.copyFrom_dtoh(entry_value.value()); - return value; + return table_entry->value(); } } // namespace @@ -814,17 +805,12 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( return base::unexpected(std::nullopt); } - auto best_entry_result = LoadedPackage::GetEntryFromOffset(best_type, best_offset); - if (!best_entry_result.has_value()) { - return base::unexpected(best_entry_result.error()); - } - - const incfs::map_ptr<ResTable_entry> best_entry = *best_entry_result; - if (!best_entry) { - return base::unexpected(IOError::PAGES_MISSING); + auto best_entry_verified = LoadedPackage::GetEntryFromOffset(best_type, best_offset); + if (!best_entry_verified.has_value()) { + return base::unexpected(best_entry_verified.error()); } - const auto entry = GetEntryValue(best_entry.verified()); + const auto entry = GetEntryValue(*best_entry_verified); if (!entry.has_value()) { return base::unexpected(entry.error()); } @@ -837,7 +823,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( .package_name = &best_package->GetPackageName(), .type_string_ref = StringPoolRef(best_package->GetTypeStringPool(), best_type->id - 1), .entry_string_ref = StringPoolRef(best_package->GetKeyStringPool(), - best_entry->key.index), + (*best_entry_verified)->key()), .dynamic_ref_table = package_group.dynamic_ref_table.get(), }; } diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp index 5b69cca2d747..386f718208b3 100644 --- a/libs/androidfw/LoadedArsc.cpp +++ b/libs/androidfw/LoadedArsc.cpp @@ -88,7 +88,9 @@ static bool VerifyResTableType(incfs::map_ptr<ResTable_type> header) { // Make sure that there is enough room for the entry offsets. const size_t offsets_offset = dtohs(header->header.headerSize); const size_t entries_offset = dtohl(header->entriesStart); - const size_t offsets_length = sizeof(uint32_t) * entry_count; + const size_t offsets_length = header->flags & ResTable_type::FLAG_OFFSET16 + ? sizeof(uint16_t) * entry_count + : sizeof(uint32_t) * entry_count; if (offsets_offset > entries_offset || entries_offset - offsets_offset < offsets_length) { LOG(ERROR) << "RES_TABLE_TYPE_TYPE entry offsets overlap actual entry data."; @@ -107,8 +109,8 @@ static bool VerifyResTableType(incfs::map_ptr<ResTable_type> header) { return true; } -static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry( - incfs::verified_map_ptr<ResTable_type> type, uint32_t entry_offset) { +static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> +VerifyResTableEntry(incfs::verified_map_ptr<ResTable_type> type, uint32_t entry_offset) { // Check that the offset is aligned. if (UNLIKELY(entry_offset & 0x03U)) { LOG(ERROR) << "Entry at offset " << entry_offset << " is not 4-byte aligned."; @@ -136,7 +138,7 @@ static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry( return base::unexpected(IOError::PAGES_MISSING); } - const size_t entry_size = dtohs(entry->size); + const size_t entry_size = entry->size(); if (UNLIKELY(entry_size < sizeof(entry.value()))) { LOG(ERROR) << "ResTable_entry size " << entry_size << " at offset " << entry_offset << " is too small."; @@ -149,6 +151,11 @@ static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry( return base::unexpected(std::nullopt); } + // If entry is compact, value is already encoded, and a compact entry + // cannot be a map_entry, we are done verifying + if (entry->is_compact()) + return entry.verified(); + if (entry_size < sizeof(ResTable_map_entry)) { // There needs to be room for one Res_value struct. if (UNLIKELY(entry_offset + entry_size > chunk_size - sizeof(Res_value))) { @@ -192,7 +199,7 @@ static base::expected<std::monostate, NullOrIOError> VerifyResTableEntry( return base::unexpected(std::nullopt); } } - return {}; + return entry.verified(); } LoadedPackage::iterator::iterator(const LoadedPackage* lp, size_t ti, size_t ei) @@ -228,7 +235,7 @@ uint32_t LoadedPackage::iterator::operator*() const { entryIndex_); } -base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::GetEntry( +base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::GetEntry( incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index) { base::expected<uint32_t, NullOrIOError> entry_offset = GetEntryOffset(type_chunk, entry_index); if (UNLIKELY(!entry_offset.has_value())) { @@ -242,14 +249,13 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::GetEntryOffset( // The configuration matches and is better than the previous selection. // Find the entry value if it exists for this configuration. const size_t entry_count = dtohl(type_chunk->entryCount); - const size_t offsets_offset = dtohs(type_chunk->header.headerSize); + const auto offsets = type_chunk.offset(dtohs(type_chunk->header.headerSize)); // Check if there is the desired entry in this type. if (type_chunk->flags & ResTable_type::FLAG_SPARSE) { // This is encoded as a sparse map, so perform a binary search. bool error = false; - auto sparse_indices = type_chunk.offset(offsets_offset) - .convert<ResTable_sparseTypeEntry>().iterator(); + auto sparse_indices = offsets.convert<ResTable_sparseTypeEntry>().iterator(); auto sparse_indices_end = sparse_indices + entry_count; auto result = std::lower_bound(sparse_indices, sparse_indices_end, entry_index, [&error](const incfs::map_ptr<ResTable_sparseTypeEntry>& entry, @@ -284,26 +290,36 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::GetEntryOffset( return base::unexpected(std::nullopt); } - const auto entry_offset_ptr = type_chunk.offset(offsets_offset).convert<uint32_t>() + entry_index; - if (UNLIKELY(!entry_offset_ptr)) { - return base::unexpected(IOError::PAGES_MISSING); + uint32_t result; + + if (type_chunk->flags & ResTable_type::FLAG_OFFSET16) { + const auto entry_offset_ptr = offsets.convert<uint16_t>() + entry_index; + if (UNLIKELY(!entry_offset_ptr)) { + return base::unexpected(IOError::PAGES_MISSING); + } + result = offset_from16(entry_offset_ptr.value()); + } else { + const auto entry_offset_ptr = offsets.convert<uint32_t>() + entry_index; + if (UNLIKELY(!entry_offset_ptr)) { + return base::unexpected(IOError::PAGES_MISSING); + } + result = dtohl(entry_offset_ptr.value()); } - const uint32_t value = dtohl(entry_offset_ptr.value()); - if (value == ResTable_type::NO_ENTRY) { + if (result == ResTable_type::NO_ENTRY) { return base::unexpected(std::nullopt); } - - return value; + return result; } -base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> LoadedPackage::GetEntryFromOffset( - incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset) { +base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> +LoadedPackage::GetEntryFromOffset(incfs::verified_map_ptr<ResTable_type> type_chunk, + uint32_t offset) { auto valid = VerifyResTableEntry(type_chunk, offset); if (UNLIKELY(!valid.has_value())) { return base::unexpected(valid.error()); } - return type_chunk.offset(offset + dtohl(type_chunk->entriesStart)).convert<ResTable_entry>(); + return valid; } base::expected<std::monostate, IOError> LoadedPackage::CollectConfigurations( @@ -376,31 +392,42 @@ base::expected<uint32_t, NullOrIOError> LoadedPackage::FindEntryByName( for (const auto& type_entry : type_spec->type_entries) { const incfs::verified_map_ptr<ResTable_type>& type = type_entry.type; - size_t entry_count = dtohl(type->entryCount); - for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) { - auto entry_offset_ptr = type.offset(dtohs(type->header.headerSize)).convert<uint32_t>() + - entry_idx; - if (!entry_offset_ptr) { - return base::unexpected(IOError::PAGES_MISSING); - } + const size_t entry_count = dtohl(type->entryCount); + const auto entry_offsets = type.offset(dtohs(type->header.headerSize)); + for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) { uint32_t offset; uint16_t res_idx; if (type->flags & ResTable_type::FLAG_SPARSE) { - auto sparse_entry = entry_offset_ptr.convert<ResTable_sparseTypeEntry>(); + auto sparse_entry = entry_offsets.convert<ResTable_sparseTypeEntry>() + entry_idx; + if (!sparse_entry) { + return base::unexpected(IOError::PAGES_MISSING); + } offset = dtohs(sparse_entry->offset) * 4u; res_idx = dtohs(sparse_entry->idx); + } else if (type->flags & ResTable_type::FLAG_OFFSET16) { + auto entry = entry_offsets.convert<uint16_t>() + entry_idx; + if (!entry) { + return base::unexpected(IOError::PAGES_MISSING); + } + offset = offset_from16(entry.value()); + res_idx = entry_idx; } else { - offset = dtohl(entry_offset_ptr.value()); + auto entry = entry_offsets.convert<uint32_t>() + entry_idx; + if (!entry) { + return base::unexpected(IOError::PAGES_MISSING); + } + offset = dtohl(entry.value()); res_idx = entry_idx; } + if (offset != ResTable_type::NO_ENTRY) { auto entry = type.offset(dtohl(type->entriesStart) + offset).convert<ResTable_entry>(); if (!entry) { return base::unexpected(IOError::PAGES_MISSING); } - if (dtohl(entry->key.index) == static_cast<uint32_t>(*key_idx)) { + if (entry->key() == static_cast<uint32_t>(*key_idx)) { // The package ID will be overridden by the caller (due to runtime assignment of package // IDs for shared libraries). return make_resid(0x00, *type_idx + type_id_offset_ + 1, res_idx); diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp index b3fb1452919b..b68143d82090 100644 --- a/libs/androidfw/LocaleDataTables.cpp +++ b/libs/androidfw/LocaleDataTables.cpp @@ -143,6 +143,7 @@ const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ {0xACE00000u, 46u}, // ahl -> Latn {0xB8E00000u, 1u}, // aho -> Ahom {0x99200000u, 46u}, // ajg -> Latn + {0xCD200000u, 2u}, // ajt -> Arab {0x616B0000u, 46u}, // ak -> Latn {0xA9400000u, 101u}, // akk -> Xsux {0x81600000u, 46u}, // ala -> Latn @@ -1053,6 +1054,7 @@ const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ {0xB70D0000u, 46u}, // nyn -> Latn {0xA32D0000u, 46u}, // nzi -> Latn {0x6F630000u, 46u}, // oc -> Latn + {0x6F634553u, 46u}, // oc-ES -> Latn {0x88CE0000u, 46u}, // ogc -> Latn {0x6F6A0000u, 11u}, // oj -> Cans {0xC92E0000u, 11u}, // ojs -> Cans @@ -1093,6 +1095,7 @@ const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ {0xB4EF0000u, 71u}, // phn -> Phnx {0xAD0F0000u, 46u}, // pil -> Latn {0xBD0F0000u, 46u}, // pip -> Latn + {0xC90F0000u, 46u}, // pis -> Latn {0x814F0000u, 9u}, // pka -> Brah {0xB94F0000u, 46u}, // pko -> Latn {0x706C0000u, 46u}, // pl -> Latn @@ -1204,12 +1207,14 @@ const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ {0xE1720000u, 46u}, // sly -> Latn {0x736D0000u, 46u}, // sm -> Latn {0x81920000u, 46u}, // sma -> Latn + {0x8D920000u, 46u}, // smd -> Latn {0xA5920000u, 46u}, // smj -> Latn {0xB5920000u, 46u}, // smn -> Latn {0xBD920000u, 76u}, // smp -> Samr {0xC1920000u, 46u}, // smq -> Latn {0xC9920000u, 46u}, // sms -> Latn {0x736E0000u, 46u}, // sn -> Latn + {0x85B20000u, 46u}, // snb -> Latn {0x89B20000u, 46u}, // snc -> Latn {0xA9B20000u, 46u}, // snk -> Latn {0xBDB20000u, 46u}, // snp -> Latn @@ -1314,6 +1319,7 @@ const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ {0x746F0000u, 46u}, // to -> Latn {0x95D30000u, 46u}, // tof -> Latn {0x99D30000u, 46u}, // tog -> Latn + {0xA9D30000u, 46u}, // tok -> Latn {0xC1D30000u, 46u}, // toq -> Latn {0xA1F30000u, 46u}, // tpi -> Latn {0xB1F30000u, 46u}, // tpm -> Latn @@ -1527,6 +1533,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x61665A414C61746ELLU, // af_Latn_ZA 0xC0C0434D4C61746ELLU, // agq_Latn_CM 0xB8E0494E41686F6DLLU, // aho_Ahom_IN + 0xCD20544E41726162LLU, // ajt_Arab_TN 0x616B47484C61746ELLU, // ak_Latn_GH 0xA940495158737578LLU, // akk_Xsux_IQ 0xB560584B4C61746ELLU, // aln_Latn_XK @@ -1534,6 +1541,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x616D455445746869LLU, // am_Ethi_ET 0xB9804E474C61746ELLU, // amo_Latn_NG 0x616E45534C61746ELLU, // an_Latn_ES + 0xB5A04E474C61746ELLU, // ann_Latn_NG 0xE5C049444C61746ELLU, // aoz_Latn_ID 0x8DE0544741726162LLU, // apd_Arab_TG 0x6172454741726162LLU, // ar_Arab_EG @@ -2039,6 +2047,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xB88F49525870656FLLU, // peo_Xpeo_IR 0xACAF44454C61746ELLU, // pfl_Latn_DE 0xB4EF4C4250686E78LLU, // phn_Phnx_LB + 0xC90F53424C61746ELLU, // pis_Latn_SB 0x814F494E42726168LLU, // pka_Brah_IN 0xB94F4B454C61746ELLU, // pko_Latn_KE 0x706C504C4C61746ELLU, // pl_Latn_PL @@ -2119,11 +2128,13 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xE17249444C61746ELLU, // sly_Latn_ID 0x736D57534C61746ELLU, // sm_Latn_WS 0x819253454C61746ELLU, // sma_Latn_SE + 0x8D92414F4C61746ELLU, // smd_Latn_AO 0xA59253454C61746ELLU, // smj_Latn_SE 0xB59246494C61746ELLU, // smn_Latn_FI 0xBD92494C53616D72LLU, // smp_Samr_IL 0xC99246494C61746ELLU, // sms_Latn_FI 0x736E5A574C61746ELLU, // sn_Latn_ZW + 0x85B24D594C61746ELLU, // snb_Latn_MY 0xA9B24D4C4C61746ELLU, // snk_Latn_ML 0x736F534F4C61746ELLU, // so_Latn_SO 0x99D2555A536F6764LLU, // sog_Sogd_UZ diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 5e8a623d4205..267190a54195 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -4487,20 +4487,14 @@ ssize_t ResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag return err; } - if ((dtohs(entry.entry->flags) & ResTable_entry::FLAG_COMPLEX) != 0) { + if (entry.entry->map_entry()) { if (!mayBeBag) { ALOGW("Requesting resource 0x%08x failed because it is complex\n", resID); } return BAD_VALUE; } - const Res_value* value = reinterpret_cast<const Res_value*>( - reinterpret_cast<const uint8_t*>(entry.entry) + entry.entry->size); - - outValue->size = dtohs(value->size); - outValue->res0 = value->res0; - outValue->dataType = value->dataType; - outValue->data = dtohl(value->data); + *outValue = entry.entry->value(); // The reference may be pointing to a resource in a shared library. These // references have build-time generated package IDs. These ids may not match @@ -4691,11 +4685,10 @@ ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag, return err; } - const uint16_t entrySize = dtohs(entry.entry->size); - const uint32_t parent = entrySize >= sizeof(ResTable_map_entry) - ? dtohl(((const ResTable_map_entry*)entry.entry)->parent.ident) : 0; - const uint32_t count = entrySize >= sizeof(ResTable_map_entry) - ? dtohl(((const ResTable_map_entry*)entry.entry)->count) : 0; + const uint16_t entrySize = entry.entry->size(); + const ResTable_map_entry* map_entry = entry.entry->map_entry(); + const uint32_t parent = map_entry ? dtohl(map_entry->parent.ident) : 0; + const uint32_t count = map_entry ? dtohl(map_entry->count) : 0; size_t N = count; @@ -4759,7 +4752,7 @@ ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag, // Now merge in the new attributes... size_t curOff = (reinterpret_cast<uintptr_t>(entry.entry) - reinterpret_cast<uintptr_t>(entry.type)) - + dtohs(entry.entry->size); + + entrySize; const ResTable_map* map; bag_entry* entries = (bag_entry*)(set+1); size_t curEntry = 0; @@ -5137,7 +5130,7 @@ uint32_t ResTable::findEntry(const PackageGroup* group, ssize_t typeIndex, const continue; } - if (dtohl(entry->key.index) == (size_t) *ei) { + if (entry->key() == (size_t) *ei) { uint32_t resId = Res_MAKEID(group->id - 1, typeIndex, iter.index()); if (outTypeSpecFlags) { Entry result; @@ -6600,8 +6593,12 @@ status_t ResTable::getEntry( // Entry does not exist. continue; } - - thisOffset = dtohl(eindex[realEntryIndex]); + if (thisType->flags & ResTable_type::FLAG_OFFSET16) { + auto eindex16 = reinterpret_cast<const uint16_t*>(eindex); + thisOffset = offset_from16(eindex16[realEntryIndex]); + } else { + thisOffset = dtohl(eindex[realEntryIndex]); + } } if (thisOffset == ResTable_type::NO_ENTRY) { @@ -6651,8 +6648,8 @@ status_t ResTable::getEntry( const ResTable_entry* const entry = reinterpret_cast<const ResTable_entry*>( reinterpret_cast<const uint8_t*>(bestType) + bestOffset); - if (dtohs(entry->size) < sizeof(*entry)) { - ALOGW("ResTable_entry size 0x%x is too small", dtohs(entry->size)); + if (entry->size() < sizeof(*entry)) { + ALOGW("ResTable_entry size 0x%zx is too small", entry->size()); return BAD_TYPE; } @@ -6663,7 +6660,7 @@ status_t ResTable::getEntry( outEntry->specFlags = specFlags; outEntry->package = bestPackage; outEntry->typeStr = StringPoolRef(&bestPackage->typeStrings, actualTypeIndex - bestPackage->typeIdOffset); - outEntry->keyStr = StringPoolRef(&bestPackage->keyStrings, dtohl(entry->key.index)); + outEntry->keyStr = StringPoolRef(&bestPackage->keyStrings, entry->key()); } return NO_ERROR; } @@ -7653,6 +7650,9 @@ void ResTable::print(bool inclValues) const if (type->flags & ResTable_type::FLAG_SPARSE) { printf(" [sparse]"); } + if (type->flags & ResTable_type::FLAG_OFFSET16) { + printf(" [offset16]"); + } } printf(":\n"); @@ -7684,7 +7684,13 @@ void ResTable::print(bool inclValues) const thisOffset = static_cast<uint32_t>(dtohs(entry->offset)) * 4u; } else { entryId = entryIndex; - thisOffset = dtohl(eindex[entryIndex]); + if (type->flags & ResTable_type::FLAG_OFFSET16) { + const auto eindex16 = + reinterpret_cast<const uint16_t*>(eindex); + thisOffset = offset_from16(eindex16[entryIndex]); + } else { + thisOffset = dtohl(eindex[entryIndex]); + } if (thisOffset == ResTable_type::NO_ENTRY) { continue; } @@ -7734,7 +7740,7 @@ void ResTable::print(bool inclValues) const continue; } - uintptr_t esize = dtohs(ent->size); + uintptr_t esize = ent->size(); if ((esize&0x3) != 0) { printf("NON-INTEGER ResTable_entry SIZE: %p\n", (void *)esize); continue; @@ -7746,30 +7752,27 @@ void ResTable::print(bool inclValues) const } const Res_value* valuePtr = NULL; - const ResTable_map_entry* bagPtr = NULL; + const ResTable_map_entry* bagPtr = ent->map_entry(); Res_value value; - if ((dtohs(ent->flags)&ResTable_entry::FLAG_COMPLEX) != 0) { + if (bagPtr) { printf("<bag>"); - bagPtr = (const ResTable_map_entry*)ent; } else { - valuePtr = (const Res_value*) - (((const uint8_t*)ent) + esize); - value.copyFrom_dtoh(*valuePtr); + value = ent->value(); printf("t=0x%02x d=0x%08x (s=0x%04x r=0x%02x)", (int)value.dataType, (int)value.data, (int)value.size, (int)value.res0); } - if ((dtohs(ent->flags)&ResTable_entry::FLAG_PUBLIC) != 0) { + if (ent->flags() & ResTable_entry::FLAG_PUBLIC) { printf(" (PUBLIC)"); } printf("\n"); if (inclValues) { - if (valuePtr != NULL) { + if (bagPtr == NULL) { printf(" "); print_value(typeConfigs->package, value); - } else if (bagPtr != NULL) { + } else { const int N = dtohl(bagPtr->count); const uint8_t* baseMapPtr = (const uint8_t*)ent; size_t mapOffset = esize; diff --git a/libs/androidfw/TypeWrappers.cpp b/libs/androidfw/TypeWrappers.cpp index 647aa197a94d..70d14a11830e 100644 --- a/libs/androidfw/TypeWrappers.cpp +++ b/libs/androidfw/TypeWrappers.cpp @@ -59,7 +59,9 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { + dtohl(type->header.size); const uint32_t* const entryIndices = reinterpret_cast<const uint32_t*>( reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize)); - if (reinterpret_cast<uintptr_t>(entryIndices) + (sizeof(uint32_t) * entryCount) > containerEnd) { + const size_t indexSize = type->flags & ResTable_type::FLAG_OFFSET16 ? + sizeof(uint16_t) : sizeof(uint32_t); + if (reinterpret_cast<uintptr_t>(entryIndices) + (indexSize * entryCount) > containerEnd) { ALOGE("Type's entry indices extend beyond its boundaries"); return NULL; } @@ -73,6 +75,9 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { } entryOffset = static_cast<uint32_t>(dtohs(ResTable_sparseTypeEntry{*iter}.offset)) * 4u; + } else if (type->flags & ResTable_type::FLAG_OFFSET16) { + auto entryIndices16 = reinterpret_cast<const uint16_t*>(entryIndices); + entryOffset = offset_from16(entryIndices16[mIndex]); } else { entryOffset = dtohl(entryIndices[mIndex]); } @@ -91,11 +96,11 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { if (reinterpret_cast<uintptr_t>(entry) > containerEnd - sizeof(*entry)) { ALOGE("Entry offset at index %u points outside the Type's boundaries", mIndex); return NULL; - } else if (reinterpret_cast<uintptr_t>(entry) + dtohs(entry->size) > containerEnd) { + } else if (reinterpret_cast<uintptr_t>(entry) + entry->size() > containerEnd) { ALOGE("Entry at index %u extends beyond Type's boundaries", mIndex); return NULL; - } else if (dtohs(entry->size) < sizeof(*entry)) { - ALOGE("Entry at index %u is too small (%u)", mIndex, dtohs(entry->size)); + } else if (entry->size() < sizeof(*entry)) { + ALOGE("Entry at index %u is too small (%zu)", mIndex, entry->size()); return NULL; } return entry; diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h index e45963950b04..79d962829046 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -166,14 +166,14 @@ class LoadedPackage { base::expected<uint32_t, NullOrIOError> FindEntryByName(const std::u16string& type_name, const std::u16string& entry_name) const; - static base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> GetEntry( - incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index); + static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> + GetEntry(incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index); static base::expected<uint32_t, NullOrIOError> GetEntryOffset( incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index); - static base::expected<incfs::map_ptr<ResTable_entry>, NullOrIOError> GetEntryFromOffset( - incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset); + static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError> + GetEntryFromOffset(incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset); // Returns the string pool where type names are stored. const ResStringPool* GetTypeStringPool() const { diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index a625889eaf3c..c740832522fc 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -26,6 +26,7 @@ #include <androidfw/Errors.h> #include <androidfw/LocaleData.h> #include <androidfw/StringPiece.h> +#include <utils/ByteOrder.h> #include <utils/Errors.h> #include <utils/String16.h> #include <utils/Vector.h> @@ -1437,6 +1438,10 @@ struct ResTable_type // Mark any types that use this with a v26 qualifier to prevent runtime issues on older // platforms. FLAG_SPARSE = 0x01, + + // If set, the offsets to the entries are encoded in 16-bit, real_offset = offset * 4u + // An 16-bit offset of 0xffffu means a NO_ENTRY + FLAG_OFFSET16 = 0x02, }; uint8_t flags; @@ -1453,6 +1458,11 @@ struct ResTable_type ResTable_config config; }; +// Convert a 16-bit offset to 32-bit if FLAG_OFFSET16 is set +static inline uint32_t offset_from16(uint16_t off16) { + return dtohs(off16) == 0xffffu ? ResTable_type::NO_ENTRY : dtohs(off16) * 4u; +} + // The minimum size required to read any version of ResTable_type. constexpr size_t kResTableTypeMinSize = sizeof(ResTable_type) - sizeof(ResTable_config) + sizeof(ResTable_config::size); @@ -1480,6 +1490,8 @@ union ResTable_sparseTypeEntry { static_assert(sizeof(ResTable_sparseTypeEntry) == sizeof(uint32_t), "ResTable_sparseTypeEntry must be 4 bytes in size"); +struct ResTable_map_entry; + /** * This is the beginning of information about an entry in the resource * table. It holds the reference to the name of this entry, and is @@ -1487,12 +1499,11 @@ static_assert(sizeof(ResTable_sparseTypeEntry) == sizeof(uint32_t), * * A Res_value structure, if FLAG_COMPLEX is -not- set. * * An array of ResTable_map structures, if FLAG_COMPLEX is set. * These supply a set of name/value mappings of data. + * * If FLAG_COMPACT is set, this entry is a compact entry for + * simple values only */ -struct ResTable_entry +union ResTable_entry { - // Number of bytes in this structure. - uint16_t size; - enum { // If set, this is a complex entry, holding a set of name/value // mappings. It is followed by an array of ResTable_map structures. @@ -1504,18 +1515,91 @@ struct ResTable_entry // resources of the same name/type. This is only useful during // linking with other resource tables. FLAG_WEAK = 0x0004, + // If set, this is a compact entry with data type and value directly + // encoded in the this entry, see ResTable_entry::compact + FLAG_COMPACT = 0x0008, }; - uint16_t flags; - - // Reference into ResTable_package::keyStrings identifying this entry. - struct ResStringPool_ref key; + + struct Full { + // Number of bytes in this structure. + uint16_t size; + + uint16_t flags; + + // Reference into ResTable_package::keyStrings identifying this entry. + struct ResStringPool_ref key; + } full; + + /* A compact entry is indicated by FLAG_COMPACT, with flags at the same + * offset as a normal entry. This is only for simple data values where + * + * - size for entry or value can be inferred (both being 8 bytes). + * - key index is encoded in 16-bit + * - dataType is encoded as the higher 8-bit of flags + * - data is encoded directly in this entry + */ + struct Compact { + uint16_t key; + uint16_t flags; + uint32_t data; + } compact; + + uint16_t flags() const { return dtohs(full.flags); }; + bool is_compact() const { return flags() & FLAG_COMPACT; } + bool is_complex() const { return flags() & FLAG_COMPLEX; } + + size_t size() const { + return is_compact() ? sizeof(ResTable_entry) : dtohs(this->full.size); + } + + uint32_t key() const { + return is_compact() ? dtohs(this->compact.key) : dtohl(this->full.key.index); + } + + /* Always verify the memory associated with this entry and its value + * before calling value() or map_entry() + */ + Res_value value() const { + Res_value v; + if (is_compact()) { + v.size = sizeof(Res_value); + v.res0 = 0; + v.data = dtohl(this->compact.data); + v.dataType = dtohs(compact.flags) >> 8; + } else { + auto vaddr = reinterpret_cast<const uint8_t*>(this) + dtohs(this->full.size); + auto value = reinterpret_cast<const Res_value*>(vaddr); + v.size = dtohs(value->size); + v.res0 = value->res0; + v.data = dtohl(value->data); + v.dataType = value->dataType; + } + return v; + } + + const ResTable_map_entry* map_entry() const { + return is_complex() && !is_compact() ? + reinterpret_cast<const ResTable_map_entry*>(this) : nullptr; + } }; +/* Make sure size of ResTable_entry::Full and ResTable_entry::Compact + * be the same as ResTable_entry. This is to allow iteration of entries + * to work in either cases. + * + * The offset of flags must be at the same place for both structures, + * to ensure the correct reading to decide whether this is a full entry + * or a compact entry. + */ +static_assert(sizeof(ResTable_entry) == sizeof(ResTable_entry::Full)); +static_assert(sizeof(ResTable_entry) == sizeof(ResTable_entry::Compact)); +static_assert(offsetof(ResTable_entry, full.flags) == offsetof(ResTable_entry, compact.flags)); + /** * Extended form of a ResTable_entry for map entries, defining a parent map * resource from which to inherit values. */ -struct ResTable_map_entry : public ResTable_entry +struct ResTable_map_entry : public ResTable_entry::Full { // Resource identifier of the parent mapping, or 0 if there is none. // This is always treated as a TYPE_DYNAMIC_REFERENCE. diff --git a/libs/androidfw/tests/TypeWrappers_test.cpp b/libs/androidfw/tests/TypeWrappers_test.cpp index d69abe5d0f11..aba3ab3a06a2 100644 --- a/libs/androidfw/tests/TypeWrappers_test.cpp +++ b/libs/androidfw/tests/TypeWrappers_test.cpp @@ -37,8 +37,8 @@ void* createTypeData() { offsets[0] = 0; ResTable_entry e1; memset(&e1, 0, sizeof(e1)); - e1.size = sizeof(e1); - e1.key.index = 0; + e1.full.size = sizeof(e1); + e1.full.key.index = 0; t.header.size += sizeof(e1); Res_value v1; @@ -50,8 +50,8 @@ void* createTypeData() { offsets[2] = sizeof(e1) + sizeof(v1); ResTable_entry e2; memset(&e2, 0, sizeof(e2)); - e2.size = sizeof(e2); - e2.key.index = 1; + e2.full.size = sizeof(e2); + e2.full.key.index = 1; t.header.size += sizeof(e2); Res_value v2; @@ -83,7 +83,7 @@ TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) { TypeVariant::iterator iter = v.beginEntries(); ASSERT_EQ(uint32_t(0), iter.index()); ASSERT_TRUE(NULL != *iter); - ASSERT_EQ(uint32_t(0), iter->key.index); + ASSERT_EQ(uint32_t(0), iter->full.key.index); ASSERT_NE(v.endEntries(), iter); iter++; @@ -96,7 +96,7 @@ TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) { ASSERT_EQ(uint32_t(2), iter.index()); ASSERT_TRUE(NULL != *iter); - ASSERT_EQ(uint32_t(1), iter->key.index); + ASSERT_EQ(uint32_t(1), iter->full.key.index); ASSERT_NE(v.endEntries(), iter); iter++; diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp index c18edcd8689b..15049302d322 100644 --- a/media/jni/android_media_tv_Tuner.cpp +++ b/media/jni/android_media_tv_Tuner.cpp @@ -360,6 +360,7 @@ void LnbClientCallbackImpl::onEvent(const LnbEventType lnbEventType) { lnb, gFields.onLnbEventID, (jint)lnbEventType); + env->DeleteLocalRef(lnb); } else { ALOGE("LnbClientCallbackImpl::onEvent:" "Lnb object has been freed. Ignoring callback."); @@ -378,6 +379,7 @@ void LnbClientCallbackImpl::onDiseqcMessage(const vector<uint8_t> &diseqcMessage lnb, gFields.onLnbDiseqcMessageID, array); + env->DeleteLocalRef(lnb); } else { ALOGE("LnbClientCallbackImpl::onDiseqcMessage:" "Lnb object has been freed. Ignoring callback."); @@ -404,6 +406,7 @@ void DvrClientCallbackImpl::onRecordStatus(RecordStatus status) { jobject dvr(env->NewLocalRef(mDvrObj)); if (!env->IsSameObject(dvr, nullptr)) { env->CallVoidMethod(dvr, gFields.onDvrRecordStatusID, (jint)status); + env->DeleteLocalRef(dvr); } else { ALOGE("DvrClientCallbackImpl::onRecordStatus:" "Dvr object has been freed. Ignoring callback."); @@ -416,6 +419,7 @@ void DvrClientCallbackImpl::onPlaybackStatus(PlaybackStatus status) { jobject dvr(env->NewLocalRef(mDvrObj)); if (!env->IsSameObject(dvr, nullptr)) { env->CallVoidMethod(dvr, gFields.onDvrPlaybackStatusID, (jint)status); + env->DeleteLocalRef(dvr); } else { ALOGE("DvrClientCallbackImpl::onPlaybackStatus:" "Dvr object has been freed. Ignoring callback."); @@ -603,6 +607,7 @@ void FilterClientCallbackImpl::getSectionEvent(jobjectArray &arr, const int size jobject obj = env->NewObject(eventClazz, eventInit, tableId, version, sectionNum, dataLength); env->SetObjectArrayElement(arr, size, obj); + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getMediaEvent(jobjectArray &arr, const int size, @@ -673,6 +678,10 @@ void FilterClientCallbackImpl::getMediaEvent(jobjectArray &arr, const int size, } env->SetObjectArrayElement(arr, size, obj); + if(audioDescriptor != nullptr) { + env->DeleteLocalRef(audioDescriptor); + } + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getPesEvent(jobjectArray &arr, const int size, @@ -688,6 +697,7 @@ void FilterClientCallbackImpl::getPesEvent(jobjectArray &arr, const int size, jobject obj = env->NewObject(eventClazz, eventInit, streamId, dataLength, mpuSequenceNumber); env->SetObjectArrayElement(arr, size, obj); + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getTsRecordEvent(jobjectArray &arr, const int size, @@ -725,6 +735,7 @@ void FilterClientCallbackImpl::getTsRecordEvent(jobjectArray &arr, const int siz jobject obj = env->NewObject(eventClazz, eventInit, jpid, ts, sc, byteNumber, pts, firstMbInSlice); env->SetObjectArrayElement(arr, size, obj); + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getMmtpRecordEvent(jobjectArray &arr, const int size, @@ -745,6 +756,7 @@ void FilterClientCallbackImpl::getMmtpRecordEvent(jobjectArray &arr, const int s jobject obj = env->NewObject(eventClazz, eventInit, scHevcIndexMask, byteNumber, mpuSequenceNumber, pts, firstMbInSlice, tsIndexMask); env->SetObjectArrayElement(arr, size, obj); + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getDownloadEvent(jobjectArray &arr, const int size, @@ -764,6 +776,7 @@ void FilterClientCallbackImpl::getDownloadEvent(jobjectArray &arr, const int siz jobject obj = env->NewObject(eventClazz, eventInit, itemId, downloadId, mpuSequenceNumber, itemFragmentIndex, lastItemFragmentIndex, dataLength); env->SetObjectArrayElement(arr, size, obj); + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getIpPayloadEvent(jobjectArray &arr, const int size, @@ -776,6 +789,7 @@ void FilterClientCallbackImpl::getIpPayloadEvent(jobjectArray &arr, const int si jint dataLength = ipPayloadEvent.dataLength; jobject obj = env->NewObject(eventClazz, eventInit, dataLength); env->SetObjectArrayElement(arr, size, obj); + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getTemiEvent(jobjectArray &arr, const int size, @@ -794,6 +808,8 @@ void FilterClientCallbackImpl::getTemiEvent(jobjectArray &arr, const int size, jobject obj = env->NewObject(eventClazz, eventInit, pts, descrTag, array); env->SetObjectArrayElement(arr, size, obj); + env->DeleteLocalRef(array); + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getScramblingStatusEvent(jobjectArray &arr, const int size, @@ -807,6 +823,7 @@ void FilterClientCallbackImpl::getScramblingStatusEvent(jobjectArray &arr, const .get<DemuxFilterMonitorEvent::Tag::scramblingStatus>(); jobject obj = env->NewObject(eventClazz, eventInit, scramblingStatus); env->SetObjectArrayElement(arr, size, obj); + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getIpCidChangeEvent(jobjectArray &arr, const int size, @@ -819,6 +836,7 @@ void FilterClientCallbackImpl::getIpCidChangeEvent(jobjectArray &arr, const int .get<DemuxFilterMonitorEvent::Tag::cid>(); jobject obj = env->NewObject(eventClazz, eventInit, cid); env->SetObjectArrayElement(arr, size, obj); + env->DeleteLocalRef(obj); } void FilterClientCallbackImpl::getRestartEvent(jobjectArray &arr, const int size, @@ -922,10 +940,12 @@ void FilterClientCallbackImpl::onFilterEvent(const vector<DemuxFilterEvent> &eve methodID = gFields.onSharedFilterEventID; } env->CallVoidMethod(filter, methodID, array); + env->DeleteLocalRef(filter); } else { ALOGE("FilterClientCallbackImpl::onFilterEvent:" "Filter object has been freed. Ignoring callback."); } + env->DeleteLocalRef(array); } void FilterClientCallbackImpl::onFilterStatus(const DemuxFilterStatus status) { @@ -938,6 +958,7 @@ void FilterClientCallbackImpl::onFilterStatus(const DemuxFilterStatus status) { methodID = gFields.onSharedFilterStatusID; } env->CallVoidMethod(filter, methodID, (jint)static_cast<uint8_t>(status)); + env->DeleteLocalRef(filter); } else { ALOGE("FilterClientCallbackImpl::onFilterStatus:" "Filter object has been freed. Ignoring callback."); @@ -1006,6 +1027,7 @@ void FrontendClientCallbackImpl::onEvent(FrontendEventType frontendEventType) { frontend, gFields.onFrontendEventID, (jint)frontendEventType); + env->DeleteLocalRef(frontend); } else { ALOGW("FrontendClientCallbackImpl::onEvent:" "Frontend object has been freed. Ignoring callback."); @@ -1028,6 +1050,7 @@ void FrontendClientCallbackImpl::onScanMessage( continue; } executeOnScanMessage(env, clazz, frontend, type, message); + env->DeleteLocalRef(frontend); } } @@ -1069,6 +1092,7 @@ void FrontendClientCallbackImpl::executeOnScanMessage( env->SetLongArrayRegion(freqs, 0, v.size(), reinterpret_cast<jlong *>(&v[0])); env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onFrequenciesReport", "([J)V"), freqs); + env->DeleteLocalRef(freqs); break; } case FrontendScanMessageType::SYMBOL_RATE: { @@ -1077,6 +1101,7 @@ void FrontendClientCallbackImpl::executeOnScanMessage( env->SetIntArrayRegion(symbolRates, 0, v.size(), reinterpret_cast<jint *>(&v[0])); env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onSymbolRates", "([I)V"), symbolRates); + env->DeleteLocalRef(symbolRates); break; } case FrontendScanMessageType::HIERARCHY: { @@ -1094,6 +1119,7 @@ void FrontendClientCallbackImpl::executeOnScanMessage( 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); break; } case FrontendScanMessageType::GROUP_IDS: { @@ -1101,6 +1127,7 @@ void FrontendClientCallbackImpl::executeOnScanMessage( 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); break; } case FrontendScanMessageType::INPUT_STREAM_IDS: { @@ -1109,6 +1136,7 @@ void FrontendClientCallbackImpl::executeOnScanMessage( env->SetIntArrayRegion(streamIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0])); env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onInputStreamIds", "([I)V"), streamIds); + env->DeleteLocalRef(streamIds); break; } case FrontendScanMessageType::STANDARD: { @@ -1142,12 +1170,14 @@ void FrontendClientCallbackImpl::executeOnScanMessage( jboolean lls = info.bLlsFlag; jobject obj = env->NewObject(plpClazz, init, plpId, lls); env->SetObjectArrayElement(array, i, obj); + env->DeleteLocalRef(obj); } env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onAtsc3PlpInfos", "([Landroid/media/tv/tuner/frontend/" "Atsc3PlpInfo;)V"), array); + env->DeleteLocalRef(array); break; } case FrontendScanMessageType::MODULATION: { @@ -1219,6 +1249,7 @@ void FrontendClientCallbackImpl::executeOnScanMessage( env->SetIntArrayRegion(cellIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0])); env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onDvbtCellIdsReported", "([I)V"), cellIds); + env->DeleteLocalRef(cellIds); break; } default: @@ -1673,6 +1704,7 @@ jobjectArray JTuner::getFrontendStatusReadiness(jintArray types) { 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); } return valObj; } @@ -2081,6 +2113,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, s.get<FrontendStatus::Tag::isDemodLocked>()); env->SetObjectField(statusObj, field, newBooleanObj); + env->DeleteLocalRef(newBooleanObj); break; } case FrontendStatus::Tag::snr: { @@ -2088,6 +2121,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::snr>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::ber: { @@ -2095,6 +2129,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::ber>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::per: { @@ -2102,6 +2137,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::per>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::preBer: { @@ -2109,6 +2145,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::preBer>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::signalQuality: { @@ -2116,6 +2153,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::signalQuality>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::signalStrength: { @@ -2124,6 +2162,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::signalStrength>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::symbolRate: { @@ -2131,6 +2170,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::symbolRate>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::innerFec: { @@ -2141,6 +2181,8 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->NewObject(longClazz, initLong, static_cast<long>(s.get<FrontendStatus::Tag::innerFec>())); env->SetObjectField(statusObj, field, newLongObj); + env->DeleteLocalRef(longClazz); + env->DeleteLocalRef(newLongObj); break; } case FrontendStatus::Tag::modulationStatus: { @@ -2183,6 +2225,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { if (valid) { jobject newIntegerObj = env->NewObject(intClazz, initInt, intModulation); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); } break; } @@ -2192,6 +2235,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->NewObject(intClazz, initInt, static_cast<jint>(s.get<FrontendStatus::Tag::inversion>())); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::lnbVoltage: { @@ -2200,6 +2244,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->NewObject(intClazz, initInt, static_cast<jint>(s.get<FrontendStatus::Tag::lnbVoltage>())); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::plpId: { @@ -2207,6 +2252,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::plpId>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::isEWBS: { @@ -2214,6 +2260,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, s.get<FrontendStatus::Tag::isEWBS>()); env->SetObjectField(statusObj, field, newBooleanObj); + env->DeleteLocalRef(newBooleanObj); break; } case FrontendStatus::Tag::agc: { @@ -2221,6 +2268,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::agc>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::isLnaOn: { @@ -2228,6 +2276,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, s.get<FrontendStatus::Tag::isLnaOn>()); env->SetObjectField(statusObj, field, newBooleanObj); + env->DeleteLocalRef(newBooleanObj); break; } case FrontendStatus::Tag::isLayerError: { @@ -2241,6 +2290,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->SetBooleanArrayRegion(valObj, i, 1, &x); } env->SetObjectField(statusObj, field, valObj); + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::mer: { @@ -2248,6 +2298,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::mer>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::freqOffset: { @@ -2255,6 +2306,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newLongObj = env->NewObject(longClazz, initLong, s.get<FrontendStatus::Tag::freqOffset>()); env->SetObjectField(statusObj, field, newLongObj); + env->DeleteLocalRef(newLongObj); break; } case FrontendStatus::Tag::hierarchy: { @@ -2263,6 +2315,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->NewObject(intClazz, initInt, static_cast<jint>(s.get<FrontendStatus::Tag::hierarchy>())); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::isRfLocked: { @@ -2270,6 +2323,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, s.get<FrontendStatus::Tag::isRfLocked>()); env->SetObjectField(statusObj, field, newBooleanObj); + env->DeleteLocalRef(newBooleanObj); break; } case FrontendStatus::Tag::plpInfo: { @@ -2289,9 +2343,12 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject plpObj = env->NewObject(plpClazz, initPlp, plpId, isLocked, uec); env->SetObjectArrayElement(valObj, i, plpObj); + env->DeleteLocalRef(plpObj); } env->SetObjectField(statusObj, field, valObj); + env->DeleteLocalRef(plpClazz); + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::modulations: { @@ -2374,6 +2431,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { if (valid) { env->SetObjectField(statusObj, field, valObj); } + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::bers: { @@ -2384,6 +2442,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&v[0])); env->SetObjectField(statusObj, field, valObj); + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::codeRates: { @@ -2394,6 +2453,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&v[0])); env->SetObjectField(statusObj, field, valObj); + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::bandwidth: { @@ -2434,6 +2494,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { if (valid) { jobject newIntegerObj = env->NewObject(intClazz, initInt, intBandwidth); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); } break; } @@ -2465,6 +2526,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { if (valid) { jobject newIntegerObj = env->NewObject(intClazz, initInt, intInterval); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); } break; } @@ -2497,6 +2559,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { if (valid) { jobject newIntegerObj = env->NewObject(intClazz, initInt, intTransmissionMode); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); } break; } @@ -2505,6 +2568,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::uec>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::systemId: { @@ -2512,6 +2576,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::systemId>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::interleaving: { @@ -2558,6 +2623,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { if (valid) { env->SetObjectField(statusObj, field, valObj); } + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::isdbtSegment: { @@ -2568,6 +2634,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint*>(&v[0])); env->SetObjectField(statusObj, field, valObj); + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::tsDataRate: { @@ -2578,6 +2645,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&v[0])); env->SetObjectField(statusObj, field, valObj); + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::rollOff: { @@ -2605,6 +2673,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { if (valid) { jobject newIntegerObj = env->NewObject(intClazz, initInt, intRollOff); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); } break; } @@ -2613,6 +2682,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, s.get<FrontendStatus::Tag::isMiso>()); env->SetObjectField(statusObj, field, newBooleanObj); + env->DeleteLocalRef(newBooleanObj); break; } case FrontendStatus::Tag::isLinear: { @@ -2620,6 +2690,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, s.get<FrontendStatus::Tag::isLinear>()); env->SetObjectField(statusObj, field, newBooleanObj); + env->DeleteLocalRef(newBooleanObj); break; } case FrontendStatus::Tag::isShortFrames: { @@ -2627,6 +2698,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean, s.get<FrontendStatus::Tag::isShortFrames>()); env->SetObjectField(statusObj, field, newBooleanObj); + env->DeleteLocalRef(newBooleanObj); break; } case FrontendStatus::Tag::isdbtMode: { @@ -2634,6 +2706,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject newIntegerObj = env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::isdbtMode>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::partialReceptionFlag: { @@ -2643,6 +2716,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::partialReceptionFlag>()); env->SetObjectField(statusObj, field, newIntegerObj); + env->DeleteLocalRef(newIntegerObj); break; } case FrontendStatus::Tag::streamIdList: { @@ -2653,6 +2727,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&ids[0])); env->SetObjectField(statusObj, field, valObj); + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::dvbtCellIds: { @@ -2663,6 +2738,7 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&ids[0])); env->SetObjectField(statusObj, field, valObj); + env->DeleteLocalRef(valObj); break; } case FrontendStatus::Tag::allPlpInfo: { @@ -2678,9 +2754,12 @@ jobject JTuner::getFrontendStatus(jintArray types) { jobject plpObj = env->NewObject(plpClazz, initPlp, plpInfos[i].plpId, plpInfos[i].bLlsFlag); env->SetObjectArrayElement(valObj, i, plpObj); + env->DeleteLocalRef(plpObj); } env->SetObjectField(statusObj, field, valObj); + env->DeleteLocalRef(plpClazz); + env->DeleteLocalRef(valObj); break; } } @@ -2837,6 +2916,7 @@ static vector<FrontendAtsc3PlpSettings> getAtsc3PlpSettings(JNIEnv *env, const j .fec = fec, }; plps[i] = frontendAtsc3PlpSettings; + env->DeleteLocalRef(plp); } return plps; } @@ -3192,6 +3272,7 @@ static FrontendSettings getIsdbtFrontendSettings(JNIEnv *env, const jobject& set env->GetIntField(layer, env->GetFieldID(layerClazz, "mCodeRate", "I"))); frontendIsdbtSettings.layerSettings[i].numOfSegment = env->GetIntField(layer, env->GetFieldID(layerClazz, "mNumOfSegments", "I")); + env->DeleteLocalRef(layer); } frontendSettings.set<FrontendSettings::Tag::isdbt>(frontendIsdbtSettings); diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java index 220aa33bf497..c524037fe444 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java @@ -28,6 +28,7 @@ import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.util.Log; import android.view.KeyEvent; +import android.webkit.URLUtil; import android.webkit.WebView; import com.android.phone.slice.SlicePurchaseController; @@ -60,36 +61,38 @@ public class SlicePurchaseActivity extends Activity { @NonNull private WebView mWebView; @NonNull private Context mApplicationContext; + @NonNull private Intent mIntent; + @Nullable private URL mUrl; private int mSubId; @TelephonyManager.PremiumCapability protected int mCapability; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Intent intent = getIntent(); - mSubId = intent.getIntExtra(SlicePurchaseController.EXTRA_SUB_ID, + mIntent = getIntent(); + mSubId = mIntent.getIntExtra(SlicePurchaseController.EXTRA_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID); - mCapability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY, + mCapability = mIntent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY, SlicePurchaseController.PREMIUM_CAPABILITY_INVALID); mApplicationContext = getApplicationContext(); - URL url = getUrl(); + mUrl = getUrl(); logd("onCreate: subId=" + mSubId + ", capability=" + TelephonyManager.convertPremiumCapabilityToString(mCapability) - + ", url=" + url); + + ", url=" + mUrl); // Cancel network boost notification mApplicationContext.getSystemService(NotificationManager.class) .cancel(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability); // Verify intent and values are valid - if (!SlicePurchaseBroadcastReceiver.isIntentValid(intent)) { - loge("Not starting SlicePurchaseActivity with an invalid Intent: " + intent); + if (!SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)) { + loge("Not starting SlicePurchaseActivity with an invalid Intent: " + mIntent); SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse( - intent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED); + mIntent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED); finishAndRemoveTask(); return; } - if (url == null) { + if (mUrl == null) { String error = "Unable to create a URL from carrier configs."; loge(error); Intent data = new Intent(); @@ -97,7 +100,7 @@ public class SlicePurchaseActivity extends Activity { SlicePurchaseController.FAILURE_CODE_CARRIER_URL_UNAVAILABLE); data.putExtra(SlicePurchaseController.EXTRA_FAILURE_REASON, error); SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext, - getIntent(), SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data); + mIntent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data); finishAndRemoveTask(); return; } @@ -105,7 +108,7 @@ public class SlicePurchaseActivity extends Activity { loge("Unable to start the slice purchase application on the non-default data " + "subscription: " + mSubId); SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse( - intent, SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION); + mIntent, SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION); finishAndRemoveTask(); return; } @@ -114,16 +117,7 @@ public class SlicePurchaseActivity extends Activity { SlicePurchaseBroadcastReceiver.updateSlicePurchaseActivity(mCapability, this); // Create and configure WebView - mWebView = new WebView(this); - // Enable JavaScript for the carrier purchase website to send results back to - // the slice purchase application. - mWebView.getSettings().setJavaScriptEnabled(true); - mWebView.addJavascriptInterface( - new SlicePurchaseWebInterface(this), "SlicePurchaseWebInterface"); - - // Display WebView - setContentView(mWebView); - mWebView.loadUrl(url.toString()); + setupWebView(); } protected void onPurchaseSuccessful(long duration) { @@ -134,7 +128,7 @@ public class SlicePurchaseActivity extends Activity { Intent intent = new Intent(); intent.putExtra(SlicePurchaseController.EXTRA_PURCHASE_DURATION, duration); SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext, - getIntent(), SlicePurchaseController.EXTRA_INTENT_SUCCESS, intent); + mIntent, SlicePurchaseController.EXTRA_INTENT_SUCCESS, intent); finishAndRemoveTask(); } @@ -147,7 +141,7 @@ public class SlicePurchaseActivity extends Activity { data.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE, failureCode); data.putExtra(SlicePurchaseController.EXTRA_FAILURE_REASON, failureReason); SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext, - getIntent(), SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data); + mIntent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data); finishAndRemoveTask(); } @@ -166,7 +160,7 @@ public class SlicePurchaseActivity extends Activity { protected void onDestroy() { logd("onDestroy: User canceled the purchase by closing the application."); SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse( - getIntent(), SlicePurchaseController.EXTRA_INTENT_CANCELED); + mIntent, SlicePurchaseController.EXTRA_INTENT_CANCELED); SlicePurchaseBroadcastReceiver.removeSlicePurchaseActivity(mCapability); super.onDestroy(); } @@ -175,14 +169,37 @@ public class SlicePurchaseActivity extends Activity { String url = mApplicationContext.getSystemService(CarrierConfigManager.class) .getConfigForSubId(mSubId).getString( CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING); - try { - return new URL(url); - } catch (MalformedURLException e) { - loge("Invalid URL: " + url); + boolean isUrlValid = URLUtil.isValidUrl(url); + if (URLUtil.isAssetUrl(url)) { + isUrlValid = url.equals(SlicePurchaseController.SLICE_PURCHASE_TEST_FILE); } + if (isUrlValid) { + try { + return new URL(url); + } catch (MalformedURLException ignored) { + } + } + loge("Invalid URL: " + url); return null; } + private void setupWebView() { + // Create WebView + mWebView = new WebView(this); + + // Enable JavaScript for the carrier purchase website to send results back to + // the slice purchase application. + mWebView.getSettings().setJavaScriptEnabled(true); + mWebView.addJavascriptInterface( + new SlicePurchaseWebInterface(this), "SlicePurchaseWebInterface"); + + // Display WebView + setContentView(mWebView); + + // Load the URL + mWebView.loadUrl(mUrl.toString()); + } + private static void logd(@NonNull String s) { Log.d(TAG, s); } diff --git a/packages/CredentialManager/res/values/themes.xml b/packages/CredentialManager/res/values/themes.xml index feec74608000..a58a0383b9b2 100644 --- a/packages/CredentialManager/res/values/themes.xml +++ b/packages/CredentialManager/res/values/themes.xml @@ -2,11 +2,12 @@ <resources> <style name="Theme.CredentialSelector" parent="@android:style/ThemeOverlay.Material"> - <item name="android:statusBarColor">@color/purple_700</item> + <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:windowContentOverlay">@null</item> <item name="android:windowNoTitle">true</item> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsTranslucent">true</item> <item name="android:colorBackgroundCacheHint">@null</item> + <item name="fontFamily">google-sans</item> </style> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 8bd7cf03008b..b848a47f37de 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -38,12 +38,15 @@ import android.os.Binder import android.os.Bundle import android.os.ResultReceiver import com.android.credentialmanager.createflow.ActiveEntry -import com.android.credentialmanager.createflow.CreatePasskeyUiState +import com.android.credentialmanager.createflow.CreateCredentialUiState import com.android.credentialmanager.createflow.CreateScreenState import com.android.credentialmanager.createflow.EnabledProviderInfo import com.android.credentialmanager.createflow.RequestDisplayInfo import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.GetScreenState +import com.android.credentialmanager.jetpack.developer.CreateCredentialRequest.Companion.createFrom +import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest +import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL // Consider repo per screen, similar to view model? @@ -123,7 +126,7 @@ class CredentialManagerRepo( ) } - fun createPasskeyInitialUiState(): CreatePasskeyUiState { + fun createCredentialInitialUiState(): CreateCredentialUiState { val providerEnabledList = CreateFlowUtils.toEnabledProviderList( // Handle runtime cast error providerEnabledList as List<CreateCredentialProviderData>, context) @@ -135,13 +138,22 @@ class CredentialManagerRepo( providerEnabledList.forEach{providerInfo -> providerInfo.createOptions = providerInfo.createOptions.sortedWith(compareBy { it.lastUsedTimeMillis }).reversed() if (providerInfo.isDefault) {hasDefault = true; defaultProvider = providerInfo} } - // TODO: covert from real requestInfo - val requestDisplayInfo = RequestDisplayInfo( + // TODO: covert from real requestInfo for create passkey + var requestDisplayInfo = RequestDisplayInfo( "Elisa Beckett", "beckett-bakert@gmail.com", TYPE_PUBLIC_KEY_CREDENTIAL, "tribank") - return CreatePasskeyUiState( + val createCredentialRequest = requestInfo.createCredentialRequest + val createCredentialRequestJetpack = createCredentialRequest?.let { createFrom(it) } + if (createCredentialRequestJetpack is CreatePasswordRequest) { + requestDisplayInfo = RequestDisplayInfo( + createCredentialRequestJetpack.id, + createCredentialRequestJetpack.password, + TYPE_PASSWORD_CREDENTIAL, + "tribank") + } + return CreateCredentialUiState( enabledProviders = providerEnabledList, disabledProviders = providerDisabledList, if (hasDefault) @@ -388,15 +400,15 @@ class CredentialManagerRepo( } private fun testCreateRequestInfo(): RequestInfo { - val data = Bundle() + val data = toBundle("beckett-bakert@gmail.com", "password123") return RequestInfo.newCreateRequestInfo( Binder(), CreateCredentialRequest( - TYPE_PUBLIC_KEY_CREDENTIAL, + TYPE_PASSWORD_CREDENTIAL, data ), /*isFirstUsage=*/false, - "tribank.us" + "tribank" ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 78edaa936bcd..1041a33333b3 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -28,8 +28,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.android.credentialmanager.common.DialogType import com.android.credentialmanager.common.DialogResult import com.android.credentialmanager.common.ResultState -import com.android.credentialmanager.createflow.CreatePasskeyScreen -import com.android.credentialmanager.createflow.CreatePasskeyViewModel +import com.android.credentialmanager.createflow.CreateCredentialScreen +import com.android.credentialmanager.createflow.CreateCredentialViewModel import com.android.credentialmanager.getflow.GetCredentialScreen import com.android.credentialmanager.getflow.GetCredentialViewModel import com.android.credentialmanager.ui.theme.CredentialSelectorTheme @@ -63,12 +63,12 @@ class CredentialSelectorActivity : ComponentActivity() { val dialogType = DialogType.toDialogType(operationType) when (dialogType) { DialogType.CREATE_PASSKEY -> { - val viewModel: CreatePasskeyViewModel = viewModel() + val viewModel: CreateCredentialViewModel = viewModel() viewModel.observeDialogResult().observe( this@CredentialSelectorActivity, onCancel ) - CreatePasskeyScreen(viewModel = viewModel) + CreateCredentialScreen(viewModel = viewModel) } DialogType.GET_CREDENTIALS -> { val viewModel: GetCredentialViewModel = viewModel() diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 33fb154f44f0..df797436baf6 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -16,6 +16,7 @@ package com.android.credentialmanager +import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import android.credentials.ui.Entry @@ -146,9 +147,17 @@ class CreateFlowUtils { ): List<com.android.credentialmanager.createflow.EnabledProviderInfo> { // TODO: get from the actual service info val packageManager = context.packageManager + return providerDataList.map { + val componentName = ComponentName.unflattenFromString(it.providerFlattenedComponentName) + var packageName = componentName?.packageName + if (componentName == null) { + // TODO: Remove once test data is fixed + packageName = it.providerFlattenedComponentName + } + val pkgInfo = packageManager - .getPackageInfo(it.providerFlattenedComponentName, + .getPackageInfo(packageName!!, PackageManager.PackageInfoFlags.of(0)) com.android.credentialmanager.createflow.EnabledProviderInfo( // TODO: decide what to do when failed to load a provider icon diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt index f1f453da4f38..61e11feff3c0 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt @@ -62,6 +62,7 @@ import com.android.credentialmanager.R import com.android.credentialmanager.common.material.ModalBottomSheetValue.Expanded import com.android.credentialmanager.common.material.ModalBottomSheetValue.HalfExpanded import com.android.credentialmanager.common.material.ModalBottomSheetValue.Hidden +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import kotlin.math.max @@ -318,7 +319,7 @@ fun ModalBottomSheetLayout( rememberModalBottomSheetState(Hidden), sheetShape: Shape = MaterialTheme.shapes.large, sheetElevation: Dp = ModalBottomSheetDefaults.Elevation, - sheetBackgroundColor: Color = MaterialTheme.colorScheme.surface, + sheetBackgroundColor: Color = ModalBottomSheetDefaults.scrimColor, sheetContentColor: Color = contentColorFor(sheetBackgroundColor), scrimColor: Color = ModalBottomSheetDefaults.scrimColor, content: @Composable () -> Unit @@ -476,5 +477,5 @@ object ModalBottomSheetDefaults { */ val scrimColor: Color @Composable - get() = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f) + get() = LocalAndroidColorScheme.current.colorSurfaceHighlight }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt new file mode 100644 index 000000000000..51a1cbbbf942 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -0,0 +1,68 @@ +/* + * 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.credentialmanager.common.ui + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.SuggestionChipDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.android.credentialmanager.ui.theme.EntryShape +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Entry( + onClick: () -> Unit, + label: @Composable () -> Unit, + modifier: Modifier = Modifier, + icon: @Composable (() -> Unit)? = null, +) { + SuggestionChip( + modifier = modifier.fillMaxWidth(), + onClick = onClick, + shape = EntryShape.FullSmallRoundedCorner, + label = label, + icon = icon, + border = null, + colors = SuggestionChipDefaults.suggestionChipColors( + containerColor = LocalAndroidColorScheme.current.colorSurface, + ), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TransparentBackgroundEntry( + onClick: () -> Unit, + label: @Composable () -> Unit, + modifier: Modifier = Modifier, + icon: @Composable (() -> Unit)? = null, +) { + SuggestionChip( + modifier = modifier.fillMaxWidth(), + onClick = onClick, + label = label, + icon = icon, + border = null, + colors = SuggestionChipDefaults.suggestionChipColors( + containerColor = Color.Transparent, + ), + ) +}
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 67b704f5d787..27d366d55937 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package com.android.credentialmanager.createflow import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL @@ -15,10 +17,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.outlined.NewReleases @@ -40,12 +42,14 @@ import com.android.credentialmanager.common.material.ModalBottomSheetValue import com.android.credentialmanager.common.material.rememberModalBottomSheetState import com.android.credentialmanager.common.ui.CancelButton import com.android.credentialmanager.common.ui.ConfirmButton +import com.android.credentialmanager.common.ui.Entry +import com.android.credentialmanager.ui.theme.EntryShape import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CreatePasskeyScreen( - viewModel: CreatePasskeyViewModel, +fun CreateCredentialScreen( + viewModel: CreateCredentialViewModel, ) { val state = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Expanded, @@ -91,7 +95,7 @@ fun CreatePasskeyScreen( } }, scrimColor = MaterialTheme.colorScheme.scrim, - sheetShape = MaterialTheme.shapes.medium, + sheetShape = EntryShape.TopRoundedCorner, ) {} LaunchedEffect(state.currentValue) { if (state.currentValue == ModalBottomSheetValue.Hidden) { @@ -100,6 +104,7 @@ fun CreatePasskeyScreen( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConfirmationCard( onConfirm: () -> Unit, @@ -179,7 +184,7 @@ fun ProviderSelectionCard( color = Color.Transparent ) Card( - shape = MaterialTheme.shapes.large, + shape = MaterialTheme.shapes.medium, modifier = Modifier .padding(horizontal = 24.dp) .align(alignment = Alignment.CenterHorizontally), @@ -243,14 +248,16 @@ fun MoreOptionsSelectionCard( Icons.Filled.ArrowBack, stringResource(R.string.accessibility_back_arrow_button)) } - } + }, + colors = TopAppBarDefaults.smallTopAppBarColors + (containerColor = Color.Transparent), ) Divider( thickness = 8.dp, color = Color.Transparent ) Card( - shape = MaterialTheme.shapes.large, + shape = MaterialTheme.shapes.medium, modifier = Modifier .padding(horizontal = 24.dp) .align(alignment = Alignment.CenterHorizontally) @@ -349,20 +356,17 @@ fun MoreOptionsRowIntroCard( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), + Entry( onClick = {onProviderSelected(providerInfo.name)}, icon = { - Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + Image(modifier = Modifier.size(32.dp).padding(start = 10.dp), bitmap = providerInfo.icon.toBitmap().asImageBitmap(), // painter = painterResource(R.drawable.ic_passkey), // TODO: add description. contentDescription = "") }, - shape = MaterialTheme.shapes.large, label = { Text( text = providerInfo.displayName, @@ -391,7 +395,8 @@ fun CreationSelectionCard( bitmap = providerInfo.icon.toBitmap().asImageBitmap(), contentDescription = null, tint = Color.Unspecified, - modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(all = 24.dp) + modifier = Modifier.align(alignment = Alignment.CenterHorizontally) + .padding(all = 24.dp).size(32.dp) ) Text( text = when (requestDisplayInfo.type) { @@ -425,7 +430,7 @@ fun CreationSelectionCard( ) } Card( - shape = MaterialTheme.shapes.large, + shape = MaterialTheme.shapes.medium, modifier = Modifier .padding(horizontal = 24.dp) .align(alignment = Alignment.CenterHorizontally), @@ -491,27 +496,35 @@ fun PrimaryCreateOptionRow( createOptionInfo: CreateOptionInfo, onOptionSelected: () -> Unit ) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), + Entry( onClick = onOptionSelected, icon = { - Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + Image(modifier = Modifier.size(32.dp).padding(start = 10.dp), bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), contentDescription = null) }, - shape = MaterialTheme.shapes.large, label = { Column() { - Text( - text = requestDisplayInfo.userName, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(top = 16.dp) - ) - Text( - text = requestDisplayInfo.displayName, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) - ) + // TODO: Add the function to hide/view password when the type is create password + if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL || + requestDisplayInfo.type == TYPE_PASSWORD_CREDENTIAL) { + Text( + text = requestDisplayInfo.title, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 16.dp) + ) + Text( + text = requestDisplayInfo.subtitle, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + } else { + Text( + text = requestDisplayInfo.subtitle, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 16.dp, bottom = 16.dp) + ) + } } } ) @@ -524,15 +537,13 @@ fun MoreOptionsInfoRow( createOptionInfo: CreateOptionInfo, onOptionSelected: () -> Unit ) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), + Entry( onClick = onOptionSelected, icon = { - Image(modifier = Modifier.size(32.dp, 32.dp).padding(start = 16.dp), + Image(modifier = Modifier.size(32.dp).padding(start = 16.dp), bitmap = providerInfo.icon.toBitmap().asImageBitmap(), contentDescription = null) }, - shape = MaterialTheme.shapes.large, label = { Column() { Text( @@ -593,8 +604,7 @@ fun MoreOptionsDisabledProvidersRow( disabledProviders: List<ProviderInfo>, onDisabledPasswordManagerSelected: () -> Unit, ) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), + Entry( onClick = onDisabledPasswordManagerSelected, icon = { Icon( @@ -603,7 +613,6 @@ fun MoreOptionsDisabledProvidersRow( modifier = Modifier.padding(start = 16.dp) ) }, - shape = MaterialTheme.shapes.large, label = { Column() { Text( @@ -626,8 +635,7 @@ fun MoreOptionsDisabledProvidersRow( fun RemoteEntryRow( onRemoteEntrySelected: () -> Unit, ) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), + Entry( onClick = onRemoteEntrySelected, icon = { Icon( @@ -637,7 +645,6 @@ fun RemoteEntryRow( modifier = Modifier.padding(start = 18.dp) ) }, - shape = MaterialTheme.shapes.large, label = { Column() { Text( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt index af74b8ea4de1..6be019fa0882 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt @@ -27,7 +27,7 @@ import com.android.credentialmanager.CredentialManagerRepo import com.android.credentialmanager.common.DialogResult import com.android.credentialmanager.common.ResultState -data class CreatePasskeyUiState( +data class CreateCredentialUiState( val enabledProviders: List<EnabledProviderInfo>, val disabledProviders: List<DisabledProviderInfo>? = null, val currentScreenState: CreateScreenState, @@ -35,11 +35,11 @@ data class CreatePasskeyUiState( val activeEntry: ActiveEntry? = null, ) -class CreatePasskeyViewModel( +class CreateCredentialViewModel( credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance() ) : ViewModel() { - var uiState by mutableStateOf(credManRepo.createPasskeyInitialUiState()) + var uiState by mutableStateOf(credManRepo.createCredentialInitialUiState()) private set val dialogResult: MutableLiveData<DialogResult> by lazy { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 123c3d454905..1ab234a0e0bc 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -62,8 +62,8 @@ class RemoteInfo( ) : EntryInfo(entryKey, entrySubkey) data class RequestDisplayInfo( - val userName: String, - val displayName: String, + val title: String, + val subtitle: String, val type: String, val appDomainName: String, ) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index dcdd71a283a8..6fd51dd3b69a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card @@ -33,7 +34,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -46,6 +46,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.android.credentialmanager.R @@ -53,6 +54,8 @@ import com.android.credentialmanager.common.material.ModalBottomSheetLayout import com.android.credentialmanager.common.material.ModalBottomSheetValue import com.android.credentialmanager.common.material.rememberModalBottomSheetState import com.android.credentialmanager.common.ui.CancelButton +import com.android.credentialmanager.common.ui.Entry +import com.android.credentialmanager.common.ui.TransparentBackgroundEntry import com.android.credentialmanager.jetpack.developer.PublicKeyCredential @Composable @@ -107,6 +110,9 @@ fun PrimarySelectionCard( Card() { Column() { Text( + modifier = Modifier.padding(all = 24.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineSmall, text = stringResource( if (sortedUserNameToCredentialEntryList.size == 1) { if (sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList @@ -117,12 +123,10 @@ fun PrimarySelectionCard( } else R.string.get_dialog_title_choose_sign_in_for, requestDisplayInfo.appDomainName ), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally) ) Card( - shape = MaterialTheme.shapes.large, + shape = MaterialTheme.shapes.medium, modifier = Modifier .padding(horizontal = 24.dp) .align(alignment = Alignment.CenterHorizontally) @@ -254,9 +258,14 @@ fun ActionChips( modifier = Modifier.padding(vertical = 8.dp) ) // TODO: tweak padding. - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - actionChips.forEach { - ActionEntryRow(it, onEntrySelected) + Card( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + shape = MaterialTheme.shapes.medium, + ) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + actionChips.forEach { + ActionEntryRow(it, onEntrySelected) + } } } } @@ -271,8 +280,18 @@ fun LockedCredentials( style = MaterialTheme.typography.labelLarge, modifier = Modifier.padding(vertical = 8.dp) ) - authenticationEntryList.forEach { - AuthenticationEntryRow(it, onEntrySelected) + Card( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + shape = MaterialTheme.shapes.medium, + ) { + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + authenticationEntryList.forEach { + AuthenticationEntryRow(it, onEntrySelected) + } + } } } @@ -287,8 +306,18 @@ fun PerUserNameCredentials( style = MaterialTheme.typography.labelLarge, modifier = Modifier.padding(vertical = 8.dp) ) - perUserNameCredentialEntryList.sortedCredentialEntryList.forEach { - CredentialEntryRow(it, onEntrySelected) + Card( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + shape = MaterialTheme.shapes.medium, + ) { + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + perUserNameCredentialEntryList.sortedCredentialEntryList.forEach { + CredentialEntryRow(it, onEntrySelected) + } + } } } @@ -298,16 +327,14 @@ fun CredentialEntryRow( credentialEntryInfo: CredentialEntryInfo, onEntrySelected: (EntryInfo) -> Unit, ) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), + Entry( onClick = {onEntrySelected(credentialEntryInfo)}, icon = { - Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + Image(modifier = Modifier.padding(start = 10.dp).size(32.dp), bitmap = credentialEntryInfo.icon.toBitmap().asImageBitmap(), // TODO: add description. contentDescription = "") }, - shape = MaterialTheme.shapes.large, label = { Column() { // TODO: fix the text values. @@ -338,16 +365,14 @@ fun AuthenticationEntryRow( authenticationEntryInfo: AuthenticationEntryInfo, onEntrySelected: (EntryInfo) -> Unit, ) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), + Entry( onClick = {onEntrySelected(authenticationEntryInfo)}, icon = { - Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + Image(modifier = Modifier.padding(start = 10.dp).size(32.dp), bitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(), // TODO: add description. contentDescription = "") }, - shape = MaterialTheme.shapes.large, label = { Column() { // TODO: fix the text values. @@ -372,16 +397,13 @@ fun ActionEntryRow( actionEntryInfo: ActionEntryInfo, onEntrySelected: (EntryInfo) -> Unit, ) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), - onClick = { onEntrySelected(actionEntryInfo) }, + TransparentBackgroundEntry( icon = { - Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), + Image(modifier = Modifier.padding(start = 10.dp).size(32.dp), bitmap = actionEntryInfo.icon.toBitmap().asImageBitmap(), // TODO: add description. contentDescription = "") }, - shape = MaterialTheme.shapes.large, label = { Column() { Text( @@ -395,17 +417,16 @@ fun ActionEntryRow( ) } } - } + }, + onClick = { onEntrySelected(actionEntryInfo) }, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun SignInAnotherWayRow(onSelect: () -> Unit) { - SuggestionChip( - modifier = Modifier.fillMaxWidth(), + Entry( onClick = onSelect, - shape = MaterialTheme.shapes.large, label = { Text( text = stringResource(R.string.get_dialog_use_saved_passkey_for), diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index c1d9ea9b9188..c541e08ffbb4 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -64,8 +64,6 @@ class CredentialEntryInfo( val lastUsedTimeMillis: Long?, ) : EntryInfo(providerId, entryKey, entrySubkey) -// TODO: handle sub credential type values like password obfuscation. - class AuthenticationEntryInfo( providerId: String, entryKey: String, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt new file mode 100644 index 000000000000..15ae3295416b --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt @@ -0,0 +1,75 @@ +/* + * 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.credentialmanager.ui.theme + +import android.annotation.ColorInt +import android.content.Context +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import com.android.internal.R + +/** CompositionLocal used to pass [AndroidColorScheme] down the tree. */ +val LocalAndroidColorScheme = + staticCompositionLocalOf<AndroidColorScheme> { + throw IllegalStateException( + "No AndroidColorScheme configured. Make sure to use LocalAndroidColorScheme in a " + + "Composable surrounded by a CredentialSelectorTheme {}." + ) + } + +/** + * The Android color scheme. + * + * Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future, + * most of the colors in this class will be removed in favor of their M3 counterpart. + */ +class AndroidColorScheme internal constructor(context: Context) { + + val colorPrimary = getColor(context, R.attr.colorPrimary) + val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark) + val colorAccent = getColor(context, R.attr.colorAccent) + val colorAccentPrimary = getColor(context, R.attr.colorAccentPrimary) + val colorAccentSecondary = getColor(context, R.attr.colorAccentSecondary) + val colorAccentTertiary = getColor(context, R.attr.colorAccentTertiary) + val colorAccentPrimaryVariant = getColor(context, R.attr.colorAccentPrimaryVariant) + val colorAccentSecondaryVariant = getColor(context, R.attr.colorAccentSecondaryVariant) + val colorAccentTertiaryVariant = getColor(context, R.attr.colorAccentTertiaryVariant) + val colorSurface = getColor(context, R.attr.colorSurface) + val colorSurfaceHighlight = getColor(context, R.attr.colorSurfaceHighlight) + val colorSurfaceVariant = getColor(context, R.attr.colorSurfaceVariant) + val colorSurfaceHeader = getColor(context, R.attr.colorSurfaceHeader) + val colorError = getColor(context, R.attr.colorError) + val colorBackground = getColor(context, R.attr.colorBackground) + val colorBackgroundFloating = getColor(context, R.attr.colorBackgroundFloating) + val panelColorBackground = getColor(context, R.attr.panelColorBackground) + val textColorPrimary = getColor(context, R.attr.textColorPrimary) + val textColorSecondary = getColor(context, R.attr.textColorSecondary) + val textColorTertiary = getColor(context, R.attr.textColorTertiary) + val textColorPrimaryInverse = getColor(context, R.attr.textColorPrimaryInverse) + val textColorSecondaryInverse = getColor(context, R.attr.textColorSecondaryInverse) + val textColorTertiaryInverse = getColor(context, R.attr.textColorTertiaryInverse) + val textColorOnAccent = getColor(context, R.attr.textColorOnAccent) + val colorForeground = getColor(context, R.attr.colorForeground) + val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse) + + private fun getColor(context: Context, attr: Int): Color { + val ta = context.obtainStyledAttributes(intArrayOf(attr)) + @ColorInt val color = ta.getColor(0, 0) + ta.recycle() + return Color(color) + } +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt index 5ea69930e334..d8a8f162ba40 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt @@ -6,6 +6,15 @@ import androidx.compose.ui.unit.dp val Shapes = Shapes( small = RoundedCornerShape(100.dp), - medium = RoundedCornerShape(20.dp), + medium = RoundedCornerShape(28.dp), large = RoundedCornerShape(0.dp) ) + +object EntryShape { + val TopRoundedCorner = RoundedCornerShape(28.dp, 28.dp, 0.dp, 0.dp) + val BottomRoundedCorner = RoundedCornerShape(0.dp, 0.dp, 28.dp, 28.dp) + // Used for middle entries. + val FullSmallRoundedCorner = RoundedCornerShape(4.dp, 4.dp, 4.dp, 4.dp) + // Used for when there's a single entry. + val FullMediumRoundedCorner = RoundedCornerShape(28.dp, 28.dp, 28.dp, 28.dp) +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt index 248df92bac59..3ca0e4494ab6 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt @@ -2,37 +2,37 @@ package com.android.credentialmanager.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable - -private val AppDarkColorScheme = darkColorScheme( - primary = Purple200, - secondary = Purple700, - tertiary = Teal200 -) - -private val AppLightColorScheme = lightColorScheme( - primary = Purple500, - secondary = Purple700, - tertiary = Teal200 -) +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext @Composable fun CredentialSelectorTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { - val AppColorScheme = if (darkTheme) { - AppDarkColorScheme - } else { - AppLightColorScheme - } + val context = LocalContext.current + + val colorScheme = + if (darkTheme) { + dynamicDarkColorScheme(context) + } else { + dynamicLightColorScheme(context) + } + val androidColorScheme = AndroidColorScheme(context) + val typography = Typography MaterialTheme( - colorScheme = AppColorScheme, - typography = Typography, - shapes = Shapes, - content = content - ) + colorScheme, + typography = typography, + shapes = Shapes + ) { + CompositionLocalProvider( + LocalAndroidColorScheme provides androidColorScheme, + ) { + content() + } + } } diff --git a/packages/DynamicSystemInstallationService/res/values-en-rCA/strings.xml b/packages/DynamicSystemInstallationService/res/values-en-rCA/strings.xml index 62dba985f715..08672938ff77 100644 --- a/packages/DynamicSystemInstallationService/res/values-en-rCA/strings.xml +++ b/packages/DynamicSystemInstallationService/res/values-en-rCA/strings.xml @@ -3,8 +3,8 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="keyguard_description" msgid="8582605799129954556">"Please enter your password and continue to Dynamic System Updates"</string> <string name="notification_install_completed" msgid="6252047868415172643">"Dynamic system is ready. To start using it, restart your device."</string> - <string name="notification_install_inprogress" msgid="7383334330065065017">"Installation in progress"</string> - <string name="notification_install_failed" msgid="4066039210317521404">"Installation failed"</string> + <string name="notification_install_inprogress" msgid="7383334330065065017">"Install in progress"</string> + <string name="notification_install_failed" msgid="4066039210317521404">"Install failed"</string> <string name="notification_image_validation_failed" msgid="2720357826403917016">"Image validation failed. Abort installation."</string> <string name="notification_dynsystem_in_use" msgid="1053194595682188396">"Currently running a dynamic system. Restart to use the original Android version."</string> <string name="notification_action_cancel" msgid="5929299408545961077">"Cancel"</string> diff --git a/packages/InputDevices/res/values-en-rCA/strings.xml b/packages/InputDevices/res/values-en-rCA/strings.xml index ab48729d5006..116178300348 100644 --- a/packages/InputDevices/res/values-en-rCA/strings.xml +++ b/packages/InputDevices/res/values-en-rCA/strings.xml @@ -19,7 +19,7 @@ <string name="keyboard_layout_swiss_german_label" msgid="2305520941993314258">"Swiss German"</string> <string name="keyboard_layout_belgian" msgid="2011984572838651558">"Belgian"</string> <string name="keyboard_layout_bulgarian" msgid="8951224309972028398">"Bulgarian"</string> - <string name="keyboard_layout_bulgarian_phonetic" msgid="7568914730360106653">"Bulgarian, phonetic"</string> + <string name="keyboard_layout_bulgarian_phonetic" msgid="7568914730360106653">"Bulgarian, Phonetic"</string> <string name="keyboard_layout_italian" msgid="6497079660449781213">"Italian"</string> <string name="keyboard_layout_danish" msgid="8036432066627127851">"Danish"</string> <string name="keyboard_layout_norwegian" msgid="9090097917011040937">"Norwegian"</string> diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp index 2eaa73e8b21c..eb7aaa71fc7b 100644 --- a/packages/SettingsLib/Spa/spa/Android.bp +++ b/packages/SettingsLib/Spa/spa/Android.bp @@ -40,7 +40,6 @@ android_library { ], kotlincflags: [ "-Xjvm-default=all", - "-opt-in=kotlin.RequiresOptIn", ], min_sdk_version: "31", } diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 4944784c190c..854359641bc0 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -44,7 +44,7 @@ android { } kotlinOptions { jvmTarget = '1.8' - freeCompilerArgs = ["-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn"] + freeCompilerArgs = ["-Xjvm-default=all"] } buildFeatures { compose true diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle index cbfbb9ccbe90..3e50b2990309 100644 --- a/packages/SettingsLib/Spa/testutils/build.gradle +++ b/packages/SettingsLib/Spa/testutils/build.gradle @@ -42,7 +42,7 @@ android { } kotlinOptions { jvmTarget = '1.8' - freeCompilerArgs = ["-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn"] + freeCompilerArgs = ["-Xjvm-default=all"] } } diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp index 18ae09ea4e1e..4a7418fd101d 100644 --- a/packages/SettingsLib/SpaPrivileged/Android.bp +++ b/packages/SettingsLib/SpaPrivileged/Android.bp @@ -30,7 +30,6 @@ android_library { ], kotlincflags: [ "-Xjvm-default=all", - "-opt-in=kotlin.RequiresOptIn", ], } diff --git a/packages/SettingsLib/SpaPrivileged/tests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/Android.bp index 12955c887480..5cd74e30c207 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/Android.bp +++ b/packages/SettingsLib/SpaPrivileged/tests/Android.bp @@ -39,7 +39,4 @@ android_test { "mockito-target-minus-junit4", "truth-prebuilt", ], - kotlincflags: [ - "-opt-in=kotlin.RequiresOptIn", - ], } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 950ee21ae7b5..9583a59148fc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -668,6 +668,12 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> * @param bluetoothProfile the Bluetooth profile */ public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) { + if (BluetoothUtils.D) { + Log.d(TAG, "onActiveDeviceChanged: " + + "profile " + BluetoothProfile.getProfileName(bluetoothProfile) + + ", device " + mDevice.getAnonymizedAddress() + + ", isActive " + isActive); + } boolean changed = false; switch (bluetoothProfile) { case BluetoothProfile.A2DP: diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index fa96a2f0ee7f..0b7b2f935e91 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -112,10 +112,10 @@ import com.android.internal.os.BackgroundThread; import com.android.internal.util.FrameworkStatsLog; import com.android.providers.settings.SettingsState.Setting; -import libcore.util.HexEncoding; - import com.google.android.collect.Sets; +import libcore.util.HexEncoding; + import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; @@ -1144,7 +1144,7 @@ public class SettingsProvider extends ContentProvider { Slog.v(LOG_TAG, "getConfigSetting(" + name + ")"); } - DeviceConfig.enforceReadPermission(getContext(), /*namespace=*/name.split("/")[0]); + DeviceConfig.enforceReadPermission(/*namespace=*/name.split("/")[0]); // Get the value. synchronized (mLock) { @@ -1317,7 +1317,7 @@ public class SettingsProvider extends ContentProvider { Slog.v(LOG_TAG, "getAllConfigFlags() for " + prefix); } - DeviceConfig.enforceReadPermission(getContext(), + DeviceConfig.enforceReadPermission( prefix != null ? prefix.split("/")[0] : null); synchronized (mLock) { diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 2e4a245df6a6..01c080990cfd 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -771,6 +771,9 @@ <!-- Permissions required for CTS test - CtsAppFgsTestCases --> <uses-permission android:name="android.permission.USE_EXACT_ALARM" /> + <!-- Permission required for CTS test - ApplicationExemptionsTests --> + <uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" android:defaultToDeviceProtectedStorage="true" diff --git a/packages/Shell/res/values-en-rCA/strings.xml b/packages/Shell/res/values-en-rCA/strings.xml index 546281360131..65ab725da36e 100644 --- a/packages/Shell/res/values-en-rCA/strings.xml +++ b/packages/Shell/res/values-en-rCA/strings.xml @@ -28,7 +28,7 @@ <string name="bugreport_finished_pending_screenshot_text" product="tv" msgid="2343263822812016950">"Select to share your bug report without a screenshot or wait for the screenshot to finish"</string> <string name="bugreport_finished_pending_screenshot_text" product="watch" msgid="1474435374470177193">"Tap to share your bug report without a screenshot or wait for the screenshot to finish"</string> <string name="bugreport_finished_pending_screenshot_text" product="default" msgid="1474435374470177193">"Tap to share your bug report without a screenshot or wait for the screenshot to finish"</string> - <string name="bugreport_confirm" msgid="5917407234515812495">"Bug reports contain data from the system\'s various log files, which may include data that you consider sensitive (such as app-usage and location data). Only share bug reports with people and apps that you trust."</string> + <string name="bugreport_confirm" msgid="5917407234515812495">"Bug reports contain data from the system\'s various log files, which may include data you consider sensitive (such as app-usage and location data). Only share bug reports with people and apps you trust."</string> <string name="bugreport_confirm_dont_repeat" msgid="6179945398364357318">"Don\'t show again"</string> <string name="bugreport_storage_title" msgid="5332488144740527109">"Bug reports"</string> <string name="bugreport_unreadable_text" msgid="586517851044535486">"Bug report file could not be read"</string> diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt index 1d808ba7ee16..74e6d85f5374 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt @@ -59,11 +59,11 @@ class BindServiceOnMainThreadDetector : Detector(), SourceCodeScanner { !hasWorkerThreadAnnotation(context, node.getParentOfType(UClass::class.java)) ) { context.report( - ISSUE, - method, - context.getLocation(node), - "This method should be annotated with `@WorkerThread` because " + - "it calls ${method.name}", + issue = ISSUE, + location = context.getLocation(node), + message = + "This method should be annotated with `@WorkerThread` because " + + "it calls ${method.name}", ) } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt index 112992913661..344d0a3f3187 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt @@ -52,10 +52,9 @@ class BroadcastSentViaContextDetector : Detector(), SourceCodeScanner { val evaluator = context.evaluator if (evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { context.report( - ISSUE, - method, - context.getNameLocation(node), - "`Context.${method.name}()` should be replaced with " + + issue = ISSUE, + location = context.getNameLocation(node), + message = "`Context.${method.name}()` should be replaced with " + "`BroadcastSender.${method.name}()`" ) } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt index bab76ab4bce2..14099ebef56c 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt @@ -38,10 +38,9 @@ class NonInjectedMainThreadDetector : Detector(), SourceCodeScanner { override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { context.report( - ISSUE, - method, - context.getNameLocation(node), - "Replace with injected `@Main Executor`." + issue = ISSUE, + location = context.getNameLocation(node), + message = "Replace with injected `@Main Executor`." ) } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt index b62290025437..aa4b2f766bf0 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt @@ -44,11 +44,11 @@ class NonInjectedServiceDetector : Detector(), SourceCodeScanner { method.containingClass?.qualifiedName == CLASS_CONTEXT ) { context.report( - ISSUE, - method, - context.getNameLocation(node), - "Use `@Inject` to get system-level service handles instead of " + - "`Context.getSystemService()`" + issue = ISSUE, + location = context.getNameLocation(node), + message = + "Use `@Inject` to get system-level service handles instead of " + + "`Context.getSystemService()`" ) } else if ( evaluator.isStatic(method) && @@ -56,10 +56,10 @@ class NonInjectedServiceDetector : Detector(), SourceCodeScanner { method.containingClass?.qualifiedName == "android.accounts.AccountManager" ) { context.report( - ISSUE, - method, - context.getNameLocation(node), - "Replace `AccountManager.get()` with an injected instance of `AccountManager`" + issue = ISSUE, + location = context.getNameLocation(node), + message = + "Replace `AccountManager.get()` with an injected instance of `AccountManager`" ) } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt index 4ba3afc7f7e2..5840e8f8dfb6 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt @@ -38,10 +38,10 @@ class RegisterReceiverViaContextDetector : Detector(), SourceCodeScanner { override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { context.report( - ISSUE, - method, - context.getNameLocation(node), - "Register `BroadcastReceiver` using `BroadcastDispatcher` instead of `Context`" + issue = ISSUE, + location = context.getNameLocation(node), + message = "Register `BroadcastReceiver` using `BroadcastDispatcher` instead " + + "of `Context`" ) } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt index 7be21a512f89..b15a41b226df 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt @@ -46,10 +46,10 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { method.containingClass?.qualifiedName == "android.app.ActivityManager" ) { context.report( - ISSUE_SLOW_USER_ID_QUERY, - method, - context.getNameLocation(node), - "Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`" + issue = ISSUE_SLOW_USER_ID_QUERY, + location = context.getNameLocation(node), + message = + "Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`" ) } if ( @@ -58,10 +58,9 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { method.containingClass?.qualifiedName == "android.os.UserManager" ) { context.report( - ISSUE_SLOW_USER_INFO_QUERY, - method, - context.getNameLocation(node), - "Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`" + issue = ISSUE_SLOW_USER_INFO_QUERY, + location = context.getNameLocation(node), + message = "Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`" ) } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt index 4b9aa13c0240..bf025894d66f 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt @@ -44,10 +44,9 @@ class SoftwareBitmapDetector : Detector(), SourceCodeScanner { val evaluator = context.evaluator if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) { context.report( - ISSUE, - referenced, - context.getNameLocation(reference), - "Replace software bitmap with `Config.HARDWARE`" + issue = ISSUE, + location = context.getNameLocation(reference), + message = "Replace software bitmap with `Config.HARDWARE`" ) } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt index 1db072548a76..22f15bdcb5bd 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt @@ -66,10 +66,9 @@ class StaticSettingsProviderDetector : Detector(), SourceCodeScanner { val subclassName = className.substring(CLASS_SETTINGS.length + 1) context.report( - ISSUE, - method, - context.getNameLocation(node), - "`@Inject` a ${subclassName}Settings instead" + issue = ISSUE, + location = context.getNameLocation(node), + message = "`@Inject` a ${subclassName}Settings instead" ) } diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt index c35ac61a6543..426211e0f327 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt @@ -126,6 +126,32 @@ class BindServiceOnMainThreadDetectorTest : SystemUILintDetectorTest() { } @Test + fun testSuppressUnbindService() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.content.ServiceConnection; + + @SuppressLint("BindServiceOnMainThread") + public class TestClass { + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expectClean() + } + + @Test fun testWorkerMethod() { lint() .files( diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt index 376acb56fac9..30b68f7e7a75 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt @@ -129,6 +129,34 @@ class BroadcastSentViaContextDetectorTest : SystemUILintDetectorTest() { } @Test + fun testSuppressSendBroadcastInActivity() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.app.Activity; + import android.os.UserHandle; + + public class TestClass { + @SuppressWarnings("BroadcastSentViaContext") + public void send(Activity activity) { + Intent intent = new Intent(Intent.ACTION_VIEW); + activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); + } + + } + """ + ) + .indented(), + *stubs + ) + .issues(BroadcastSentViaContextDetector.ISSUE) + .run() + .expectClean() + } + + @Test fun testSendBroadcastInBroadcastSender() { lint() .files( diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt index 301c338f9b42..ed3d14a1f33f 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt @@ -61,6 +61,32 @@ class NonInjectedMainThreadDetectorTest : SystemUILintDetectorTest() { } @Test + fun testSuppressGetMainThreadHandler() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.os.Handler; + + @SuppressWarnings("NonInjectedMainThread") + public class TestClass { + public void test(Context context) { + Handler mainThreadHandler = context.getMainThreadHandler(); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(NonInjectedMainThreadDetector.ISSUE) + .run() + .expectClean() + } + + @Test fun testGetMainLooper() { lint() .files( diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt index 0a74bfcfee57..846129aa12c1 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt @@ -91,6 +91,32 @@ class NonInjectedServiceDetectorTest : SystemUILintDetectorTest() { } @Test + fun testSuppressGetServiceWithClass() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.os.UserManager; + + public class TestClass { + @SuppressLint("NonInjectedService") + public void getSystemServiceWithoutDagger(Context context) { + context.getSystemService(UserManager.class); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(NonInjectedServiceDetector.ISSUE) + .run() + .expectClean() + } + + @Test fun testGetAccountManager() { lint() .files( diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt index 9ed7aa029b1d..0ac8f8e7c672 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt @@ -63,6 +63,34 @@ class RegisterReceiverViaContextDetectorTest : SystemUILintDetectorTest() { } @Test + fun testSuppressRegisterReceiver() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.IntentFilter; + + @SuppressWarnings("RegisterReceiverViaContext") + public class TestClass { + public void bind(Context context, BroadcastReceiver receiver, + IntentFilter filter) { + context.registerReceiver(receiver, filter, 0); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterReceiverViaContextDetector.ISSUE) + .run() + .expectClean() + } + + @Test fun testRegisterReceiverAsUser() { lint() .files( diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt index 54cac7b35598..34a424918a79 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt @@ -76,7 +76,7 @@ class SlowUserQueryDetectorTest : SystemUILintDetectorTest() { import android.os.UserManager; public class TestClass { - public void slewlyGetUserInfo(UserManager userManager) { + public void slowlyGetUserInfo(UserManager userManager) { userManager.getUserInfo(); } } @@ -101,6 +101,34 @@ class SlowUserQueryDetectorTest : SystemUILintDetectorTest() { } @Test + fun testSuppressGetUserInfo() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.os.UserManager; + + public class TestClass { + @SuppressWarnings("SlowUserInfoQuery") + public void slowlyGetUserInfo(UserManager userManager) { + userManager.getUserInfo(); + } + } + """ + ) + .indented(), + *stubs + ) + .issues( + SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY, + SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY + ) + .run() + .expectClean() + } + + @Test fun testUserTrackerGetUserId() { lint() .files( diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt index c632636eb9c8..34becc6a5b04 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt @@ -63,6 +63,31 @@ class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { } @Test + fun testSuppressSoftwareBitmap() { + lint() + .files( + TestFiles.java( + """ + import android.graphics.Bitmap; + + @SuppressWarnings("SoftwareBitmap") + public class TestClass { + public void test() { + Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565); + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(SoftwareBitmapDetector.ISSUE) + .run() + .expectClean() + } + + @Test fun testHardwareBitmap() { lint() .files( diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt index b83ed7067bc3..efe4c90ec44f 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt @@ -28,7 +28,7 @@ class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() { override fun getIssues(): List<Issue> = listOf(StaticSettingsProviderDetector.ISSUE) @Test - fun testGetServiceWithString() { + fun testSuppressGetServiceWithString() { lint() .files( TestFiles.java( @@ -204,5 +204,34 @@ class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() { ) } + @Test + fun testGetServiceWithString() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + + import android.provider.Settings; + import android.provider.Settings.Global; + import android.provider.Settings.Secure; + + public class TestClass { + @SuppressWarnings("StaticSettingsProvider") + public void getSystemServiceWithoutDagger(Context context) { + final ContentResolver cr = mContext.getContentResolver(); + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(StaticSettingsProviderDetector.ISSUE) + .run() + .expectClean() + } + private val stubs = androidStubs } diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags index a3b4b385f5bd..69767867ebd7 100644 --- a/packages/SystemUI/proguard.flags +++ b/packages/SystemUI/proguard.flags @@ -25,9 +25,15 @@ -keep class ** extends androidx.preference.PreferenceFragment -keep class com.android.systemui.tuner.* + +# The plugins and animation subpackages both act as shared libraries that might be referenced in +# dynamically-loaded plugin APKs. -keep class com.android.systemui.plugins.** { *; } +-keep class !com.android.systemui.animation.R$**,com.android.systemui.animation.** { + *; +} -keep class com.android.systemui.fragments.FragmentService$FragmentCreator { *; } diff --git a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml index c8ba237088f2..a948c04020f3 100644 --- a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml +++ b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml @@ -23,7 +23,7 @@ <string name="keyguard_enter_your_pin" msgid="5429932527814874032">"Enter your PIN"</string> <string name="keyguard_enter_your_pattern" msgid="351503370332324745">"Enter your pattern"</string> <string name="keyguard_enter_your_password" msgid="7225626204122735501">"Enter your password"</string> - <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Invalid card."</string> + <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Invalid Card."</string> <string name="keyguard_charged" msgid="5478247181205188995">"Charged"</string> <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging wirelessly"</string> <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string> @@ -70,7 +70,7 @@ <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Incorrect SIM PIN code you must now contact your carrier to unlock your device."</string> <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Incorrect SIM PIN code, you have # remaining attempt before you must contact your carrier to unlock your device.}other{Incorrect SIM PIN code, you have # remaining attempts. }}"</string> <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is unusable. Contact your carrier."</string> - <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Incorrect SIM PUK code; you have # remaining attempt before SIM becomes permanently unusable.}other{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}}"</string> + <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Incorrect SIM PUK code, you have # remaining attempt before SIM becomes permanently unusable.}other{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}}"</string> <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN operation failed!"</string> <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK operation failed!"</string> <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Switch input method"</string> @@ -83,12 +83,12 @@ <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string> <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string> <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string> - <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string> + <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognized"</string> <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"To use Face Unlock, turn on camera access in Settings"</string> <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Enter SIM PIN. You have # remaining attempt before you must contact your carrier to unlock your device.}other{Enter SIM PIN. You have # remaining attempts.}}"</string> <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM is now disabled. Enter PUK code to continue. You have # remaining attempt before SIM becomes permanently unusable. Contact carrier for details.}other{SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact carrier for details.}}"</string> <string name="clock_title_default" msgid="6342735240617459864">"Default"</string> <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string> - <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string> + <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string> <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string> </resources> diff --git a/packages/SystemUI/res/drawable/ic_watch.xml b/packages/SystemUI/res/drawable/ic_watch.xml new file mode 100644 index 000000000000..8ff880cef029 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_watch.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<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="#FFFFFFFF" + android:pathData="M16,0L8,0l-0.95,5.73C5.19,7.19 4,9.45 4,12s1.19,4.81 3.05,6.27L8,24 + h8l0.96,-5.73C18.81,16.81 20,14.54 20,12s-1.19,-4.81 -3.04,-6.27L16,0z + M12,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/SystemUI/res/layout/keyguard_status_bar.xml b/packages/SystemUI/res/layout/keyguard_status_bar.xml index d27fa192e741..8b8594032816 100644 --- a/packages/SystemUI/res/layout/keyguard_status_bar.xml +++ b/packages/SystemUI/res/layout/keyguard_status_bar.xml @@ -34,30 +34,13 @@ android:paddingTop="@dimen/status_bar_padding_top" android:layout_alignParentEnd="true" android:gravity="center_vertical|end" > - <com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer + + <include android:id="@+id/user_switcher_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" - android:orientation="horizontal" - android:paddingTop="4dp" - android:paddingBottom="4dp" - android:paddingStart="8dp" - android:paddingEnd="8dp" - android:background="@drawable/status_bar_user_chip_bg" - android:visibility="visible" > - <ImageView android:id="@+id/current_user_avatar" - android:layout_width="@dimen/multi_user_avatar_keyguard_size" - android:layout_height="@dimen/multi_user_avatar_keyguard_size" - android:scaleType="centerInside" - android:paddingEnd="4dp" /> - - <TextView android:id="@+id/current_user_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.StatusBar.Clock" - /> - </com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer> + android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin" + layout="@layout/status_bar_user_chip_container" /> <FrameLayout android:id="@+id/system_icons_container" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml index 80e65a3b3295..f7600e606731 100644 --- a/packages/SystemUI/res/layout/status_bar.xml +++ b/packages/SystemUI/res/layout/status_bar.xml @@ -136,31 +136,12 @@ android:gravity="center_vertical|end" android:clipChildren="false"> - <com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer + <include android:id="@+id/user_switcher_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" - android:orientation="horizontal" - android:paddingTop="4dp" - android:paddingBottom="4dp" - android:paddingStart="8dp" - android:paddingEnd="8dp" - android:layout_marginEnd="16dp" - android:background="@drawable/status_bar_user_chip_bg" - android:visibility="visible" > - <ImageView android:id="@+id/current_user_avatar" - android:layout_width="@dimen/multi_user_avatar_keyguard_size" - android:layout_height="@dimen/multi_user_avatar_keyguard_size" - android:scaleType="centerInside" - android:paddingEnd="4dp" /> - - <TextView android:id="@+id/current_user_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.StatusBar.Clock" - /> - </com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer> + android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin" + layout="@layout/status_bar_user_chip_container" /> <include layout="@layout/system_icons" /> </com.android.keyguard.AlphaOptimizedLinearLayout> diff --git a/packages/SystemUI/res/layout/status_bar_user_chip_container.xml b/packages/SystemUI/res/layout/status_bar_user_chip_container.xml new file mode 100644 index 000000000000..b374074958cb --- /dev/null +++ b/packages/SystemUI/res/layout/status_bar_user_chip_container.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/user_switcher_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="horizontal" + android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin" + android:background="@drawable/status_bar_user_chip_bg" + android:visibility="visible" > + <ImageView android:id="@+id/current_user_avatar" + android:layout_width="@dimen/status_bar_user_chip_avatar_size" + android:layout_height="@dimen/status_bar_user_chip_avatar_size" + android:layout_margin="4dp" + android:scaleType="centerInside" /> + + <TextView android:id="@+id/current_user_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingEnd="8dp" + android:textAppearance="@style/TextAppearance.StatusBar.UserChip" + /> +</com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer> diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml index 8388b67c1fa1..bafdb11ab211 100644 --- a/packages/SystemUI/res/layout/super_notification_shade.xml +++ b/packages/SystemUI/res/layout/super_notification_shade.xml @@ -26,12 +26,12 @@ android:fitsSystemWindows="true"> <com.android.systemui.statusbar.BackDropView - android:id="@+id/backdrop" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="gone" - sysui:ignoreRightInset="true" - > + android:id="@+id/backdrop" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + sysui:ignoreRightInset="true" + > <ImageView android:id="@+id/backdrop_back" android:layout_width="match_parent" android:scaleType="centerCrop" @@ -49,7 +49,7 @@ android:layout_height="match_parent" android:importantForAccessibility="no" sysui:ignoreRightInset="true" - /> + /> <com.android.systemui.scrim.ScrimView android:id="@+id/scrim_notifications" @@ -57,17 +57,17 @@ android:layout_height="match_parent" android:importantForAccessibility="no" sysui:ignoreRightInset="true" - /> + /> <com.android.systemui.statusbar.LightRevealScrim - android:id="@+id/light_reveal_scrim" - android:layout_width="match_parent" - android:layout_height="match_parent" /> + android:id="@+id/light_reveal_scrim" + android:layout_width="match_parent" + android:layout_height="match_parent" /> <include layout="@layout/status_bar_expanded" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="invisible" /> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="invisible" /> <include layout="@layout/brightness_mirror_container" /> diff --git a/packages/SystemUI/res/values/bools.xml b/packages/SystemUI/res/values/bools.xml index 8221d78fbfd7..04fc4b8c45a8 100644 --- a/packages/SystemUI/res/values/bools.xml +++ b/packages/SystemUI/res/values/bools.xml @@ -25,6 +25,9 @@ <!-- Whether to enable clipping on Quick Settings --> <bool name="qs_enable_clipping">true</bool> + <!-- Whether to enable clipping on Notification Views --> + <bool name="notification_enable_clipping">true</bool> + <!-- Whether to enable transparent background for notification scrims --> <bool name="notification_scrim_transparent">false</bool> </resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index f4d802bf745e..7a362040427a 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -806,4 +806,24 @@ <!-- Whether the device should display hotspot UI. If true, UI will display only when tethering is available. If false, UI will never show regardless of tethering availability" --> <bool name="config_show_wifi_tethering">true</bool> + + <!-- A collection of "slots" for placing quick affordance actions on the lock screen when the + device is locked. Each item is a string consisting of two parts, separated by the ':' character. + The first part is the unique ID for the slot, it is not a human-visible name, but should still + be unique across all slots specified. The second part is the capacity and must be a positive + integer; this is how many quick affordance actions that user is allowed to add to the slot. --> + <string-array name="config_keyguardQuickAffordanceSlots" translatable="false"> + <item>bottom_start:1</item> + <item>bottom_end:1</item> + </string-array> + + <!-- A collection of defaults for the quick affordances on the lock screen. Each item must be a + string with two parts: the ID of the slot and the comma-delimited list of affordance IDs, + separated by a colon ':' character. For example: <item>bottom_end:home,wallet</item>. The + default is displayed by System UI as long as the user hasn't made a different choice for that + slot. If the user did make a choice, even if the choice is the "None" option, the default is + ignored. --> + <string-array name="config_keyguardQuickAffordanceDefaults" translatable="false"> + </string-array> + </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index fbdccff38731..437d89beaa9e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1422,6 +1422,11 @@ <dimen name="ongoing_call_chip_icon_text_padding">4dp</dimen> <dimen name="ongoing_call_chip_corner_radius">28dp</dimen> + <!-- Status bar user chip --> + <dimen name="status_bar_user_chip_avatar_size">16dp</dimen> + <dimen name="status_bar_user_chip_end_margin">12dp</dimen> + <dimen name="status_bar_user_chip_text_size">12sp</dimen> + <!-- Internet panel related dimensions --> <dimen name="internet_dialog_list_max_height">662dp</dimen> <!-- The height of the WiFi network in Internet panel. --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index eb291fd5f12a..9eafdb959f07 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -439,16 +439,16 @@ <string name="accessibility_battery_level">Battery <xliff:g id="number">%d</xliff:g> percent.</string> <!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery (not shown on the screen). [CHAR LIMIT=NONE] --> - <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$d</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage</string> + <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$d</xliff:g> percent, <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g></string> <!-- Content description of the battery level icon for accessibility while the device is charging (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="accessibility_battery_level_charging">Battery charging, <xliff:g id="battery_percentage">%d</xliff:g> percent.</string> <!-- Content description of the battery level icon for accessibility, with information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] --> - <string name="accessibility_battery_level_charging_paused">Battery <xliff:g id="percentage" example="90%">%d</xliff:g> percent. Charging paused for battery protection.</string> + <string name="accessibility_battery_level_charging_paused">Battery <xliff:g id="percentage" example="90%">%d</xliff:g> percent, charging paused for battery protection.</string> <!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery *and* information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] --> - <string name="accessibility_battery_level_charging_paused_with_estimate">Battery <xliff:g id="percentage" example="90%">%1$d</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage. Charging paused for battery protection.</string> + <string name="accessibility_battery_level_charging_paused_with_estimate">Battery <xliff:g id="percentage" example="90%">%1$d</xliff:g> percent, <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g>, charging paused for battery protection.</string> <!-- Content description of overflow icon container of the notifications for accessibility (not shown on the screen)[CHAR LIMIT=NONE] --> <string name="accessibility_overflow_action">See all notifications</string> @@ -1996,6 +1996,9 @@ <!-- SysUI Tuner: Summary of no shortcut being selected [CHAR LIMIT=60] --> <string name="lockscreen_none">None</string> + <!-- ClockId to use when none is set by user --> + <string name="lockscreen_clock_id_fallback" translatable="false">DEFAULT</string> + <!-- SysUI Tuner: Format string for describing launching an app [CHAR LIMIT=60] --> <string name="tuner_launch_app">Launch <xliff:g id="app" example="Settings">%1$s</xliff:g></string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index ae80070dfa97..fe4f639c307e 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -23,6 +23,12 @@ <item name="android:textColor">@color/status_bar_clock_color</item> </style> + <style name="TextAppearance.StatusBar.UserChip" parent="@*android:style/TextAppearance.StatusBar.Icon"> + <item name="android:textSize">@dimen/status_bar_user_chip_text_size</item> + <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item> + <item name="android:textColor">@color/status_bar_clock_color</item> + </style> + <style name="TextAppearance.StatusBar.Expanded" parent="@*android:style/TextAppearance.StatusBar"> <item name="android:textColor">?android:attr/textColorTertiary</item> </style> diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt index 0b0595f4405f..36ac1ff9ad30 100644 --- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt @@ -34,6 +34,7 @@ import org.junit.runners.model.Statement import platform.test.screenshot.DeviceEmulationRule import platform.test.screenshot.DeviceEmulationSpec import platform.test.screenshot.MaterialYouColorsRule +import platform.test.screenshot.PathConfig import platform.test.screenshot.ScreenshotTestRule import platform.test.screenshot.getEmulatedDevicePathConfig import platform.test.screenshot.matchers.BitmapMatcher @@ -41,13 +42,19 @@ import platform.test.screenshot.matchers.BitmapMatcher /** A rule for View screenshot diff unit tests. */ class ViewScreenshotTestRule( emulationSpec: DeviceEmulationSpec, - private val matcher: BitmapMatcher = UnitTestBitmapMatcher + private val matcher: BitmapMatcher = UnitTestBitmapMatcher, + pathConfig: PathConfig = getEmulatedDevicePathConfig(emulationSpec), + assetsPathRelativeToRepo: String = "" ) : TestRule { private val colorsRule = MaterialYouColorsRule() private val deviceEmulationRule = DeviceEmulationRule(emulationSpec) private val screenshotRule = ScreenshotTestRule( - SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec)) + if (assetsPathRelativeToRepo.isBlank()) { + SystemUIGoldenImagePathManager(pathConfig) + } else { + SystemUIGoldenImagePathManager(pathConfig, assetsPathRelativeToRepo) + } ) private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java) private val delegateRule = diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 601cb66d99c2..5c2c27a36e59 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -41,6 +41,7 @@ open class ClockRegistry( val isEnabled: Boolean, userHandle: Int, defaultClockProvider: ClockProvider, + val fallbackClockId: ClockId = DEFAULT_CLOCK_ID, ) { // Usually this would be a typealias, but a SAM provides better java interop fun interface ClockChangeListener { @@ -69,10 +70,13 @@ open class ClockRegistry( context.contentResolver, Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE ) - ClockSetting.deserialize(json)?.clockId ?: DEFAULT_CLOCK_ID + if (json == null || json.isEmpty()) { + return fallbackClockId + } + ClockSetting.deserialize(json).clockId } catch (ex: Exception) { Log.e(TAG, "Failed to parse clock setting", ex) - DEFAULT_CLOCK_ID + fallbackClockId } } set(value) { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt index 599cd23f6616..23a7271afbdb 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -142,7 +142,9 @@ class DefaultClockController( currentColor = color view.setColors(DOZE_COLOR, color) - view.animateAppearOnLockscreen() + if (!animations.dozeState.isActive) { + view.animateAppearOnLockscreen() + } } } @@ -197,7 +199,7 @@ class DefaultClockController( dozeFraction: Float, foldFraction: Float, ) : ClockAnimations { - private val dozeState = AnimationState(dozeFraction) + internal val dozeState = AnimationState(dozeFraction) private val foldState = AnimationState(foldFraction) init { @@ -238,7 +240,7 @@ class DefaultClockController( get() = true } - private class AnimationState( + class AnimationState( var fraction: Float, ) { var isActive: Boolean = fraction > 0.5f diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt index c2658a9e61b1..f60db2ad2687 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt @@ -108,4 +108,30 @@ object KeyguardQuickAffordanceProviderContract { const val AFFORDANCE_ID = "affordance_id" } } + + /** + * Table for flags. + * + * Flags are key-value pairs. + * + * Supported operations: + * - Query - to know the values of flags, query the [FlagsTable.URI] [Uri]. The result set will + * contain rows, each of which with the columns from [FlagsTable.Columns]. + */ + object FlagsTable { + const val TABLE_NAME = "flags" + val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build() + + /** + * Flag denoting whether the customizable lock screen quick affordances feature is enabled. + */ + const val FLAG_NAME_FEATURE_ENABLED = "is_feature_enabled" + + object Columns { + /** String. Unique ID for the flag. */ + const val NAME = "name" + /** Int. Value of the flag. `1` means `true` and `0` means `false`. */ + const val VALUE = "value" + } + } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index a790d89ac1ae..f45887cf7630 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -71,14 +71,20 @@ public class PreviewPositionHelper { int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); RectF thumbnailClipHint = new RectF(); - float scaledTaskbarSize = 0; + float scaledTaskbarSize; + float canvasScreenRatio; if (mSplitBounds != null) { float fullscreenTaskWidth; float fullscreenTaskHeight; - float canvasScreenRatio; float taskPercent; - if (!mSplitBounds.appsStackedVertically) { + if (mSplitBounds.appsStackedVertically) { + taskPercent = mDesiredStagePosition != STAGE_POSITION_TOP_OR_LEFT + ? mSplitBounds.topTaskPercent + : (1 - (mSplitBounds.topTaskPercent + mSplitBounds.dividerHeightPercent)); + fullscreenTaskHeight = screenHeightPx * taskPercent; + canvasScreenRatio = canvasHeight / fullscreenTaskHeight; + } else { // For landscape, scale the width taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT ? mSplitBounds.leftTaskPercent @@ -86,17 +92,12 @@ public class PreviewPositionHelper { // Scale landscape width to that of actual screen fullscreenTaskWidth = screenWidthPx * taskPercent; canvasScreenRatio = canvasWidth / fullscreenTaskWidth; - } else { - taskPercent = mDesiredStagePosition != STAGE_POSITION_TOP_OR_LEFT - ? mSplitBounds.leftTaskPercent - : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent)); - // Scale landscape width to that of actual screen - fullscreenTaskHeight = screenHeightPx * taskPercent; - canvasScreenRatio = canvasHeight / fullscreenTaskHeight; } - scaledTaskbarSize = taskbarSize * canvasScreenRatio; - thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0; + } else { + canvasScreenRatio = (float) canvasWidth / screenWidthPx; } + scaledTaskbarSize = taskbarSize * canvasScreenRatio; + thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0; float scale = thumbnailData.scale; final float thumbnailScale; diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt index 74519c21820b..05372fec7211 100644 --- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt +++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt @@ -22,7 +22,11 @@ object FlagsFactory { private val flagMap = mutableMapOf<String, Flag<*>>() val knownFlags: Map<String, Flag<*>> - get() = flagMap + get() { + // We need to access Flags in order to initialize our map. + assert(flagMap.contains(Flags.TEAMFOOD.name)) { "Where is teamfood?" } + return flagMap + } fun unreleasedFlag( id: Int, diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt index 7b216017df7d..8323d0971ad7 100644 --- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt @@ -34,6 +34,9 @@ abstract class FlagsModule { @Binds abstract fun bindsFeatureFlagDebug(impl: FeatureFlagsDebug): FeatureFlags + @Binds + abstract fun bindsRestarter(debugRestarter: FeatureFlagsDebugRestarter): Restarter + @Module companion object { @JvmStatic diff --git a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsFactory.kt b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsFactory.kt index 89c0786af6e3..27c5699df70f 100644 --- a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsFactory.kt +++ b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsFactory.kt @@ -22,7 +22,11 @@ object FlagsFactory { private val flagMap = mutableMapOf<String, Flag<*>>() val knownFlags: Map<String, Flag<*>> - get() = flagMap + get() { + // We need to access Flags in order to initialize our map. + assert(flagMap.contains(Flags.TEAMFOOD.name)) { "Where is teamfood?" } + return flagMap + } fun unreleasedFlag( id: Int, diff --git a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt index aef887667527..87beff76290d 100644 --- a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt @@ -27,4 +27,7 @@ import dagger.Module abstract class FlagsModule { @Binds abstract fun bindsFeatureFlagRelease(impl: FeatureFlagsRelease): FeatureFlags + + @Binds + abstract fun bindsRestarter(debugRestarter: FeatureFlagsReleaseRestarter): Restarter } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java index 8fa7b11e2664..2b660dee4f16 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java @@ -21,7 +21,6 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.media.AudioManager; import android.os.SystemClock; -import android.service.trust.TrustAgentService; import android.telephony.TelephonyManager; import android.util.Log; import android.util.MathUtils; @@ -68,30 +67,24 @@ public class KeyguardHostViewController extends ViewController<KeyguardHostView> private final KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { @Override - public void onTrustGrantedWithFlags(int flags, int userId, String message) { - if (userId != KeyguardUpdateMonitor.getCurrentUser()) return; - boolean bouncerVisible = mView.isVisibleToUser(); - boolean temporaryAndRenewable = - (flags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) - != 0; - boolean initiatedByUser = - (flags & TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER) != 0; - boolean dismissKeyguard = - (flags & TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD) != 0; - - if (initiatedByUser || dismissKeyguard) { - if ((mViewMediatorCallback.isScreenOn() || temporaryAndRenewable) - && (bouncerVisible || dismissKeyguard)) { - if (!bouncerVisible) { - // The trust agent dismissed the keyguard without the user proving - // that they are present (by swiping up to show the bouncer). That's - // fine if the user proved presence via some other way to the trust - //agent. - Log.i(TAG, "TrustAgent dismissed Keyguard."); - } - mSecurityCallback.dismiss(false /* authenticated */, userId, - /* bypassSecondaryLockScreen */ false, SecurityMode.Invalid); - } else { + public void onTrustGrantedForCurrentUser(boolean dismissKeyguard, + TrustGrantFlags flags, String message) { + if (dismissKeyguard) { + if (!mView.isVisibleToUser()) { + // The trust agent dismissed the keyguard without the user proving + // that they are present (by swiping up to show the bouncer). That's + // fine if the user proved presence via some other way to the trust + // agent. + Log.i(TAG, "TrustAgent dismissed Keyguard."); + } + mSecurityCallback.dismiss( + false /* authenticated */, + KeyguardUpdateMonitor.getCurrentUser(), + /* bypassSecondaryLockScreen */ false, + SecurityMode.Invalid + ); + } else { + if (flags.isInitiatedByUser() || flags.dismissKeyguardRequested()) { mViewMediatorCallback.playTrustedSound(); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt index a0206f1f1e70..819768544b0c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt @@ -50,10 +50,9 @@ data class KeyguardFaceListenModel( override val listening: Boolean, // keep sorted val authInterruptActive: Boolean, - val becauseCannotSkipBouncer: Boolean, val biometricSettingEnabledForUser: Boolean, val bouncerFullyShown: Boolean, - val faceAuthenticated: Boolean, + val faceAndFpNotAuthenticated: Boolean, val faceDisabled: Boolean, val faceLockedOut: Boolean, val fpLockedOut: Boolean, @@ -67,7 +66,9 @@ data class KeyguardFaceListenModel( val secureCameraLaunched: Boolean, val switchingUser: Boolean, val udfpsBouncerShowing: Boolean, -) : KeyguardListenModel() + val udfpsFingerDown: Boolean, + val userNotTrustedOrDetectionIsNeeded: Boolean, + ) : KeyguardListenModel() /** * Verbose debug information associated with [KeyguardUpdateMonitor.shouldTriggerActiveUnlock]. */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index d694dc0d7bf6..ce22a81befb5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -472,10 +472,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab FACE_AUTH_TRIGGERED_TRUST_DISABLED); } - String message = null; - if (KeyguardUpdateMonitor.getCurrentUser() == userId) { - final boolean userHasTrust = getUserHasTrust(userId); - if (userHasTrust && trustGrantedMessages != null) { + if (enabled) { + String message = null; + if (KeyguardUpdateMonitor.getCurrentUser() == userId + && trustGrantedMessages != null) { + // Show the first non-empty string provided by a trust agent OR intentionally pass + // an empty string through (to prevent the default trust agent string from showing) for (String msg : trustGrantedMessages) { message = msg; if (!TextUtils.isEmpty(message)) { @@ -483,21 +485,39 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } } + + mLogger.logTrustGrantedWithFlags(flags, userId, message); + if (userId == getCurrentUser()) { + final TrustGrantFlags trustGrantFlags = new TrustGrantFlags(flags); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onTrustGrantedForCurrentUser( + shouldDismissKeyguardOnTrustGrantedWithCurrentUser(trustGrantFlags), + trustGrantFlags, message); + } + } + } } + mLogger.logTrustChanged(wasTrusted, enabled, userId); - if (message != null) { - mLogger.logShowTrustGrantedMessage(message.toString()); - } for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { cb.onTrustChanged(userId); - if (enabled) { - cb.onTrustGrantedWithFlags(flags, userId, message); - } } } + } + /** + * Whether the trust granted call with its passed flags should dismiss keyguard. + * It's assumed that the trust was granted for the current user. + */ + private boolean shouldDismissKeyguardOnTrustGrantedWithCurrentUser(TrustGrantFlags flags) { + final boolean isBouncerShowing = mPrimaryBouncerIsOrWillBeShowing || mUdfpsBouncerShowing; + return (flags.isInitiatedByUser() || flags.dismissKeyguardRequested()) + && (mDeviceInteractive || flags.temporaryAndRenewable()) + && (isBouncerShowing || flags.dismissKeyguardRequested()); } @Override @@ -774,9 +794,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } // Don't send cancel if authentication succeeds mFingerprintCancelSignal = null; + mLogger.logFingerprintSuccess(userId, isStrongBiometric); updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_FP_AUTHENTICATED); - mLogger.logFingerprintSuccess(userId, isStrongBiometric); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -2691,9 +2711,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean canBypass = mKeyguardBypassController != null && mKeyguardBypassController.canBypass(); // There's no reason to ask the HAL for authentication when the user can dismiss the - // bouncer, unless we're bypassing and need to auto-dismiss the lock screen even when - // TrustAgents or biometrics are keeping the device unlocked. - final boolean becauseCannotSkipBouncer = !getUserCanSkipBouncer(user) || canBypass; + // bouncer because the user is trusted, unless we're bypassing and need to auto-dismiss + // the lock screen even when TrustAgents are keeping the device unlocked. + final boolean userNotTrustedOrDetectionIsNeeded = !getUserHasTrust(user) || canBypass; // Scan even when encrypted or timeout to show a preemptive bouncer when bypassing. // Lock-down mode shouldn't scan, since it is more explicit. @@ -2710,11 +2730,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab strongAuthAllowsScanning = false; } - // If the face has recently been authenticated do not attempt to authenticate again. - final boolean faceAuthenticated = getIsFaceAuthenticated(); + // If the face or fp has recently been authenticated do not attempt to authenticate again. + final boolean faceAndFpNotAuthenticated = !getUserUnlockedWithBiometric(user); final boolean faceDisabledForUser = isFaceDisabled(user); final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user); final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant(); + final boolean isUdfpsFingerDown = mAuthController.isUdfpsFingerDown(); // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware. @@ -2724,13 +2745,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || mOccludingAppRequestingFace || awakeKeyguard || shouldListenForFaceAssistant - || mAuthController.isUdfpsFingerDown() + || isUdfpsFingerDown || mUdfpsBouncerShowing) - && !mSwitchingUser && !faceDisabledForUser && becauseCannotSkipBouncer + && !mSwitchingUser && !faceDisabledForUser && userNotTrustedOrDetectionIsNeeded && !mKeyguardGoingAway && biometricEnabledForUser && strongAuthAllowsScanning && mIsPrimaryUser && (!mSecureCameraLaunched || mOccludingAppRequestingFace) - && !faceAuthenticated + && faceAndFpNotAuthenticated && !mGoingToSleep // We only care about fp locked out state and not face because we still trigger // face auth even when face is locked out to show the user a message that face @@ -2744,10 +2765,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab user, shouldListen, mAuthInterruptActive, - becauseCannotSkipBouncer, biometricEnabledForUser, mPrimaryBouncerFullyShown, - faceAuthenticated, + faceAndFpNotAuthenticated, faceDisabledForUser, isFaceLockedOut(), fpLockedOut, @@ -2760,7 +2780,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab strongAuthAllowsScanning, mSecureCameraLaunched, mSwitchingUser, - mUdfpsBouncerShowing)); + mUdfpsBouncerShowing, + isUdfpsFingerDown, + userNotTrustedOrDetectionIsNeeded)); return shouldListen; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index c5142f309a46..1d58fc9cf94b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -19,6 +19,7 @@ import android.hardware.biometrics.BiometricSourceType; import android.telephony.TelephonyManager; import android.view.WindowManagerPolicyConstants; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.settingslib.fuelgauge.BatteryStatus; @@ -175,11 +176,13 @@ public class KeyguardUpdateMonitorCallback { /** * Called after trust was granted. - * @param userId of the user that has been granted trust + * @param dismissKeyguard whether the keyguard should be dismissed as a result of the + * trustGranted * @param message optional message the trust agent has provided to show that should indicate * why trust was granted. */ - public void onTrustGrantedWithFlags(int flags, int userId, @Nullable String message) { } + public void onTrustGrantedForCurrentUser(boolean dismissKeyguard, + @NonNull TrustGrantFlags flags, @Nullable String message) { } /** * Called when a biometric has been acquired. diff --git a/packages/SystemUI/src/com/android/keyguard/TrustGrantFlags.java b/packages/SystemUI/src/com/android/keyguard/TrustGrantFlags.java new file mode 100644 index 000000000000..d33732cd5536 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/TrustGrantFlags.java @@ -0,0 +1,98 @@ +/* + * 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.keyguard; + +import android.service.trust.TrustAgentService; + +import java.util.Objects; + +/** + * Translating {@link android.service.trust.TrustAgentService.GrantTrustFlags} to a more + * parsable object. These flags are requested by a TrustAgent. + */ +public class TrustGrantFlags { + final int mFlags; + + public TrustGrantFlags(int flags) { + this.mFlags = flags; + } + + /** {@link TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER} */ + public boolean isInitiatedByUser() { + return (mFlags & TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER) != 0; + } + + /** + * Trust agent is requesting to dismiss the keyguard. + * See {@link TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD}. + * + * This does not guarantee that the keyguard is dismissed. + * KeyguardUpdateMonitor makes the final determination whether the keyguard should be dismissed. + * {@link KeyguardUpdateMonitorCallback#onTrustGrantedForCurrentUser( + * boolean, TrustGrantFlags, String). + */ + public boolean dismissKeyguardRequested() { + return (mFlags & TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD) != 0; + } + + /** {@link TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE} */ + public boolean temporaryAndRenewable() { + return (mFlags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) != 0; + } + + /** {@link TrustAgentService.FLAG_GRANT_TRUST_DISPLAY_MESSAGE} */ + public boolean displayMessage() { + return (mFlags & TrustAgentService.FLAG_GRANT_TRUST_DISPLAY_MESSAGE) != 0; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TrustGrantFlags)) { + return false; + } + + return ((TrustGrantFlags) o).mFlags == this.mFlags; + } + + @Override + public int hashCode() { + return Objects.hash(mFlags); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(mFlags); + sb.append("]="); + + if (isInitiatedByUser()) { + sb.append("initiatedByUser|"); + } + if (dismissKeyguardRequested()) { + sb.append("dismissKeyguard|"); + } + if (temporaryAndRenewable()) { + sb.append("temporaryAndRenewable|"); + } + if (displayMessage()) { + sb.append("displayMessage|"); + } + + return sb.toString(); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index 9767313331d3..b514f60efc7d 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -20,6 +20,7 @@ import android.content.Context; import android.os.Handler; import android.os.UserHandle; +import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Main; @@ -50,6 +51,7 @@ public abstract class ClockRegistryModule { handler, featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), UserHandle.USER_ALL, - defaultClockProvider); + defaultClockProvider, + context.getString(R.string.lockscreen_clock_id_fallback)); } } diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java index 8fc86004c400..a7d4455b43c2 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java @@ -21,10 +21,7 @@ import com.android.systemui.R; import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.statusbar.phone.KeyguardStatusBarView; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherControllerImpl; -import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -50,10 +47,4 @@ public abstract class KeyguardStatusBarViewModule { static StatusBarUserSwitcherContainer getUserSwitcherContainer(KeyguardStatusBarView view) { return view.findViewById(R.id.user_switcher_container); } - - /** */ - @Binds - @KeyguardStatusBarViewScope - abstract StatusBarUserSwitcherController bindStatusBarUserSwitcherController( - StatusBarUserSwitcherControllerImpl controller); } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 81b8dfed36a8..676370093aee 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -25,6 +25,7 @@ import com.android.keyguard.ActiveUnlockConfig import com.android.keyguard.FaceAuthUiEvent import com.android.keyguard.KeyguardListenModel import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.keyguard.TrustGrantFlags import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel import com.android.systemui.plugins.log.LogLevel.DEBUG @@ -368,12 +369,16 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" }) } - fun logShowTrustGrantedMessage( + fun logTrustGrantedWithFlags( + flags: Int, + userId: Int, message: String? ) { logBuffer.log(TAG, DEBUG, { + int1 = flags + int2 = userId str1 = message - }, { "showTrustGrantedMessage message$str1" }) + }, { "trustGrantedWithFlags[user=$int2] flags=${TrustGrantFlags(int1)} message=$str1" }) } fun logTrustChanged( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING new file mode 100644 index 000000000000..794eba4d8de9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING @@ -0,0 +1,16 @@ +{ + "presubmit": [ + { + // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+) + "name": "SystemUIGoogleBiometricsScreenshotTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ] +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java index ad966125b9e8..bdad41348c95 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java @@ -37,6 +37,9 @@ public abstract class UdfpsAnimationView extends FrameLayout { private float mDialogSuggestedAlpha = 1f; private float mNotificationShadeExpansion = 0f; + // Used for Udfps ellipse detection when flag is true, set by AnimationViewController + boolean mUseExpandedOverlay = false; + // mAlpha takes into consideration the status bar expansion amount and dialog suggested alpha private int mAlpha; boolean mPauseAuth; @@ -118,6 +121,24 @@ public abstract class UdfpsAnimationView extends FrameLayout { } /** + * Converts coordinates of RectF relative to the screen to coordinates relative to this view. + * + * @param bounds RectF based off screen coordinates in current orientation + */ + RectF getBoundsRelativeToView(RectF bounds) { + int[] pos = getLocationOnScreen(); + + RectF output = new RectF( + bounds.left - pos[0], + bounds.top - pos[1], + bounds.right - pos[0], + bounds.bottom - pos[1] + ); + + return output; + } + + /** * Set the suggested alpha based on whether a dialog was recently shown or hidden. * @param dialogSuggestedAlpha value from 0f to 1f. */ diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 5469d298227c..1d4281fbf451 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -166,6 +166,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { // The current request from FingerprintService. Null if no current request. @Nullable UdfpsControllerOverlay mOverlay; + @Nullable private UdfpsEllipseDetection mUdfpsEllipseDetection; // The fingerprint AOD trigger doesn't provide an ACTION_UP/ACTION_CANCEL event to tell us when // to turn off high brightness mode. To get around this limitation, the state of the AOD @@ -354,6 +355,10 @@ public class UdfpsController implements DozeReceiver, Dumpable { if (!mOverlayParams.equals(overlayParams)) { mOverlayParams = overlayParams; + if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { + mUdfpsEllipseDetection.updateOverlayParams(overlayParams); + } + final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateBouncer(); // When the bounds change it's always necessary to re-create the overlay's window with @@ -493,8 +498,23 @@ public class UdfpsController implements DozeReceiver, Dumpable { mVelocityTracker.clear(); } - boolean withinSensorArea = + boolean withinSensorArea; + if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { + // Ellipse detection + withinSensorArea = mUdfpsEllipseDetection.isGoodEllipseOverlap(event); + } else { + // Centroid with expanded overlay + withinSensorArea = + isWithinSensorArea(udfpsView, event.getRawX(), + event.getRawY(), fromUdfpsView); + } + } else { + // Centroid with sensor sized view + withinSensorArea = isWithinSensorArea(udfpsView, event.getX(), event.getY(), fromUdfpsView); + } + if (withinSensorArea) { Trace.beginAsyncSection("UdfpsController.e2e.onPointerDown", 0); Log.v(TAG, "onTouch | action down"); @@ -525,9 +545,25 @@ public class UdfpsController implements DozeReceiver, Dumpable { ? event.getPointerId(0) : event.findPointerIndex(mActivePointerId); if (idx == event.getActionIndex()) { - boolean actionMoveWithinSensorArea = - isWithinSensorArea(udfpsView, event.getX(idx), event.getY(idx), - fromUdfpsView); + boolean actionMoveWithinSensorArea; + if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { + // Ellipse detection + actionMoveWithinSensorArea = + mUdfpsEllipseDetection.isGoodEllipseOverlap(event); + } else { + // Centroid with expanded overlay + actionMoveWithinSensorArea = + isWithinSensorArea(udfpsView, event.getRawX(idx), + event.getRawY(idx), fromUdfpsView); + } + } else { + // Centroid with sensor sized view + actionMoveWithinSensorArea = + isWithinSensorArea(udfpsView, event.getX(idx), + event.getY(idx), fromUdfpsView); + } + if ((fromUdfpsView || actionMoveWithinSensorArea) && shouldTryToDismissKeyguard()) { Log.v(TAG, "onTouch | dismiss keyguard ACTION_MOVE"); @@ -725,6 +761,10 @@ public class UdfpsController implements DozeReceiver, Dumpable { udfpsHapticsSimulator.setUdfpsController(this); udfpsShell.setUdfpsOverlayController(mUdfpsOverlayController); + + if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) { + mUdfpsEllipseDetection = new UdfpsEllipseDetection(mOverlayParams); + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 0bb24f8663ec..8db4927ee059 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -49,6 +49,7 @@ import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager @@ -103,6 +104,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private set private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams() + private var sensorBounds: Rect = Rect() private var overlayTouchListener: TouchExplorationStateChangeListener? = null @@ -120,6 +122,10 @@ class UdfpsControllerOverlay @JvmOverloads constructor( privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY // Avoid announcing window title. accessibilityTitle = " " + + if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + } } /** A helper if the [requestReason] was due to enrollment. */ @@ -160,6 +166,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean { if (overlayView == null) { overlayParams = params + sensorBounds = Rect(params.sensorBounds) try { overlayView = (inflater.inflate( R.layout.udfps_view, null, false @@ -178,6 +185,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( } windowManager.addView(this, coreLayoutParams.updateDimensions(animation)) + sensorRect = sensorBounds touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled overlayTouchListener = TouchExplorationStateChangeListener { if (accessibilityManager.isTouchExplorationEnabled) { @@ -194,6 +202,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( overlayTouchListener!! ) overlayTouchListener?.onTouchExplorationStateChanged(true) + useExpandedOverlay = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) } } catch (e: RuntimeException) { Log.e(TAG, "showUdfpsOverlay | failed to add window", e) @@ -225,13 +234,14 @@ class UdfpsControllerOverlay @JvmOverloads constructor( REASON_ENROLL_ENROLLING -> { UdfpsEnrollViewController( view.addUdfpsView(R.layout.udfps_enroll_view) { - updateSensorLocation(overlayParams.sensorBounds) + updateSensorLocation(sensorBounds) }, enrollHelper ?: throw IllegalStateException("no enrollment helper"), statusBarStateController, shadeExpansionStateManager, dialogManager, dumpManager, + featureFlags, overlayParams.scaleFactor ) } @@ -420,7 +430,12 @@ class UdfpsControllerOverlay @JvmOverloads constructor( } // Original sensorBounds assume portrait mode. - val rotatedSensorBounds = Rect(overlayParams.sensorBounds) + var rotatedBounds = + if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + Rect(overlayParams.overlayBounds) + } else { + Rect(overlayParams.sensorBounds) + } val rot = overlayParams.rotation if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { @@ -434,18 +449,27 @@ class UdfpsControllerOverlay @JvmOverloads constructor( } else { Log.v(TAG, "Rotate UDFPS bounds " + Surface.rotationToString(rot)) RotationUtils.rotateBounds( - rotatedSensorBounds, + rotatedBounds, overlayParams.naturalDisplayWidth, overlayParams.naturalDisplayHeight, rot ) + + if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + RotationUtils.rotateBounds( + sensorBounds, + overlayParams.naturalDisplayWidth, + overlayParams.naturalDisplayHeight, + rot + ) + } } } - x = rotatedSensorBounds.left - paddingX - y = rotatedSensorBounds.top - paddingY - height = rotatedSensorBounds.height() + 2 * paddingX - width = rotatedSensorBounds.width() + 2 * paddingY + x = rotatedBounds.left - paddingX + y = rotatedBounds.top - paddingY + height = rotatedBounds.height() + 2 * paddingX + width = rotatedBounds.width() + 2 * paddingY return this } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt new file mode 100644 index 000000000000..8ae4775467df --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt @@ -0,0 +1,92 @@ +/* + * 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.biometrics + +import android.graphics.Point +import android.graphics.Rect +import android.util.RotationUtils +import android.view.MotionEvent +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin + +private const val TAG = "UdfpsEllipseDetection" + +private const val NEEDED_POINTS = 2 + +class UdfpsEllipseDetection(overlayParams: UdfpsOverlayParams) { + var sensorRect = Rect() + var points: Array<Point> = emptyArray() + + init { + sensorRect = Rect(overlayParams.sensorBounds) + + points = calculateSensorPoints(sensorRect) + } + + fun updateOverlayParams(params: UdfpsOverlayParams) { + sensorRect = Rect(params.sensorBounds) + + val rot = params.rotation + RotationUtils.rotateBounds( + sensorRect, + params.naturalDisplayWidth, + params.naturalDisplayHeight, + rot + ) + + points = calculateSensorPoints(sensorRect) + } + + fun isGoodEllipseOverlap(event: MotionEvent): Boolean { + return points.count { checkPoint(event, it) } >= NEEDED_POINTS + } + + private fun checkPoint(event: MotionEvent, point: Point): Boolean { + // Calculate if sensor point is within ellipse + // Formula: ((cos(o)(xE - xS) + sin(o)(yE - yS))^2 / a^2) + ((sin(o)(xE - xS) + cos(o)(yE - + // yS))^2 / b^2) <= 1 + val a: Float = cos(event.orientation) * (point.x - event.rawX) + val b: Float = sin(event.orientation) * (point.y - event.rawY) + val c: Float = sin(event.orientation) * (point.x - event.rawX) + val d: Float = cos(event.orientation) * (point.y - event.rawY) + val result = + (a + b).pow(2) / (event.touchMinor / 2).pow(2) + + (c - d).pow(2) / (event.touchMajor / 2).pow(2) + + return result <= 1 + } +} + +fun calculateSensorPoints(sensorRect: Rect): Array<Point> { + val sensorX = sensorRect.centerX() + val sensorY = sensorRect.centerY() + val cornerOffset: Int = sensorRect.width() / 4 + val sideOffset: Int = sensorRect.width() / 3 + + return arrayOf( + Point(sensorX - cornerOffset, sensorY - cornerOffset), + Point(sensorX, sensorY - sideOffset), + Point(sensorX + cornerOffset, sensorY - cornerOffset), + Point(sensorX - sideOffset, sensorY), + Point(sensorX, sensorY), + Point(sensorX + sideOffset, sensorY), + Point(sensorX - cornerOffset, sensorY + cornerOffset), + Point(sensorX, sensorY + sideOffset), + Point(sensorX + cornerOffset, sensorY + cornerOffset) + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java index 49e378e4a76f..af7e0b6244df 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java @@ -99,12 +99,11 @@ public class UdfpsEnrollProgressBarDrawable extends Drawable { mProgressColor = context.getColor(R.color.udfps_enroll_progress); final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); mIsAccessibilityEnabled = am.isTouchExplorationEnabled(); + mOnFirstBucketFailedColor = context.getColor(R.color.udfps_moving_target_fill_error); if (!mIsAccessibilityEnabled) { mHelpColor = context.getColor(R.color.udfps_enroll_progress_help); - mOnFirstBucketFailedColor = context.getColor(R.color.udfps_moving_target_fill_error); } else { mHelpColor = context.getColor(R.color.udfps_enroll_progress_help_with_talkback); - mOnFirstBucketFailedColor = mHelpColor; } mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark); mCheckmarkDrawable.mutate(); @@ -197,6 +196,7 @@ public class UdfpsEnrollProgressBarDrawable extends Drawable { } } + mShowingHelp = showingHelp; mRemainingSteps = remainingSteps; mTotalSteps = totalSteps; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java index 69c37b2b9a62..87be42c3426e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java @@ -18,6 +18,7 @@ package com.android.systemui.biometrics; import android.content.Context; import android.graphics.Rect; +import android.graphics.RectF; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; @@ -41,6 +42,9 @@ public class UdfpsEnrollView extends UdfpsAnimationView { @NonNull private ImageView mFingerprintView; @NonNull private ImageView mFingerprintProgressView; + private LayoutParams mProgressParams; + private float mProgressBarRadius; + public UdfpsEnrollView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mFingerprintDrawable = new UdfpsEnrollDrawable(mContext); @@ -57,6 +61,32 @@ public class UdfpsEnrollView extends UdfpsAnimationView { } @Override + void onSensorRectUpdated(RectF bounds) { + if (mUseExpandedOverlay) { + RectF converted = getBoundsRelativeToView(bounds); + + mProgressParams = new LayoutParams( + (int) (converted.width() + mProgressBarRadius * 2), + (int) (converted.height() + mProgressBarRadius * 2)); + mProgressParams.setMargins( + (int) (converted.left - mProgressBarRadius), + (int) (converted.top - mProgressBarRadius), + (int) (converted.right + mProgressBarRadius), + (int) (converted.bottom + mProgressBarRadius) + ); + + mFingerprintProgressView.setLayoutParams(mProgressParams); + super.onSensorRectUpdated(converted); + } else { + super.onSensorRectUpdated(bounds); + } + } + + void setProgressBarRadius(float radius) { + mProgressBarRadius = radius; + } + + @Override public UdfpsDrawable getDrawable() { return mFingerprintDrawable; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java index e01273f2a092..40176654a8ec 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java @@ -21,6 +21,8 @@ import android.graphics.PointF; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.phone.SystemUIDialogManager; @@ -57,6 +59,7 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp @NonNull ShadeExpansionStateManager shadeExpansionStateManager, @NonNull SystemUIDialogManager systemUIDialogManager, @NonNull DumpManager dumpManager, + @NonNull FeatureFlags featureFlags, float scaleFactor) { super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager, dumpManager); @@ -64,6 +67,11 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp R.integer.config_udfpsEnrollProgressBar)); mEnrollHelper = enrollHelper; mView.setEnrollHelper(mEnrollHelper); + mView.setProgressBarRadius(mEnrollProgressBarRadius); + + if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { + mView.mUseExpandedOverlay = true; + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java index bc274a0af95f..339b8cafd8dd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java @@ -26,6 +26,7 @@ import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.RectF; import android.util.AttributeSet; import android.util.MathUtils; import android.view.View; @@ -75,6 +76,8 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { private int mAnimationType = ANIMATION_NONE; private boolean mFullyInflated; + private LayoutParams mParams; + public UdfpsKeyguardView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mFingerprintDrawable = new UdfpsFpDrawable(context); @@ -239,6 +242,22 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { updateAlpha(); } + @Override + void onSensorRectUpdated(RectF bounds) { + super.onSensorRectUpdated(bounds); + + if (mUseExpandedOverlay) { + mParams = new LayoutParams((int) bounds.width(), (int) bounds.height()); + RectF converted = getBoundsRelativeToView(bounds); + mParams.setMargins( + (int) converted.left, + (int) converted.top, + (int) converted.right, + (int) converted.bottom + ); + } + } + /** * Animates in the bg protection circle behind the fp icon to highlight the icon. */ @@ -277,6 +296,7 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { pw.println(" mUdfpsRequested=" + mUdfpsRequested); pw.println(" mInterpolatedDarkAmount=" + mInterpolatedDarkAmount); pw.println(" mAnimationType=" + mAnimationType); + pw.println(" mUseExpandedOverlay=" + mUseExpandedOverlay); } private final AsyncLayoutInflater.OnInflateFinishedListener mLayoutInflaterFinishListener = @@ -291,7 +311,12 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { updatePadding(); updateColor(); updateAlpha(); - parent.addView(view); + + if (mUseExpandedOverlay) { + parent.addView(view, mParams); + } else { + parent.addView(view); + } // requires call to invalidate to update the color mLockScreenFp.addValueCallback( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt index 91967f95c861..63144fcea761 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt @@ -52,7 +52,6 @@ import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch /** Class that coordinates non-HBM animations during keyguard authentication. */ @@ -82,6 +81,8 @@ constructor( systemUIDialogManager, dumpManager ) { + private val useExpandedOverlay: Boolean = + featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) private val isModernBouncerEnabled: Boolean = featureFlags.isEnabled(Flags.MODERN_BOUNCER) private var showingUdfpsBouncer = false private var udfpsRequested = false @@ -233,7 +234,13 @@ constructor( if (transitionToFullShadeProgress != 0f) { return } - udfpsController.onTouch(event) + + // Forwarding touches not needed with expanded overlay + if (useExpandedOverlay) { + return + } else { + udfpsController.onTouch(event) + } } } @@ -322,6 +329,7 @@ constructor( keyguardViewManager.setAlternateBouncer(mAlternateBouncer) lockScreenShadeTransitionController.udfpsKeyguardViewController = this activityLaunchAnimator.addListener(activityLaunchAnimatorListener) + view.mUseExpandedOverlay = useExpandedOverlay } override fun onViewDetached() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt index a15456d46897..4a8877edfa53 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt @@ -20,6 +20,7 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.PointF +import android.graphics.Rect import android.graphics.RectF import android.util.AttributeSet import android.util.Log @@ -38,9 +39,12 @@ class UdfpsView( attrs: AttributeSet? ) : FrameLayout(context, attrs), DozeReceiver { + // Use expanded overlay when feature flag is true, set by UdfpsViewController + var useExpandedOverlay: Boolean = false + // sensorRect may be bigger than the sensor. True sensor dimensions are defined in // overlayParams.sensorBounds - private val sensorRect = RectF() + var sensorRect = Rect() private var mUdfpsDisplayMode: UdfpsDisplayModeProvider? = null private val debugTextPaint = Paint().apply { isAntiAlias = true @@ -92,13 +96,19 @@ class UdfpsView( val paddingX = animationViewController?.paddingX ?: 0 val paddingY = animationViewController?.paddingY ?: 0 - sensorRect.set( - paddingX.toFloat(), - paddingY.toFloat(), - (overlayParams.sensorBounds.width() + paddingX).toFloat(), - (overlayParams.sensorBounds.height() + paddingY).toFloat() - ) - animationViewController?.onSensorRectUpdated(RectF(sensorRect)) + // Updates sensor rect in relation to the overlay view + if (useExpandedOverlay) { + animationViewController?.onSensorRectUpdated(RectF(sensorRect)) + } else { + sensorRect.set( + paddingX, + paddingY, + (overlayParams.sensorBounds.width() + paddingX), + (overlayParams.sensorBounds.height() + paddingY) + ) + + animationViewController?.onSensorRectUpdated(RectF(sensorRect)) + } } fun onTouchOutsideView() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt index 5110a9cfb33b..6fb8e345322c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt @@ -26,6 +26,7 @@ object CredentialPasswordViewBinder { view: CredentialPasswordView, host: CredentialView.Host, viewModel: CredentialViewModel, + requestFocusForInput: Boolean, ) { val imeManager = view.context.getSystemService(InputMethodManager::class.java)!! @@ -34,8 +35,10 @@ object CredentialPasswordViewBinder { val onBackInvokedCallback = OnBackInvokedCallback { host.onCredentialAborted() } view.repeatWhenAttached { - passwordField.requestFocus() - passwordField.scheduleShowSoftInput() + if (requestFocusForInput) { + passwordField.requestFocus() + passwordField.scheduleShowSoftInput() + } repeatOnLifecycle(Lifecycle.State.STARTED) { // observe credential validation attempts and submit/cancel buttons diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt index 4765551df3f0..b692ad35caee 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt @@ -9,7 +9,6 @@ import com.android.systemui.biometrics.ui.CredentialPatternView import com.android.systemui.biometrics.ui.CredentialView import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch /** Sub-binder for the [CredentialPatternView]. */ @@ -30,7 +29,7 @@ object CredentialPatternViewBinder { viewModel.header.collect { header -> lockPatternView.setOnPatternListener( OnPatternDetectedListener { pattern -> - if (pattern.isPatternLongEnough()) { + if (pattern.isPatternTooShort()) { // Pattern size is less than the minimum // do not count it as a failed attempt viewModel.showPatternTooShortError() @@ -71,5 +70,5 @@ private class OnPatternDetectedListener( } } -private fun List<LockPatternView.Cell>.isPatternLongEnough(): Boolean = +private fun List<LockPatternView.Cell>.isPatternTooShort(): Boolean = size < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt index fcc948756972..e2d36dc6abe1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt @@ -17,7 +17,6 @@ import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.lifecycle.repeatWhenAttached import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -40,6 +39,7 @@ object CredentialViewBinder { panelViewController: AuthPanelController, animatePanel: Boolean, maxErrorDuration: Long = 3_000L, + requestFocusForInput: Boolean = true, ) { val titleView: TextView = view.requireViewById(R.id.title) val subtitleView: TextView = view.requireViewById(R.id.subtitle) @@ -110,7 +110,8 @@ object CredentialViewBinder { // bind the auth widget when (view) { - is CredentialPasswordView -> CredentialPasswordViewBinder.bind(view, host, viewModel) + is CredentialPasswordView -> + CredentialPasswordViewBinder.bind(view, host, viewModel, requestFocusForInput) is CredentialPatternView -> CredentialPatternViewBinder.bind(view, host, viewModel) else -> throw IllegalStateException("unexpected view type: ${view.javaClass.name}") } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt index 115edd115ffe..c6428ef6eb8e 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt @@ -91,11 +91,12 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( override var currentUserId = userTracker.userId private set - private val serviceListingCallback = ServiceListing.Callback { + private val serviceListingCallback = ServiceListing.Callback { list -> + Log.d(TAG, "ServiceConfig reloaded, count: ${list.size}") + val newServices = list.map { ControlsServiceInfo(userTracker.userContext, it) } + // After here, `list` is not captured, so we don't risk modifying it outside of the callback backgroundExecutor.execute { if (userChangeInProgress.get() > 0) return@execute - Log.d(TAG, "ServiceConfig reloaded, count: ${it.size}") - val newServices = it.map { ControlsServiceInfo(userTracker.userContext, it) } if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) { newServices.forEach(ControlsServiceInfo::resolvePanelActivity) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java index d3555eec0243..b30e0c22e566 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java @@ -17,6 +17,7 @@ package com.android.systemui.dagger; import com.android.systemui.keyguard.KeyguardQuickAffordanceProvider; +import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; import dagger.Subcomponent; @@ -28,6 +29,7 @@ import dagger.Subcomponent; @Subcomponent(modules = { DefaultComponentBinder.class, DependencyProvider.class, + NotificationInsetsModule.class, QsFrameTranslateModule.class, SystemUIBinder.class, SystemUIModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index a14b0ee04d8a..6dc4f5c60108 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.KeyguardSliceProvider; import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.people.PeopleProvider; +import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.unfold.FoldStateLogger; @@ -65,6 +66,7 @@ import dagger.Subcomponent; @Subcomponent(modules = { DefaultComponentBinder.class, DependencyProvider.class, + NotificationInsetsModule.class, QsFrameTranslateModule.class, SystemUIBinder.class, SystemUIModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt index d537d4b51b6c..000bbe6afc50 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt +++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt @@ -54,6 +54,9 @@ class DemoModeController constructor( private val receiverMap: Map<String, MutableList<DemoMode>> init { + // Don't persist demo mode across restarts. + requestFinishDemoMode() + val m = mutableMapOf<String, MutableList<DemoMode>>() DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) @@ -74,7 +77,6 @@ class DemoModeController constructor( // content changes to know if the setting turned on or off tracker.startTracking() - // TODO: We should probably exit demo mode if we booted up with it on isInDemoMode = tracker.isInDemoMode val demoFilter = IntentFilter() diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index b69afeb37371..0c14ed5c2164 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -133,9 +133,9 @@ public class DozeLog implements Dumpable { /** * Appends fling event to the logs */ - public void traceFling(boolean expand, boolean aboveThreshold, boolean thresholdNeeded, + public void traceFling(boolean expand, boolean aboveThreshold, boolean screenOnFromTouch) { - mLogger.logFling(expand, aboveThreshold, thresholdNeeded, screenOnFromTouch); + mLogger.logFling(expand, aboveThreshold, screenOnFromTouch); } /** diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt index 18c8e01cbf76..b5dbe21a2cf7 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt @@ -96,13 +96,11 @@ class DozeLogger @Inject constructor( fun logFling( expand: Boolean, aboveThreshold: Boolean, - thresholdNeeded: Boolean, screenOnFromTouch: Boolean ) { buffer.log(TAG, DEBUG, { bool1 = expand bool2 = aboveThreshold - bool3 = thresholdNeeded bool4 = screenOnFromTouch }, { "Fling expand=$bool1 aboveThreshold=$bool2 thresholdNeeded=$bool3 " + diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java index b03ae59d0d61..81df4ed51d8d 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java @@ -337,7 +337,6 @@ public class FeatureFlagsDebug implements FeatureFlags { Log.i(TAG, "Android Restart Suppressed"); return; } - Log.i(TAG, "Restarting Android"); mRestarter.restart(); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt new file mode 100644 index 000000000000..3d9f62768a37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt @@ -0,0 +1,51 @@ +/* + * 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.flags + +import android.util.Log +import com.android.systemui.keyguard.WakefulnessLifecycle +import javax.inject.Inject + +/** Restarts SystemUI when the screen is locked. */ +class FeatureFlagsDebugRestarter +@Inject +constructor( + private val wakefulnessLifecycle: WakefulnessLifecycle, + private val systemExitRestarter: SystemExitRestarter, +) : Restarter { + + val observer = + object : WakefulnessLifecycle.Observer { + override fun onFinishedGoingToSleep() { + Log.d(FeatureFlagsDebug.TAG, "Restarting due to systemui flag change") + restartNow() + } + } + + override fun restart() { + Log.d(FeatureFlagsDebug.TAG, "Restart requested. Restarting on next screen off.") + if (wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP) { + restartNow() + } else { + wakefulnessLifecycle.addObserver(observer) + } + } + + private fun restartNow() { + systemExitRestarter.restart() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt new file mode 100644 index 000000000000..a3f0f665c056 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt @@ -0,0 +1,82 @@ +/* + * 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.flags + +import android.util.Log +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.util.concurrency.DelayableExecutor +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** Restarts SystemUI when the device appears idle. */ +class FeatureFlagsReleaseRestarter +@Inject +constructor( + private val wakefulnessLifecycle: WakefulnessLifecycle, + private val batteryController: BatteryController, + @Background private val bgExecutor: DelayableExecutor, + private val systemExitRestarter: SystemExitRestarter +) : Restarter { + var shouldRestart = false + var pendingRestart: Runnable? = null + + val observer = + object : WakefulnessLifecycle.Observer { + override fun onFinishedGoingToSleep() { + maybeScheduleRestart() + } + } + + val batteryCallback = + object : BatteryController.BatteryStateChangeCallback { + override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { + maybeScheduleRestart() + } + } + + override fun restart() { + Log.d(FeatureFlagsDebug.TAG, "Restart requested. Restarting when plugged in and idle.") + if (!shouldRestart) { + // Don't bother scheduling twice. + shouldRestart = true + wakefulnessLifecycle.addObserver(observer) + batteryController.addCallback(batteryCallback) + maybeScheduleRestart() + } + } + + private fun maybeScheduleRestart() { + if ( + wakefulnessLifecycle.wakefulness == WAKEFULNESS_ASLEEP && batteryController.isPluggedIn + ) { + if (pendingRestart == null) { + pendingRestart = bgExecutor.executeDelayed(this::restartNow, 30L, TimeUnit.SECONDS) + } + } else if (pendingRestart != null) { + pendingRestart?.run() + pendingRestart = null + } + } + + private fun restartNow() { + Log.d(FeatureFlagsRelease.TAG, "Restarting due to systemui flag change") + systemExitRestarter.restart() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 784e92d37e28..51691c27f537 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -61,6 +61,9 @@ object Flags { // TODO(b/254512517): Tracking Bug val FSI_REQUIRES_KEYGUARD = unreleasedFlag(110, "fsi_requires_keyguard", teamfood = true) + // TODO(b/259130119): Tracking Bug + val FSI_ON_DND_UPDATE = unreleasedFlag(259130119, "fsi_on_dnd_update", teamfood = true) + // TODO(b/254512538): Tracking Bug val INSTANT_VOICE_REPLY = unreleasedFlag(111, "instant_voice_reply", teamfood = true) @@ -90,7 +93,8 @@ object Flags { // TODO(b/257315550): Tracking Bug val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when") - // next id: 119 + val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD = + unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true) // 200 - keyguard/lockscreen // ** Flag retired ** @@ -141,9 +145,9 @@ object Flags { /** * Whether to enable the code powering customizable lock screen quick affordances. * - * Note that this flag does not enable individual implementations of quick affordances like the - * new camera quick affordance. Look for individual flags for those. + * This flag enables any new prebuilt quick affordances as well. */ + // TODO(b/255618149): Tracking Bug @JvmField val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES = unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = false) @@ -363,9 +367,7 @@ object Flags { // 1300 - screenshots // TODO(b/254512719): Tracking Bug - @JvmField - val SCREENSHOT_REQUEST_PROCESSOR = - unreleasedFlag(1300, "screenshot_request_processor", teamfood = true) + @JvmField val SCREENSHOT_REQUEST_PROCESSOR = releasedFlag(1300, "screenshot_request_processor") // TODO(b/254513155): Tracking Bug @JvmField @@ -386,9 +388,7 @@ object Flags { unreleasedFlag(1600, "a11y_floating_menu_fling_spring_animations") // 1700 - clipboard - @JvmField - val CLIPBOARD_OVERLAY_REFACTOR = - unreleasedFlag(1700, "clipboard_overlay_refactor", teamfood = true) + @JvmField val CLIPBOARD_OVERLAY_REFACTOR = releasedFlag(1700, "clipboard_overlay_refactor") @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = unreleasedFlag(1701, "clipboard_remote_behavior") // 1800 - shade container @@ -404,4 +404,10 @@ object Flags { // 2100 - Falsing Manager @JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps") + + // 2200 - udfps + // TODO(b/259264861): Tracking Bug + @JvmField val UDFPS_NEW_TOUCH_DETECTION = unreleasedFlag(2200, "udfps_new_touch_detection") + @JvmField val UDFPS_ELLIPSE_DEBUG_UI = unreleasedFlag(2201, "udfps_ellipse_debug") + @JvmField val UDFPS_ELLIPSE_DETECTION = unreleasedFlag(2202, "udfps_ellipse_detection") } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt index 18d7bcf9b3fc..8442230fc5b1 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt @@ -15,7 +15,6 @@ */ package com.android.systemui.flags -import com.android.internal.statusbar.IStatusBarService import dagger.Module import dagger.Provides import javax.inject.Named @@ -32,15 +31,5 @@ interface FlagsCommonModule { fun providesAllFlags(): Map<Int, Flag<*>> { return FlagsFactory.knownFlags.map { it.value.id to it.value }.toMap() } - - @JvmStatic - @Provides - fun providesRestarter(barService: IStatusBarService): Restarter { - return object : Restarter { - override fun restart() { - barService.restart() - } - } - } } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt new file mode 100644 index 000000000000..f1b1be47a84f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.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.flags + +import javax.inject.Inject + +class SystemExitRestarter @Inject constructor() : Restarter { + override fun restart() { + System.exit(0) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java index 1f52fc6e022c..9b2e6b8ee544 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java @@ -375,7 +375,7 @@ public class KeyguardIndicationRotateTextViewController extends public static final int INDICATION_TYPE_ALIGNMENT = 4; public static final int INDICATION_TYPE_TRANSIENT = 5; public static final int INDICATION_TYPE_TRUST = 6; - public static final int INDICATION_TYPE_RESTING = 7; + public static final int INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE = 7; public static final int INDICATION_TYPE_USER_LOCKED = 8; public static final int INDICATION_TYPE_REVERSE_CHARGING = 10; public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE = 11; @@ -390,7 +390,7 @@ public class KeyguardIndicationRotateTextViewController extends INDICATION_TYPE_ALIGNMENT, INDICATION_TYPE_TRANSIENT, INDICATION_TYPE_TRUST, - INDICATION_TYPE_RESTING, + INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE, INDICATION_TYPE_USER_LOCKED, INDICATION_TYPE_REVERSE_CHARGING, INDICATION_TYPE_BIOMETRIC_MESSAGE, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt index 0f4581ce3e61..1f1ed007fca0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt @@ -31,7 +31,6 @@ import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCall import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract import javax.inject.Inject -import kotlinx.coroutines.runBlocking class KeyguardQuickAffordanceProvider : ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer { @@ -57,6 +56,11 @@ class KeyguardQuickAffordanceProvider : Contract.SelectionTable.TABLE_NAME, MATCH_CODE_ALL_SELECTIONS, ) + addURI( + Contract.AUTHORITY, + Contract.FlagsTable.TABLE_NAME, + MATCH_CODE_ALL_FLAGS, + ) } override fun onCreate(): Boolean { @@ -77,6 +81,7 @@ class KeyguardQuickAffordanceProvider : when (uriMatcher.match(uri)) { MATCH_CODE_ALL_SLOTS, MATCH_CODE_ALL_AFFORDANCES, + MATCH_CODE_ALL_FLAGS, MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd." else -> null } @@ -86,6 +91,7 @@ class KeyguardQuickAffordanceProvider : MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME + MATCH_CODE_ALL_FLAGS -> Contract.FlagsTable.TABLE_NAME else -> null } @@ -115,6 +121,7 @@ class KeyguardQuickAffordanceProvider : MATCH_CODE_ALL_AFFORDANCES -> queryAffordances() MATCH_CODE_ALL_SLOTS -> querySlots() MATCH_CODE_ALL_SELECTIONS -> querySelections() + MATCH_CODE_ALL_FLAGS -> queryFlags() else -> null } } @@ -171,12 +178,11 @@ class KeyguardQuickAffordanceProvider : throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!") } - val success = runBlocking { + val success = interactor.select( slotId = slotId, affordanceId = affordanceId, ) - } return if (success) { Log.d(TAG, "Successfully selected $affordanceId for slot $slotId") @@ -196,7 +202,7 @@ class KeyguardQuickAffordanceProvider : ) ) .apply { - val affordanceIdsBySlotId = runBlocking { interactor.getSelections() } + val affordanceIdsBySlotId = interactor.getSelections() affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) -> affordanceIds.forEach { affordanceId -> addRow( @@ -250,6 +256,29 @@ class KeyguardQuickAffordanceProvider : } } + private fun queryFlags(): Cursor { + return MatrixCursor( + arrayOf( + Contract.FlagsTable.Columns.NAME, + Contract.FlagsTable.Columns.VALUE, + ) + ) + .apply { + interactor.getPickerFlags().forEach { flag -> + addRow( + arrayOf( + flag.name, + if (flag.value) { + 1 + } else { + 0 + }, + ) + ) + } + } + } + private fun deleteSelection( uri: Uri, selectionArgs: Array<out String>?, @@ -271,12 +300,11 @@ class KeyguardQuickAffordanceProvider : ) } - val deleted = runBlocking { + val deleted = interactor.unselect( slotId = slotId, affordanceId = affordanceId, ) - } return if (deleted) { Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId") @@ -293,5 +321,6 @@ class KeyguardQuickAffordanceProvider : private const val MATCH_CODE_ALL_SLOTS = 1 private const val MATCH_CODE_ALL_AFFORDANCES = 2 private const val MATCH_CODE_ALL_SELECTIONS = 3 + private const val MATCH_CODE_ALL_FLAGS = 4 } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt index a069582f6692..f5220b8fae92 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt @@ -24,6 +24,7 @@ package com.android.systemui.keyguard.data.quickaffordance */ object BuiltInKeyguardQuickAffordanceKeys { // Please keep alphabetical order of const names to simplify future maintenance. + const val CAMERA = "camera" const val HOME_CONTROLS = "home" const val QR_CODE_SCANNER = "qr_code_scanner" const val QUICK_ACCESS_WALLET = "wallet" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt new file mode 100644 index 000000000000..3c09aab60443 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt @@ -0,0 +1,62 @@ +/* + * 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.keyguard.data.quickaffordance + +import android.app.StatusBarManager +import android.content.Context +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.camera.CameraGestureHelper +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +@SysUISingleton +class CameraQuickAffordanceConfig @Inject constructor( + @Application private val context: Context, + private val cameraGestureHelper: CameraGestureHelper, +) : KeyguardQuickAffordanceConfig { + + override val key: String + get() = BuiltInKeyguardQuickAffordanceKeys.CAMERA + + override val pickerName: String + get() = context.getString(R.string.accessibility_camera_button) + + override val pickerIconResourceId: Int + get() = com.android.internal.R.drawable.perm_group_camera + + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> + get() = flowOf( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = Icon.Resource( + com.android.internal.R.drawable.perm_group_camera, + ContentDescription.Resource(R.string.accessibility_camera_button) + ) + ) + ) + + override fun onTriggered(expandable: Expandable?): KeyguardQuickAffordanceConfig.OnTriggeredResult { + cameraGestureHelper.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt index bea9363efc81..f7225a249eda 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt @@ -29,8 +29,10 @@ object KeyguardDataQuickAffordanceModule { home: HomeControlsKeyguardQuickAffordanceConfig, quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, + camera: CameraQuickAffordanceConfig, ): Set<KeyguardQuickAffordanceConfig> { return setOf( + camera, home, quickAccessWallet, qrCodeScanner, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt new file mode 100644 index 000000000000..766096f1fa2b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt @@ -0,0 +1,214 @@ +/* + * 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.keyguard.data.quickaffordance + +import android.os.UserHandle +import android.provider.Settings +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.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer.Companion.BINDINGS +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Keeps quick affordance selections and legacy user settings in sync. + * + * "Legacy user settings" are user settings like: Settings > Display > Lock screen > "Show device + * controls" Settings > Display > Lock screen > "Show wallet" + * + * Quick affordance selections are the ones available through the new custom lock screen experience + * from Settings > Wallpaper & Style. + * + * This class keeps these in sync, mostly for backwards compatibility purposes and in order to not + * "forget" an existing legacy user setting when the device gets updated with a version of System UI + * that has the new customizable lock screen feature. + * + * The way it works is that, when [startSyncing] is called, the syncer starts coroutines to listen + * for changes in both legacy user settings and their respective affordance selections. Whenever one + * of each pair is changed, the other member of that pair is also updated to match. For example, if + * the user turns on "Show device controls", we automatically select the home controls affordance + * for the preferred slot. Conversely, when the home controls affordance is unselected by the user, + * we set the "Show device controls" setting to "off". + * + * The class can be configured by updating its list of triplets in the code under [BINDINGS]. + */ +@SysUISingleton +class KeyguardQuickAffordanceLegacySettingSyncer +@Inject +constructor( + @Application private val scope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val secureSettings: SecureSettings, + private val selectionsManager: KeyguardQuickAffordanceSelectionManager, +) { + companion object { + private val BINDINGS = + listOf( + Binding( + settingsKey = Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS, + ), + Binding( + settingsKey = Settings.Secure.LOCKSCREEN_SHOW_WALLET, + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + affordanceId = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET, + ), + Binding( + settingsKey = Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER, + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + affordanceId = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER, + ), + ) + } + + fun startSyncing( + bindings: List<Binding> = BINDINGS, + ): Job { + return scope.launch { bindings.forEach { binding -> startSyncing(this, binding) } } + } + + private fun startSyncing( + scope: CoroutineScope, + binding: Binding, + ) { + secureSettings + .observerFlow( + names = arrayOf(binding.settingsKey), + userId = UserHandle.USER_ALL, + ) + .map { + isSet( + settingsKey = binding.settingsKey, + ) + } + .distinctUntilChanged() + .onEach { isSet -> + if (isSelected(binding.affordanceId) != isSet) { + if (isSet) { + select( + slotId = binding.slotId, + affordanceId = binding.affordanceId, + ) + } else { + unselect( + affordanceId = binding.affordanceId, + ) + } + } + } + .flowOn(backgroundDispatcher) + .launchIn(scope) + + selectionsManager.selections + .map { it.values.flatten().toSet() } + .map { it.contains(binding.affordanceId) } + .distinctUntilChanged() + .onEach { isSelected -> + if (isSet(binding.settingsKey) != isSelected) { + set(binding.settingsKey, isSelected) + } + } + .flowOn(backgroundDispatcher) + .launchIn(scope) + } + + private fun isSelected( + affordanceId: String, + ): Boolean { + return selectionsManager + .getSelections() // Map<String, List<String>> + .values // Collection<List<String>> + .flatten() // List<String> + .toSet() // Set<String> + .contains(affordanceId) + } + + private fun select( + slotId: String, + affordanceId: String, + ) { + val affordanceIdsAtSlotId = selectionsManager.getSelections()[slotId] ?: emptyList() + selectionsManager.setSelections( + slotId = slotId, + affordanceIds = affordanceIdsAtSlotId + listOf(affordanceId), + ) + } + + private fun unselect( + affordanceId: String, + ) { + val currentSelections = selectionsManager.getSelections() + val slotIdsContainingAffordanceId = + currentSelections + .filter { (_, affordanceIds) -> affordanceIds.contains(affordanceId) } + .map { (slotId, _) -> slotId } + + slotIdsContainingAffordanceId.forEach { slotId -> + val currentAffordanceIds = currentSelections[slotId] ?: emptyList() + val affordanceIdsAfterUnselecting = + currentAffordanceIds.toMutableList().apply { remove(affordanceId) } + + selectionsManager.setSelections( + slotId = slotId, + affordanceIds = affordanceIdsAfterUnselecting, + ) + } + } + + private fun isSet( + settingsKey: String, + ): Boolean { + return secureSettings.getIntForUser( + settingsKey, + 0, + UserHandle.USER_CURRENT, + ) != 0 + } + + private suspend fun set( + settingsKey: String, + isSet: Boolean, + ) { + withContext(backgroundDispatcher) { + secureSettings.putInt( + settingsKey, + if (isSet) 1 else 0, + ) + } + } + + data class Binding( + val settingsKey: String, + val slotId: String, + val affordanceId: String, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt index 9c9354fec695..b29cf45cc709 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt @@ -17,46 +17,138 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting +import com.android.systemui.R +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.dagger.qualifiers.Application +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest /** * Manages and provides access to the current "selections" of keyguard quick affordances, answering * the question "which affordances should the keyguard show?". */ @SysUISingleton -class KeyguardQuickAffordanceSelectionManager @Inject constructor() { +class KeyguardQuickAffordanceSelectionManager +@Inject +constructor( + @Application context: Context, + private val userFileManager: UserFileManager, + private val userTracker: UserTracker, +) { - // TODO(b/254858695): implement a persistence layer (database). - private val _selections = MutableStateFlow<Map<String, List<String>>>(emptyMap()) + private val sharedPrefs: SharedPreferences + get() = + userFileManager.getSharedPreferences( + FILE_NAME, + Context.MODE_PRIVATE, + userTracker.userId, + ) + + private val userId: Flow<Int> = conflatedCallbackFlow { + val callback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + trySendWithFailureLogging(newUser, TAG) + } + } + + userTracker.addCallback(callback) { it.run() } + trySendWithFailureLogging(userTracker.userId, TAG) + + awaitClose { userTracker.removeCallback(callback) } + } + private val defaults: Map<String, List<String>> by lazy { + context.resources + .getStringArray(R.array.config_keyguardQuickAffordanceDefaults) + .associate { item -> + val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER) + check(splitUp.size == 2) + val slotId = splitUp[0] + val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER) + slotId to affordanceIds + } + } /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ - val selections: Flow<Map<String, List<String>>> = _selections.asStateFlow() + val selections: Flow<Map<String, List<String>>> = + userId.flatMapLatest { + conflatedCallbackFlow { + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + trySend(getSelections()) + } + + sharedPrefs.registerOnSharedPreferenceChangeListener(listener) + send(getSelections()) + + awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } + } + } /** * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in * descending priority order. */ - suspend fun getSelections(): Map<String, List<String>> { - return _selections.value + fun getSelections(): Map<String, List<String>> { + val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } + val result = + slotKeys + .associate { key -> + val slotId = key.substring(KEY_PREFIX_SLOT.length) + val value = sharedPrefs.getString(key, null) + val affordanceIds = + if (!value.isNullOrEmpty()) { + value.split(AFFORDANCE_DELIMITER) + } else { + emptyList() + } + slotId to affordanceIds + } + .toMutableMap() + + // If the result map is missing keys, it means that the system has never set anything for + // those slots. This is where we need examine our defaults and see if there should be a + // default value for the affordances in the slot IDs that are missing from the result. + // + // Once the user makes any selection for a slot, even when they select "None", this class + // will persist a key for that slot ID. In the case of "None", it will have a value of the + // empty string. This is why this system works. + defaults.forEach { (slotId, affordanceIds) -> + if (!result.containsKey(slotId)) { + result[slotId] = affordanceIds + } + } + + return result } /** * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance * IDs should be descending priority order. */ - suspend fun setSelections( + fun setSelections( slotId: String, affordanceIds: List<String>, ) { - // Must make a copy of the map and update it, otherwise, the MutableStateFlow won't emit - // when we set its value to the same instance of the original map, even if we change the - // map by updating the value of one of its keys. - val copy = _selections.value.toMutableMap() - copy[slotId] = affordanceIds - _selections.value = copy + val key = "$KEY_PREFIX_SLOT$slotId" + val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) + sharedPrefs.edit().putString(key, value).apply() + } + + companion object { + private const val TAG = "KeyguardQuickAffordanceSelectionManager" + @VisibleForTesting const val FILE_NAME = "quick_affordance_selections" + private const val KEY_PREFIX_SLOT = "slot_" + private const val SLOT_AFFORDANCES_DELIMITER = ":" + private const val AFFORDANCE_DELIMITER = "," } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt index 95f614fbf7b1..533b3abf4fb6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -17,31 +17,31 @@ package com.android.systemui.keyguard.data.repository +import android.content.Context +import com.android.systemui.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.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation -import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch /** Abstracts access to application state related to keyguard quick affordances. */ @SysUISingleton class KeyguardQuickAffordanceRepository @Inject constructor( + @Application private val appContext: Context, @Application private val scope: CoroutineScope, - @Background private val backgroundDispatcher: CoroutineDispatcher, private val selectionManager: KeyguardQuickAffordanceSelectionManager, + legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer, private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>, ) { /** @@ -61,11 +61,39 @@ constructor( initialValue = emptyMap(), ) + private val _slotPickerRepresentations: List<KeyguardSlotPickerRepresentation> by lazy { + fun parseSlot(unparsedSlot: String): Pair<String, Int> { + val split = unparsedSlot.split(SLOT_CONFIG_DELIMITER) + check(split.size == 2) + val slotId = split[0] + val slotCapacity = split[1].toInt() + return slotId to slotCapacity + } + + val unparsedSlots = + appContext.resources.getStringArray(R.array.config_keyguardQuickAffordanceSlots) + + val seenSlotIds = mutableSetOf<String>() + unparsedSlots.mapNotNull { unparsedSlot -> + val (slotId, slotCapacity) = parseSlot(unparsedSlot) + check(!seenSlotIds.contains(slotId)) { "Duplicate slot \"$slotId\"!" } + seenSlotIds.add(slotId) + KeyguardSlotPickerRepresentation( + id = slotId, + maxSelectedAffordances = slotCapacity, + ) + } + } + + init { + legacySettingSyncer.startSyncing() + } + /** * Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the * slot with the given ID. The configs are sorted in descending priority order. */ - suspend fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> { + fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> { val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList()) return configs.filter { selections.contains(it.key) } } @@ -74,7 +102,7 @@ constructor( * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs * are sorted in descending priority order. */ - suspend fun getSelections(): Map<String, List<String>> { + fun getSelections(): Map<String, List<String>> { return selectionManager.getSelections() } @@ -86,12 +114,10 @@ constructor( slotId: String, affordanceIds: List<String>, ) { - scope.launch(backgroundDispatcher) { - selectionManager.setSelections( - slotId = slotId, - affordanceIds = affordanceIds, - ) - } + selectionManager.setSelections( + slotId = slotId, + affordanceIds = affordanceIds, + ) } /** @@ -115,14 +141,10 @@ constructor( * each slot and select which affordance(s) is/are installed in each slot on the keyguard. */ fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> { - // TODO(b/256195304): source these from a config XML file. - return listOf( - KeyguardSlotPickerRepresentation( - id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, - ), - KeyguardSlotPickerRepresentation( - id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, - ), - ) + return _slotPickerRepresentations + } + + companion object { + private const val SLOT_CONFIG_DELIMITER = ":" } } 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 92caa89bb0e8..45eb6f501287 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 @@ -28,11 +28,13 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry +import com.android.systemui.keyguard.shared.model.KeyguardPickerFlag import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy @@ -117,7 +119,7 @@ constructor( * * @return `true` if the affordance was selected successfully; `false` otherwise. */ - suspend fun select(slotId: String, affordanceId: String): Boolean { + fun select(slotId: String, affordanceId: String): Boolean { check(isUsingRepository) val slots = repository.get().getSlotPickerRepresentations() @@ -152,7 +154,7 @@ constructor( * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if * the affordance was not on the slot to begin with). */ - suspend fun unselect(slotId: String, affordanceId: String?): Boolean { + fun unselect(slotId: String, affordanceId: String?): Boolean { check(isUsingRepository) val slots = repository.get().getSlotPickerRepresentations() @@ -187,7 +189,7 @@ constructor( } /** Returns affordance IDs indexed by slot ID, for all known slots. */ - suspend fun getSelections(): Map<String, List<String>> { + fun getSelections(): Map<String, List<String>> { check(isUsingRepository) val selections = repository.get().getSelections() @@ -314,6 +316,15 @@ constructor( return repository.get().getSlotPickerRepresentations() } + fun getPickerFlags(): List<KeyguardPickerFlag> { + return listOf( + KeyguardPickerFlag( + name = KeyguardQuickAffordanceProviderContract.FlagsTable.FLAG_NAME_FEATURE_ENABLED, + value = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES), + ) + ) + } + companion object { private const val TAG = "KeyguardQuickAffordanceInteractor" private const val DELIMITER = "::" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt new file mode 100644 index 000000000000..a7a5957cd527 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt @@ -0,0 +1,24 @@ +/* + * 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.keyguard.shared.model + +/** Represents a flag that's consumed by the settings or wallpaper picker app. */ +data class KeyguardPickerFlag( + val name: String, + val value: Boolean, +) 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 14dd99023b92..3012bb41445e 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 @@ -82,7 +82,6 @@ import com.android.systemui.util.traceSection import java.io.IOException import java.io.PrintWriter import java.util.concurrent.Executor -import java.util.concurrent.Executors import javax.inject.Inject // URI fields to try loading album art from @@ -154,6 +153,7 @@ private fun allowMediaRecommendations(context: Context): Boolean { class MediaDataManager( private val context: Context, @Background private val backgroundExecutor: Executor, + @Main private val uiExecutor: Executor, @Main private val foregroundExecutor: DelayableExecutor, private val mediaControllerFactory: MediaControllerFactory, private val broadcastDispatcher: BroadcastDispatcher, @@ -171,7 +171,8 @@ class MediaDataManager( private val systemClock: SystemClock, private val tunerService: TunerService, private val mediaFlags: MediaFlags, - private val logger: MediaUiEventLogger + private val logger: MediaUiEventLogger, + private val smartspaceManager: SmartspaceManager, ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener { companion object { @@ -218,6 +219,7 @@ class MediaDataManager( constructor( context: Context, @Background backgroundExecutor: Executor, + @Main uiExecutor: Executor, @Main foregroundExecutor: DelayableExecutor, mediaControllerFactory: MediaControllerFactory, dumpManager: DumpManager, @@ -233,10 +235,12 @@ class MediaDataManager( clock: SystemClock, tunerService: TunerService, mediaFlags: MediaFlags, - logger: MediaUiEventLogger + logger: MediaUiEventLogger, + smartspaceManager: SmartspaceManager, ) : this( context, backgroundExecutor, + uiExecutor, foregroundExecutor, mediaControllerFactory, broadcastDispatcher, @@ -254,7 +258,8 @@ class MediaDataManager( clock, tunerService, mediaFlags, - logger + logger, + smartspaceManager, ) private val appChangeReceiver = @@ -314,21 +319,18 @@ class MediaDataManager( // Register for Smartspace data updates. smartspaceMediaDataProvider.registerListener(this) - val smartspaceManager: SmartspaceManager = - context.getSystemService(SmartspaceManager::class.java) smartspaceSession = smartspaceManager.createSmartspaceSession( SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() ) smartspaceSession?.let { it.addOnTargetsAvailableListener( - // Use a new thread listening to Smartspace updates instead of using the existing - // backgroundExecutor. SmartspaceSession has scheduled routine updates which can be - // unpredictable on test simulators, using the backgroundExecutor makes it's hard to - // test the threads numbers. - // Switch to use backgroundExecutor when SmartspaceSession has a good way to be - // mocked. - Executors.newCachedThreadPool(), + // Use a main uiExecutor thread listening to Smartspace updates instead of using + // the existing background executor. + // SmartspaceSession has scheduled routine updates which can be unpredictable on + // test simulators, using the backgroundExecutor makes it's hard to test the threads + // numbers. + uiExecutor, SmartspaceSession.OnTargetsAvailableListener { targets -> smartspaceMediaDataProvider.onTargetsAvailable(targets) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index 9ba3501c3434..03bb7a0f45da 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -32,8 +32,6 @@ import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.globalactions.GlobalActionsDialogLite import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.FgsManagerController @@ -42,10 +40,9 @@ import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig -import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.security.data.repository.SecurityRepository import com.android.systemui.statusbar.policy.DeviceProvisionedController -import com.android.systemui.user.UserSwitcherActivity +import com.android.systemui.user.domain.interactor.UserInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -100,13 +97,12 @@ class FooterActionsInteractorImpl @Inject constructor( private val activityStarter: ActivityStarter, - private val featureFlags: FeatureFlags, private val metricsLogger: MetricsLogger, private val uiEventLogger: UiEventLogger, private val deviceProvisionedController: DeviceProvisionedController, private val qsSecurityFooterUtils: QSSecurityFooterUtils, private val fgsManagerController: FgsManagerController, - private val userSwitchDialogController: UserSwitchDialogController, + private val userInteractor: UserInteractor, securityRepository: SecurityRepository, foregroundServicesRepository: ForegroundServicesRepository, userSwitcherRepository: UserSwitcherRepository, @@ -182,22 +178,6 @@ constructor( } override fun showUserSwitcher(context: Context, expandable: Expandable) { - if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - userSwitchDialogController.showDialog(context, expandable) - return - } - - val intent = - Intent(context, UserSwitcherActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - } - - activityStarter.startActivity( - intent, - true /* dismissShade */, - expandable.activityLaunchController(), - true /* showOverlockscreenwhenlocked */, - UserHandle.SYSTEM, - ) + userInteractor.showUserSwitcher(context, expandable) } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index 7143ba263570..b4934cf7b804 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -38,6 +38,7 @@ import android.graphics.drawable.Icon; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; @@ -81,7 +82,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; private String mScreenshotId; - private final boolean mSmartActionsEnabled; private final Random mRandom = new Random(); private final Supplier<ActionTransition> mSharedElementTransition; private final ImageExporter mImageExporter; @@ -109,8 +109,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { mParams = data; // Initialize screenshot notification smart actions provider. - mSmartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, true); mSmartActionsProvider = screenshotNotificationSmartActionsProvider; } @@ -131,8 +129,16 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Bitmap image = mParams.image; mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, requestId); + + boolean savingToOtherUser = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY) + && (user != Process.myUserHandle()); + // Smart actions don't yet work for cross-user saves. + boolean smartActionsEnabled = !savingToOtherUser + && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, + true); try { - if (mSmartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) { + if (smartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) { // Since Quick Share target recommendation does not rely on image URL, it is // queried and surfaced before image compress/export. Action intent would not be // used, because it does not contain image URL. @@ -150,10 +156,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { CompletableFuture<List<Notification.Action>> smartActionsFuture = mScreenshotSmartActions.getSmartActionsFuture( mScreenshotId, uri, image, mSmartActionsProvider, REGULAR_SMART_ACTIONS, - mSmartActionsEnabled, user); - + smartActionsEnabled, user); List<Notification.Action> smartActions = new ArrayList<>(); - if (mSmartActionsEnabled) { + if (smartActionsEnabled) { int timeoutMs = DeviceConfig.getInt( DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS, @@ -168,9 +173,12 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { mImageData.uri = uri; mImageData.owner = user; mImageData.smartActions = smartActions; - mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri); - mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri); - mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri); + mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri, + smartActionsEnabled); + mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri, + smartActionsEnabled); + mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri, + smartActionsEnabled); mImageData.quickShareAction = createQuickShareAction(mContext, mQuickShareData.quickShareAction, uri); mImageData.subject = getSubjectString(); @@ -228,7 +236,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { * Assumes that the action intent is sent immediately after being supplied. */ @VisibleForTesting - Supplier<ActionTransition> createShareAction(Context context, Resources r, Uri uri) { + Supplier<ActionTransition> createShareAction(Context context, Resources r, Uri uri, + boolean smartActionsEnabled) { return () -> { ActionTransition transition = mSharedElementTransition.get(); @@ -274,7 +283,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { .putExtra(ScreenshotController.EXTRA_DISALLOW_ENTER_PIP, true) .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, - mSmartActionsEnabled) + smartActionsEnabled) .setAction(Intent.ACTION_SEND) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, @@ -290,7 +299,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } @VisibleForTesting - Supplier<ActionTransition> createEditAction(Context context, Resources r, Uri uri) { + Supplier<ActionTransition> createEditAction(Context context, Resources r, Uri uri, + boolean smartActionsEnabled) { return () -> { ActionTransition transition = mSharedElementTransition.get(); // Note: Both the share and edit actions are proxied through ActionProxyReceiver in @@ -323,7 +333,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent) .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, - mSmartActionsEnabled) + smartActionsEnabled) .putExtra(ScreenshotController.EXTRA_OVERRIDE_TRANSITION, true) .setAction(Intent.ACTION_EDIT) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), @@ -339,7 +349,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } @VisibleForTesting - Notification.Action createDeleteAction(Context context, Resources r, Uri uri) { + Notification.Action createDeleteAction(Context context, Resources r, Uri uri, + boolean smartActionsEnabled) { // Make sure pending intents for the system user are still unique across users // by setting the (otherwise unused) request code to the current user id. int requestCode = mContext.getUserId(); @@ -350,7 +361,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { .putExtra(ScreenshotController.SCREENSHOT_URI_ID, uri.toString()) .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, - mSmartActionsEnabled) + smartActionsEnabled) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT @@ -391,7 +402,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Intent intent = new Intent(context, SmartActionsReceiver.class) .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, action.actionIntent) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled); + addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */); PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, mRandom.nextInt(), intent, @@ -445,7 +456,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Intent intent = new Intent(context, SmartActionsReceiver.class) .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, updatedPendingIntent) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled); + // We only query for quick share actions when smart actions are enabled, so we can assert + // that it's true here. + addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */); PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, mRandom.nextInt(), intent, @@ -464,7 +477,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { mScreenshotSmartActions.getSmartActionsFuture( mScreenshotId, null, image, mSmartActionsProvider, QUICK_SHARE_ACTION, - mSmartActionsEnabled, user); + true /* smartActionsEnabled */, user); int timeoutMs = DeviceConfig.getInt( DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_QUICK_SHARE_ACTIONS_TIMEOUT_MS, diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index ceef8c8ff31c..b92cf5a0a49d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -177,6 +177,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; +import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -253,6 +254,8 @@ public final class NotificationPanelViewController implements Dumpable { private static final int FLING_COLLAPSE = 1; /** Fling until QS is completely hidden. */ private static final int FLING_HIDE = 2; + /** The delay to reset the hint text when the hint animation is finished running. */ + private static final int HINT_RESET_DELAY_MS = 1200; private static final long ANIMATION_DELAY_ICON_FADE_IN = ActivityLaunchAnimator.TIMINGS.getTotalDuration() - CollapsedStatusBarFragment.FADE_IN_DURATION @@ -343,6 +346,7 @@ public final class NotificationPanelViewController implements Dumpable { private final FalsingTapListener mFalsingTapListener = this::falsingAdditionalTapRequired; private final FragmentListener mQsFragmentListener = new QsFragmentListener(); private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate(); + private final NotificationGutsManager mGutsManager; private long mDownTime; private boolean mTouchSlopExceededBeforeDown; @@ -625,7 +629,6 @@ public final class NotificationPanelViewController implements Dumpable { private float mLastGesturedOverExpansion = -1; /** Whether the current animator is the spring back animation. */ private boolean mIsSpringBackAnimation; - private boolean mInSplitShade; private float mHintDistance; private float mInitialOffsetOnTouch; private boolean mCollapsedAndHeadsUpOnDown; @@ -702,6 +705,7 @@ public final class NotificationPanelViewController implements Dumpable { ConversationNotificationManager conversationNotificationManager, MediaHierarchyManager mediaHierarchyManager, StatusBarKeyguardViewManager statusBarKeyguardViewManager, + NotificationGutsManager gutsManager, NotificationsQSContainerController notificationsQSContainerController, NotificationStackScrollLayoutController notificationStackScrollLayoutController, KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory, @@ -755,6 +759,7 @@ public final class NotificationPanelViewController implements Dumpable { mLockscreenGestureLogger = lockscreenGestureLogger; mShadeExpansionStateManager = shadeExpansionStateManager; mShadeLog = shadeLogger; + mGutsManager = gutsManager; mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { @@ -1061,7 +1066,6 @@ public final class NotificationPanelViewController implements Dumpable { mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier(); mHintDistance = mResources.getDimension(R.dimen.hint_move_distance); mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount); - mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade); mFlingAnimationUtils = mFlingAnimationUtilsBuilder.get() .setMaxLengthSeconds(0.4f).build(); mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mView.getContext()); @@ -1569,23 +1573,31 @@ public final class NotificationPanelViewController implements Dumpable { // Find the clock, so we can exclude it from this transition. FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large); - View clockView = clockContainerView.getChildAt(0); - transition.excludeTarget(clockView, /* exclude= */ true); + // The clock container can sometimes be null. If it is, just fall back to the + // old animation rather than setting up the custom animations. + if (clockContainerView == null || clockContainerView.getChildCount() == 0) { + TransitionManager.beginDelayedTransition( + mNotificationContainerParent, transition); + } else { + View clockView = clockContainerView.getChildAt(0); + + transition.excludeTarget(clockView, /* exclude= */ true); - TransitionSet set = new TransitionSet(); - set.addTransition(transition); + TransitionSet set = new TransitionSet(); + set.addTransition(transition); - SplitShadeTransitionAdapter adapter = - new SplitShadeTransitionAdapter(mKeyguardStatusViewController); + SplitShadeTransitionAdapter adapter = + new SplitShadeTransitionAdapter(mKeyguardStatusViewController); - // Use linear here, so the actual clock can pick its own interpolator. - adapter.setInterpolator(Interpolators.LINEAR); - adapter.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - adapter.addTarget(clockView); - set.addTransition(adapter); + // Use linear here, so the actual clock can pick its own interpolator. + adapter.setInterpolator(Interpolators.LINEAR); + adapter.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); + adapter.addTarget(clockView); + set.addTransition(adapter); - TransitionManager.beginDelayedTransition(mNotificationContainerParent, set); + TransitionManager.beginDelayedTransition(mNotificationContainerParent, set); + } } else { TransitionManager.beginDelayedTransition( mNotificationContainerParent, transition); @@ -1760,7 +1772,7 @@ public final class NotificationPanelViewController implements Dumpable { } public void resetViews(boolean animate) { - mCentralSurfaces.getGutsManager().closeAndSaveGuts(true /* leavebehind */, true /* force */, + mGutsManager.closeAndSaveGuts(true /* leavebehind */, true /* force */, true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */); if (animate && !isFullyCollapsed()) { animateCloseQs(true /* animateAway */); @@ -1836,6 +1848,10 @@ public final class NotificationPanelViewController implements Dumpable { public void closeQs() { cancelQsAnimation(); setQsExpansionHeight(mQsMinExpansionHeight); + // qsExpandImmediate is a safety latch in case we're calling closeQS while we're in the + // middle of animation - we need to make sure that value is always false when shade if + // fully collapsed or expanded + setQsExpandImmediate(false); } @VisibleForTesting @@ -1938,7 +1954,7 @@ public final class NotificationPanelViewController implements Dumpable { // we want to perform an overshoot animation when flinging open final boolean addOverscroll = expand - && !mInSplitShade // Split shade has its own overscroll logic + && !mSplitShadeEnabled // Split shade has its own overscroll logic && mStatusBarStateController.getState() != KEYGUARD && mOverExpansion == 0.0f && vel >= 0; @@ -2727,8 +2743,10 @@ public final class NotificationPanelViewController implements Dumpable { * as well based on the bounds of the shade and QS state. */ private void setQSClippingBounds() { - final int qsPanelBottomY = calculateQsBottomPosition(computeQsExpansionFraction()); - final boolean qsVisible = (computeQsExpansionFraction() > 0 || qsPanelBottomY > 0); + float qsExpansionFraction = computeQsExpansionFraction(); + final int qsPanelBottomY = calculateQsBottomPosition(qsExpansionFraction); + final boolean qsVisible = (qsExpansionFraction > 0 || qsPanelBottomY > 0); + checkCorrectScrimVisibility(qsExpansionFraction); int top = calculateTopQsClippingBound(qsPanelBottomY); int bottom = calculateBottomQsClippingBound(top); @@ -2739,6 +2757,19 @@ public final class NotificationPanelViewController implements Dumpable { applyQSClippingBounds(left, top, right, bottom, qsVisible); } + private void checkCorrectScrimVisibility(float expansionFraction) { + // issues with scrims visible on keyguard occur only in split shade + if (mSplitShadeEnabled) { + boolean keyguardViewsVisible = mBarState == KEYGUARD && mKeyguardOnlyContentAlpha == 1; + // expansionFraction == 1 means scrims are fully visible as their size/visibility depend + // on QS expansion + if (expansionFraction == 1 && keyguardViewsVisible) { + Log.wtf(TAG, + "Incorrect state, scrim is visible at the same time when clock is visible"); + } + } + } + private int calculateTopQsClippingBound(int qsPanelBottomY) { int top; if (mSplitShadeEnabled) { @@ -3686,7 +3717,6 @@ public final class NotificationPanelViewController implements Dumpable { private void onTrackingStopped(boolean expand) { mFalsingCollector.onTrackingStopped(); mTracking = false; - mCentralSurfaces.onTrackingStopped(expand); updatePanelExpansionAndVisibility(); if (expand) { mNotificationStackScrollLayoutController.setOverScrollAmount(0.0f, true /* onTop */, @@ -3729,14 +3759,16 @@ public final class NotificationPanelViewController implements Dumpable { @VisibleForTesting void onUnlockHintFinished() { - mCentralSurfaces.onHintFinished(); + // Delay the reset a bit so the user can read the text. + mKeyguardIndicationController.hideTransientIndicationDelayed(HINT_RESET_DELAY_MS); mScrimController.setExpansionAffectsAlpha(true); mNotificationStackScrollLayoutController.setUnlockHintRunning(false); } @VisibleForTesting void onUnlockHintStarted() { - mCentralSurfaces.onUnlockHintStarted(); + mFalsingCollector.onUnlockHintStarted(); + mKeyguardIndicationController.showActionToUnlock(); mScrimController.setExpansionAffectsAlpha(false); mNotificationStackScrollLayoutController.setUnlockHintRunning(true); } @@ -4393,7 +4425,7 @@ public final class NotificationPanelViewController implements Dumpable { ipw.print("mPanelFlingOvershootAmount="); ipw.println(mPanelFlingOvershootAmount); ipw.print("mLastGesturedOverExpansion="); ipw.println(mLastGesturedOverExpansion); ipw.print("mIsSpringBackAnimation="); ipw.println(mIsSpringBackAnimation); - ipw.print("mInSplitShade="); ipw.println(mInSplitShade); + ipw.print("mSplitShadeEnabled="); ipw.println(mSplitShadeEnabled); ipw.print("mHintDistance="); ipw.println(mHintDistance); ipw.print("mInitialOffsetOnTouch="); ipw.println(mInitialOffsetOnTouch); ipw.print("mCollapsedAndHeadsUpOnDown="); ipw.println(mCollapsedAndHeadsUpOnDown); @@ -4762,7 +4794,6 @@ public final class NotificationPanelViewController implements Dumpable { } mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold, - mCentralSurfaces.isFalsingThresholdNeeded(), mCentralSurfaces.isWakeUpComingFromTouch()); // Log collapse gesture if on lock screen. if (!expand && onKeyguard) { @@ -4811,9 +4842,6 @@ public final class NotificationPanelViewController implements Dumpable { */ private boolean isFalseTouch(float x, float y, @Classifier.InteractionType int interactionType) { - if (!mCentralSurfaces.isFalsingThresholdNeeded()) { - return false; - } if (mFalsingManager.isClassifierEnabled()) { return mFalsingManager.isFalseTouch(interactionType); } @@ -4911,7 +4939,7 @@ public final class NotificationPanelViewController implements Dumpable { float maxPanelHeight = getMaxPanelTransitionDistance(); if (mHeightAnimator == null) { // Split shade has its own overscroll logic - if (mTracking && !mInSplitShade) { + if (mTracking && !mSplitShadeEnabled) { float overExpansionPixels = Math.max(0, h - maxPanelHeight); setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */); } @@ -5459,6 +5487,12 @@ public final class NotificationPanelViewController implements Dumpable { // - from SHADE to KEYGUARD // - from SHADE_LOCKED to SHADE // - getting notified again about the current SHADE or KEYGUARD state + if (mSplitShadeEnabled && oldState == SHADE && statusBarState == KEYGUARD) { + // user can go to keyguard from different shade states and closing animation + // may not fully run - we always want to make sure we close QS when that happens + // as we never need QS open in fresh keyguard state + closeQs(); + } final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE && statusBarState == KEYGUARD && mScreenOffAnimationController.isKeyguardShowDelayed(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java index 400b0baea01b..6acf417f0ea6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java @@ -24,6 +24,7 @@ import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG; import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.LayoutRes; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; @@ -36,6 +37,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Trace; import android.util.AttributeSet; +import android.util.Pair; import android.view.ActionMode; import android.view.DisplayCutout; import android.view.InputQueue; @@ -74,6 +76,7 @@ public class NotificationShadeWindowView extends FrameLayout { private ViewTreeObserver.OnPreDrawListener mFloatingToolbarPreDrawListener; private InteractionEventHandler mInteractionEventHandler; + private LayoutInsetsController mLayoutInsetProvider; public NotificationShadeWindowView(Context context, AttributeSet attrs) { super(context, attrs); @@ -108,12 +111,10 @@ public class NotificationShadeWindowView extends FrameLayout { mLeftInset = 0; mRightInset = 0; DisplayCutout displayCutout = getRootWindowInsets().getDisplayCutout(); - if (displayCutout != null) { - mLeftInset = displayCutout.getSafeInsetLeft(); - mRightInset = displayCutout.getSafeInsetRight(); - } - mLeftInset = Math.max(insets.left, mLeftInset); - mRightInset = Math.max(insets.right, mRightInset); + Pair<Integer, Integer> pairInsets = mLayoutInsetProvider + .getinsets(windowInsets, displayCutout); + mLeftInset = pairInsets.first; + mRightInset = pairInsets.second; applyMargins(); return windowInsets; } @@ -172,6 +173,10 @@ public class NotificationShadeWindowView extends FrameLayout { mInteractionEventHandler = listener; } + protected void setLayoutInsetsController(LayoutInsetsController provider) { + mLayoutInsetProvider = provider; + } + @Override public boolean dispatchTouchEvent(MotionEvent ev) { Boolean result = mInteractionEventHandler.handleDispatchTouchEvent(ev); @@ -353,6 +358,18 @@ public class NotificationShadeWindowView extends FrameLayout { } } + /** + * Controller responsible for calculating insets for the shade window. + */ + public interface LayoutInsetsController { + + /** + * Update the insets and calculate them accordingly. + */ + Pair<Integer, Integer> getinsets(@Nullable WindowInsets windowInsets, + @Nullable DisplayCutout displayCutout); + } + interface InteractionEventHandler { /** * Returns a result for {@link ViewGroup#dispatchTouchEvent(MotionEvent)} or null to defer diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index bb67280c07b8..8379e51f7449 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -42,6 +42,7 @@ import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.statusbar.DragDownHelper; import com.android.systemui.statusbar.LockscreenShadeTransitionController; +import com.android.systemui.statusbar.NotificationInsetsController; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; @@ -76,6 +77,7 @@ public class NotificationShadeWindowViewController { private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final AmbientState mAmbientState; private final PulsingGestureListener mPulsingGestureListener; + private final NotificationInsetsController mNotificationInsetsController; private GestureDetector mPulsingWakeupGestureHandler; private View mBrightnessMirror; @@ -111,6 +113,7 @@ public class NotificationShadeWindowViewController { CentralSurfaces centralSurfaces, NotificationShadeWindowController controller, KeyguardUnlockAnimationController keyguardUnlockAnimationController, + NotificationInsetsController notificationInsetsController, AmbientState ambientState, PulsingGestureListener pulsingGestureListener, FeatureFlags featureFlags, @@ -134,6 +137,7 @@ public class NotificationShadeWindowViewController { mKeyguardUnlockAnimationController = keyguardUnlockAnimationController; mAmbientState = ambientState; mPulsingGestureListener = pulsingGestureListener; + mNotificationInsetsController = notificationInsetsController; // This view is not part of the newly inflated expanded status bar. mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container); @@ -165,6 +169,7 @@ public class NotificationShadeWindowViewController { mPulsingWakeupGestureHandler = new GestureDetector(mView.getContext(), mPulsingGestureListener); + mView.setLayoutInsetsController(mNotificationInsetsController); mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() { @Override public Boolean handleDispatchTouchEvent(MotionEvent ev) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 2101efb61443..0f27420e22b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -36,7 +36,7 @@ import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewCont import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_DISCLOSURE; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_LOGOUT; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_OWNER_INFO; -import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_RESTING; +import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRUST; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED; import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON; @@ -67,6 +67,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; @@ -74,6 +75,7 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.keyguard.TrustGrantFlags; import com.android.keyguard.logging.KeyguardLogger; import com.android.settingslib.Utils; import com.android.settingslib.fuelgauge.BatteryStatus; @@ -154,11 +156,12 @@ public class KeyguardIndicationController { private final AccessibilityManager mAccessibilityManager; private final Handler mHandler; - protected KeyguardIndicationRotateTextViewController mRotateTextViewController; + @VisibleForTesting + public KeyguardIndicationRotateTextViewController mRotateTextViewController; private BroadcastReceiver mBroadcastReceiver; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private String mRestingIndication; + private String mPersistentUnlockMessage; private String mAlignmentIndication; private CharSequence mTrustGrantedIndication; private CharSequence mTransientIndication; @@ -195,7 +198,9 @@ public class KeyguardIndicationController { public void onScreenTurnedOn() { mHandler.removeMessages(MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON); if (mBiometricErrorMessageToShowOnScreenOn != null) { - showBiometricMessage(mBiometricErrorMessageToShowOnScreenOn); + String followUpMessage = mFaceLockedOutThisAuthSession + ? faceLockedOutFollowupMessage() : null; + showBiometricMessage(mBiometricErrorMessageToShowOnScreenOn, followUpMessage); // We want to keep this message around in case the screen was off hideBiometricMessageDelayed(DEFAULT_HIDE_DELAY_MS); mBiometricErrorMessageToShowOnScreenOn = null; @@ -374,7 +379,7 @@ public class KeyguardIndicationController { updateLockScreenTrustMsg(userId, getTrustGrantedIndication(), getTrustManagedIndication()); updateLockScreenAlignmentMsg(); updateLockScreenLogoutView(); - updateLockScreenRestingMsg(); + updateLockScreenPersistentUnlockMsg(); } private void updateOrganizedOwnedDevice() { @@ -480,7 +485,8 @@ public class KeyguardIndicationController { } private void updateLockScreenUserLockedMsg(int userId) { - if (!mKeyguardUpdateMonitor.isUserUnlocked(userId)) { + if (!mKeyguardUpdateMonitor.isUserUnlocked(userId) + || mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId)) { mRotateTextViewController.updateIndication( INDICATION_TYPE_USER_LOCKED, new KeyguardIndication.Builder() @@ -585,18 +591,17 @@ public class KeyguardIndicationController { } } - private void updateLockScreenRestingMsg() { - if (!TextUtils.isEmpty(mRestingIndication) - && !mRotateTextViewController.hasIndications()) { + private void updateLockScreenPersistentUnlockMsg() { + if (!TextUtils.isEmpty(mPersistentUnlockMessage)) { mRotateTextViewController.updateIndication( - INDICATION_TYPE_RESTING, + INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE, new KeyguardIndication.Builder() - .setMessage(mRestingIndication) + .setMessage(mPersistentUnlockMessage) .setTextColor(mInitialTextColorState) .build(), - false); + true); } else { - mRotateTextViewController.hideIndication(INDICATION_TYPE_RESTING); + mRotateTextViewController.hideIndication(INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE); } } @@ -679,11 +684,8 @@ public class KeyguardIndicationController { } } - /** - * Sets the indication that is shown if nothing else is showing. - */ - public void setRestingIndication(String restingIndication) { - mRestingIndication = restingIndication; + private void setPersistentUnlockMessage(String persistentUnlockMessage) { + mPersistentUnlockMessage = persistentUnlockMessage; updateDeviceEntryIndication(false); } @@ -1117,6 +1119,9 @@ public class KeyguardIndicationController { public void onLockedOutStateChanged(BiometricSourceType biometricSourceType) { if (biometricSourceType == FACE && !mKeyguardUpdateMonitor.isFaceLockedOut()) { mFaceLockedOutThisAuthSession = false; + } else if (biometricSourceType == FINGERPRINT) { + setPersistentUnlockMessage(mKeyguardUpdateMonitor.isFingerprintLockedOut() + ? mContext.getString(R.string.keyguard_unlock) : ""); } } @@ -1188,9 +1193,9 @@ public class KeyguardIndicationController { } @Override - public void onTrustGrantedWithFlags(int flags, int userId, @Nullable String message) { - if (!isCurrentUser(userId)) return; - showTrustGrantedMessage(flags, message); + public void onTrustGrantedForCurrentUser(boolean dismissKeyguard, + @NonNull TrustGrantFlags flags, @Nullable String message) { + showTrustGrantedMessage(dismissKeyguard, message); } @Override @@ -1254,15 +1259,13 @@ public class KeyguardIndicationController { return getCurrentUser() == userId; } - void showTrustGrantedMessage(int flags, @Nullable CharSequence message) { + protected void showTrustGrantedMessage(boolean dismissKeyguard, @Nullable String message) { mTrustGrantedIndication = message; updateDeviceEntryIndication(false); } private void handleFaceLockoutError(String errString) { - int followupMsgId = canUnlockWithFingerprint() ? R.string.keyguard_suggest_fingerprint - : R.string.keyguard_unlock; - String followupMessage = mContext.getString(followupMsgId); + String followupMessage = faceLockedOutFollowupMessage(); // Lockout error can happen multiple times in a session because we trigger face auth // even when it is locked out so that the user is aware that face unlock would have // triggered but didn't because it is locked out. @@ -1280,6 +1283,12 @@ public class KeyguardIndicationController { } } + private String faceLockedOutFollowupMessage() { + int followupMsgId = canUnlockWithFingerprint() ? R.string.keyguard_suggest_fingerprint + : R.string.keyguard_unlock; + return mContext.getString(followupMsgId); + } + private static boolean isLockoutError(int msgId) { return msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT || msgId == FaceManager.FACE_ERROR_LOCKOUT; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java new file mode 100644 index 000000000000..39d7d6675ed4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java @@ -0,0 +1,26 @@ +/* + * 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.statusbar; + +import com.android.systemui.shade.NotificationShadeWindowView; + +/** + * Calculates insets for the notification shade window view. + */ +public abstract class NotificationInsetsController + implements NotificationShadeWindowView.LayoutInsetsController { +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsImpl.java new file mode 100644 index 000000000000..1ed704e66c76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsImpl.java @@ -0,0 +1,58 @@ +/* + * 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.statusbar; + +import static android.view.WindowInsets.Type.systemBars; + +import android.annotation.Nullable; +import android.graphics.Insets; +import android.util.Pair; +import android.view.DisplayCutout; +import android.view.WindowInsets; + +import com.android.systemui.dagger.SysUISingleton; + +import javax.inject.Inject; + +/** + * Default implementation of NotificationsInsetsController. + */ +@SysUISingleton +public class NotificationInsetsImpl extends NotificationInsetsController { + + @Inject + public NotificationInsetsImpl() { + + } + + @Override + public Pair<Integer, Integer> getinsets(@Nullable WindowInsets windowInsets, + @Nullable DisplayCutout displayCutout) { + final Insets insets = windowInsets.getInsetsIgnoringVisibility(systemBars()); + int leftInset = 0; + int rightInset = 0; + + if (displayCutout != null) { + leftInset = displayCutout.getSafeInsetLeft(); + rightInset = displayCutout.getSafeInsetRight(); + } + leftInset = Math.max(insets.left, leftInset); + rightInset = Math.max(insets.right, rightInset); + + return new Pair(leftInset, rightInset); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsModule.java new file mode 100644 index 000000000000..614bc0f1a6f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsModule.java @@ -0,0 +1,30 @@ +/* + * 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.systemui.statusbar; + +import com.android.systemui.dagger.SysUISingleton; + +import dagger.Binds; +import dagger.Module; + +@Module +public interface NotificationInsetsModule { + + @Binds + @SysUISingleton + NotificationInsetsController bindNotificationInsetsController(NotificationInsetsImpl impl); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index cd130854ff3f..d7eddf53dea5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -71,6 +71,7 @@ public class NotificationShelf extends ActivatableNotificationView implements private int[] mTmp = new int[2]; private boolean mHideBackground; private int mStatusBarHeight; + private boolean mEnableNotificationClipping; private AmbientState mAmbientState; private NotificationStackScrollLayoutController mHostLayoutController; private int mPaddingBetweenElements; @@ -117,7 +118,7 @@ public class NotificationShelf extends ActivatableNotificationView implements // Setting this to first in section to get the clipping to the top roundness correct. This // value determines the way we are clipping to the top roundness of the overall shade setFirstInSection(true); - initDimens(); + updateResources(); } public void bind(AmbientState ambientState, @@ -126,7 +127,7 @@ public class NotificationShelf extends ActivatableNotificationView implements mHostLayoutController = hostLayoutController; } - private void initDimens() { + private void updateResources() { Resources res = getResources(); mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext); mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height); @@ -144,6 +145,7 @@ public class NotificationShelf extends ActivatableNotificationView implements mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf); mCornerAnimationDistance = res.getDimensionPixelSize( R.dimen.notification_corner_animation_distance); + mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping); mShelfIcons.setInNotificationIconShelf(true); if (!mShowNotificationShelf) { @@ -154,7 +156,7 @@ public class NotificationShelf extends ActivatableNotificationView implements @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - initDimens(); + updateResources(); } @Override @@ -639,7 +641,8 @@ public class NotificationShelf extends ActivatableNotificationView implements } if (!isPinned) { if (viewEnd > notificationClipEnd && !shouldClipOwnTop) { - int clipBottomAmount = (int) (viewEnd - notificationClipEnd); + int clipBottomAmount = + mEnableNotificationClipping ? (int) (viewEnd - notificationClipEnd) : 0; view.setClipBottomAmount(clipBottomAmount); } else { view.setClipBottomAmount(0); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java index c070fccf9808..324e97294f4e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java @@ -24,17 +24,21 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; +import androidx.annotation.VisibleForTesting; + import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Background; import org.jetbrains.annotations.NotNull; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import javax.inject.Inject; /** - * + * A Helper class that offloads {@link Vibrator} calls to a different thread. + * {@link Vibrator} makes blocking calls that may cause SysUI to ANR. + * TODO(b/245528624): Use regular Vibrator instance once new APIs are available. */ @SysUISingleton public class VibratorHelper { @@ -53,10 +57,18 @@ public class VibratorHelper { private final Executor mExecutor; /** - * + * Creates a vibrator helper on a new single threaded {@link Executor}. */ @Inject - public VibratorHelper(@Nullable Vibrator vibrator, @Background Executor executor) { + public VibratorHelper(@Nullable Vibrator vibrator) { + this(vibrator, Executors.newSingleThreadExecutor()); + } + + /** + * Creates new vibrator helper on a specific {@link Executor}. + */ + @VisibleForTesting + public VibratorHelper(@Nullable Vibrator vibrator, Executor executor) { mExecutor = executor; mVibrator = vibrator; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt index 2734511de78c..7eb890677206 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -40,4 +40,8 @@ class NotifPipelineFlags @Inject constructor( val isSemiStableSortEnabled: Boolean by lazy { featureFlags.isEnabled(Flags.SEMI_STABLE_SORT) } + + val shouldFilterUnseenNotifsOnKeyguard: Boolean by lazy { + featureFlags.isEnabled(Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java deleted file mode 100644 index e3d71c8b29d9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.collection.coordinator; - -import androidx.annotation.NonNull; - -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope; -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; -import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider; -import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; - -import javax.inject.Inject; - -/** - * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section - * headers on the lockscreen. - */ -@CoordinatorScope -public class KeyguardCoordinator implements Coordinator { - private static final String TAG = "KeyguardCoordinator"; - private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; - private final SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider; - private final StatusBarStateController mStatusBarStateController; - - @Inject - public KeyguardCoordinator( - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, - SectionHeaderVisibilityProvider sectionHeaderVisibilityProvider, - StatusBarStateController statusBarStateController) { - mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider; - mSectionHeaderVisibilityProvider = sectionHeaderVisibilityProvider; - mStatusBarStateController = statusBarStateController; - } - - @Override - public void attach(NotifPipeline pipeline) { - - setupInvalidateNotifListCallbacks(); - // Filter at the "finalize" stage so that views remain bound by PreparationCoordinator - pipeline.addFinalizeFilter(mNotifFilter); - mKeyguardNotificationVisibilityProvider - .addOnStateChangedListener(this::invalidateListFromFilter); - updateSectionHeadersVisibility(); - } - - private final NotifFilter mNotifFilter = new NotifFilter(TAG) { - @Override - public boolean shouldFilterOut(@NonNull NotificationEntry entry, long now) { - return mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry); - } - }; - - // TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on - // these same updates - private void setupInvalidateNotifListCallbacks() { - - } - - private void invalidateListFromFilter(String reason) { - updateSectionHeadersVisibility(); - mNotifFilter.invalidateList(reason); - } - - private void updateSectionHeadersVisibility() { - boolean onKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD; - boolean neverShowSections = mSectionHeaderVisibilityProvider.getNeverShowSectionHeaders(); - boolean showSections = !onKeyguard && !neverShowSections; - mSectionHeaderVisibilityProvider.setSectionHeadersVisible(showSections); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt new file mode 100644 index 000000000000..6e5fcebf780f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt @@ -0,0 +1,147 @@ +/* + * 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.statusbar.notification.collection.coordinator + +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.NotifPipelineFlags +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider +import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +/** + * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section + * headers on the lockscreen. + */ +@CoordinatorScope +class KeyguardCoordinator +@Inject +constructor( + private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, + private val keyguardRepository: KeyguardRepository, + private val notifPipelineFlags: NotifPipelineFlags, + @Application private val scope: CoroutineScope, + private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, + private val statusBarStateController: StatusBarStateController, +) : Coordinator { + + private val unseenNotifications = mutableSetOf<NotificationEntry>() + + override fun attach(pipeline: NotifPipeline) { + setupInvalidateNotifListCallbacks() + // Filter at the "finalize" stage so that views remain bound by PreparationCoordinator + pipeline.addFinalizeFilter(notifFilter) + keyguardNotificationVisibilityProvider.addOnStateChangedListener(::invalidateListFromFilter) + updateSectionHeadersVisibility() + if (notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard) { + attachUnseenFilter(pipeline) + } + } + + private fun attachUnseenFilter(pipeline: NotifPipeline) { + pipeline.addFinalizeFilter(unseenNotifFilter) + pipeline.addCollectionListener(collectionListener) + scope.launch { clearUnseenWhenKeyguardIsDismissed() } + } + + private suspend fun clearUnseenWhenKeyguardIsDismissed() { + // Use collectLatest so that the suspending block is cancelled if isKeyguardShowing changes + // during the timeout period + keyguardRepository.isKeyguardShowing.collectLatest { isKeyguardShowing -> + if (!isKeyguardShowing) { + unseenNotifFilter.invalidateList("keyguard no longer showing") + delay(SEEN_TIMEOUT) + unseenNotifications.clear() + } + } + } + + private val collectionListener = + object : NotifCollectionListener { + override fun onEntryAdded(entry: NotificationEntry) { + if (keyguardRepository.isKeyguardShowing()) { + unseenNotifications.add(entry) + } + } + + override fun onEntryUpdated(entry: NotificationEntry) { + if (keyguardRepository.isKeyguardShowing()) { + unseenNotifications.add(entry) + } + } + + override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + unseenNotifications.remove(entry) + } + } + + @VisibleForTesting + internal val unseenNotifFilter = + object : NotifFilter("$TAG-unseen") { + override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = + when { + // Don't apply filter if the keyguard isn't currently showing + !keyguardRepository.isKeyguardShowing() -> false + // Don't apply the filter if the notification is unseen + unseenNotifications.contains(entry) -> false + // Don't apply the filter to (non-promoted) group summaries + // - summary will be pruned if necessary, depending on if children are filtered + entry.parent?.summary == entry -> false + else -> true + } + } + + private val notifFilter: NotifFilter = + object : NotifFilter(TAG) { + override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = + keyguardNotificationVisibilityProvider.shouldHideNotification(entry) + } + + // TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on + // these same updates + private fun setupInvalidateNotifListCallbacks() {} + + private fun invalidateListFromFilter(reason: String) { + updateSectionHeadersVisibility() + notifFilter.invalidateList(reason) + } + + private fun updateSectionHeadersVisibility() { + val onKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD + val neverShowSections = sectionHeaderVisibilityProvider.neverShowSectionHeaders + val showSections = !onKeyguard && !neverShowSections + sectionHeaderVisibilityProvider.sectionHeadersVisible = showSections + } + + companion object { + private const val TAG = "KeyguardCoordinator" + private val SEEN_TIMEOUT = 5.seconds + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java index 3002a6820990..a2379b270f49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java @@ -29,6 +29,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeStateEvents; +import com.android.systemui.statusbar.notification.VisibilityLocationProvider; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -62,6 +63,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable, private final HeadsUpManager mHeadsUpManager; private final ShadeStateEvents mShadeStateEvents; private final StatusBarStateController mStatusBarStateController; + private final VisibilityLocationProvider mVisibilityLocationProvider; private final VisualStabilityProvider mVisualStabilityProvider; private final WakefulnessLifecycle mWakefulnessLifecycle; @@ -94,9 +96,11 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable, HeadsUpManager headsUpManager, ShadeStateEvents shadeStateEvents, StatusBarStateController statusBarStateController, + VisibilityLocationProvider visibilityLocationProvider, VisualStabilityProvider visualStabilityProvider, WakefulnessLifecycle wakefulnessLifecycle) { mHeadsUpManager = headsUpManager; + mVisibilityLocationProvider = visibilityLocationProvider; mVisualStabilityProvider = visualStabilityProvider; mWakefulnessLifecycle = wakefulnessLifecycle; mStatusBarStateController = statusBarStateController; @@ -123,6 +127,11 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable, // HUNs to the top of the shade private final NotifStabilityManager mNotifStabilityManager = new NotifStabilityManager("VisualStabilityCoordinator") { + private boolean canMoveForHeadsUp(NotificationEntry entry) { + return entry != null && mHeadsUpManager.isAlerting(entry.getKey()) + && !mVisibilityLocationProvider.isInVisibleLocation(entry); + } + @Override public void onBeginRun() { mIsSuppressingPipelineRun = false; @@ -140,7 +149,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable, @Override public boolean isGroupChangeAllowed(@NonNull NotificationEntry entry) { final boolean isGroupChangeAllowedForEntry = - mReorderingAllowed || mHeadsUpManager.isAlerting(entry.getKey()); + mReorderingAllowed || canMoveForHeadsUp(entry); mIsSuppressingGroupChange |= !isGroupChangeAllowedForEntry; return isGroupChangeAllowedForEntry; } @@ -156,7 +165,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable, public boolean isSectionChangeAllowed(@NonNull NotificationEntry entry) { final boolean isSectionChangeAllowedForEntry = mReorderingAllowed - || mHeadsUpManager.isAlerting(entry.getKey()) + || canMoveForHeadsUp(entry) || mEntriesThatCanChangeSection.containsKey(entry.getKey()); if (!isSectionChangeAllowedForEntry) { mEntriesWithSuppressedSectionChange.add(entry.getKey()); @@ -165,8 +174,8 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable, } @Override - public boolean isEntryReorderingAllowed(@NonNull ListEntry section) { - return mReorderingAllowed; + public boolean isEntryReorderingAllowed(@NonNull ListEntry entry) { + return mReorderingAllowed || canMoveForHeadsUp(entry.getRepresentativeEntry()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisibilityLocationProviderDelegator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisibilityLocationProviderDelegator.kt new file mode 100644 index 000000000000..4bc4ecf5dae9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisibilityLocationProviderDelegator.kt @@ -0,0 +1,38 @@ +/* + * 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.statusbar.notification.collection.provider + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.VisibilityLocationProvider +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import javax.inject.Inject + +/** + * An injectable component which delegates the visibility location computation to a delegate which + * can be initialized after the initial injection, generally because it's provided by a view. + */ +@SysUISingleton +class VisibilityLocationProviderDelegator @Inject constructor() : VisibilityLocationProvider { + private var delegate: VisibilityLocationProvider? = null + + fun setDelegate(provider: VisibilityLocationProvider) { + delegate = provider + } + + override fun isInVisibleLocation(entry: NotificationEntry): Boolean = + requireNotNull(this.delegate) { "delegate not initialized" }.isInVisibleLocation(entry) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index df2de560b5b9..a7b7a239f4c2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -37,6 +37,7 @@ import com.android.systemui.shade.ShadeEventsModule; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.notification.AssistantFeedbackController; +import com.android.systemui.statusbar.notification.VisibilityLocationProvider; import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl; import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore; import com.android.systemui.statusbar.notification.collection.NotifLiveDataStoreImpl; @@ -51,6 +52,7 @@ import com.android.systemui.statusbar.notification.collection.inflation.OnUserIn import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.collection.provider.NotificationVisibilityProviderImpl; +import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; @@ -151,6 +153,11 @@ public interface NotificationsModule { @Binds NotifGutsViewManager bindNotifGutsViewManager(NotificationGutsManager notificationGutsManager); + /** Provides an instance of {@link VisibilityLocationProvider} */ + @Binds + VisibilityLocationProvider bindVisibilityLocationProvider( + VisibilityLocationProviderDelegator visibilityLocationProviderDelegator); + /** Provides an instance of {@link NotificationLogger} */ @SysUISingleton @Provides diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index e13378269ba6..0240bbcf1e19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -89,6 +89,7 @@ import com.android.systemui.statusbar.notification.collection.PipelineDumpable; import com.android.systemui.statusbar.notification.collection.PipelineDumper; import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; +import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.NotifStackController; import com.android.systemui.statusbar.notification.collection.render.NotifStats; @@ -157,6 +158,7 @@ public class NotificationStackScrollLayoutController { private final NotifCollection mNotifCollection; private final UiEventLogger mUiEventLogger; private final NotificationRemoteInputManager mRemoteInputManager; + private final VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; private final ShadeController mShadeController; private final KeyguardMediaController mKeyguardMediaController; private final SysuiStatusBarStateController mStatusBarStateController; @@ -638,6 +640,7 @@ public class NotificationStackScrollLayoutController { ShadeTransitionController shadeTransitionController, UiEventLogger uiEventLogger, NotificationRemoteInputManager remoteInputManager, + VisibilityLocationProviderDelegator visibilityLocationProviderDelegator, ShadeController shadeController, InteractionJankMonitor jankMonitor, StackStateLogger stackLogger, @@ -679,6 +682,7 @@ public class NotificationStackScrollLayoutController { mNotifCollection = notifCollection; mUiEventLogger = uiEventLogger; mRemoteInputManager = remoteInputManager; + mVisibilityLocationProviderDelegator = visibilityLocationProviderDelegator; mShadeController = shadeController; mFeatureFlags = featureFlags; mNotificationTargetsHelper = notificationTargetsHelper; @@ -750,6 +754,8 @@ public class NotificationStackScrollLayoutController { mNotificationRoundnessManager.setOnRoundingChangedCallback(mView::invalidate); mView.addOnExpandedHeightChangedListener(mNotificationRoundnessManager::setExpanded); + mVisibilityLocationProviderDelegator.setDelegate(this::isInVisibleLocation); + mTunerService.addTunable( (key, newValue) -> { switch (key) { 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 eea1d9118fb7..62f57b8d4068 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 @@ -57,6 +57,7 @@ public class StackScrollAlgorithm { private float mGapHeight; private float mGapHeightOnLockscreen; private int mCollapsedSize; + private boolean mEnableNotificationClipping; private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); private boolean mIsExpanded; @@ -85,6 +86,7 @@ public class StackScrollAlgorithm { mPaddingBetweenElements = res.getDimensionPixelSize( R.dimen.notification_divider_height); mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height); + mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping); mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop); int statusBarHeight = SystemBarUtils.getStatusBarHeight(context); mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize( @@ -289,7 +291,7 @@ public class StackScrollAlgorithm { // The bottom of this view is peeking out from under the previous view. // Clip the part that is peeking out. float overlapAmount = newNotificationEnd - firstHeadsUpEnd; - state.clipBottomAmount = (int) overlapAmount; + state.clipBottomAmount = mEnableNotificationClipping ? (int) overlapAmount : 0; } else { state.clipBottomAmount = 0; } 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 1ab9be79d3e2..be0818339b74 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -54,7 +54,6 @@ import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.statusbar.GestureRecorder; import com.android.systemui.statusbar.LightRevealScrim; import com.android.systemui.statusbar.NotificationPresenter; -import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import java.io.PrintWriter; @@ -254,8 +253,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn boolean isWakeUpComingFromTouch(); - boolean isFalsingThresholdNeeded(); - void onKeyguardViewManagerStatesUpdated(); ViewGroup getNotificationScrollLayout(); @@ -413,12 +410,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void onClosingFinished(); - void onUnlockHintStarted(); - - void onHintFinished(); - - void onTrackingStopped(boolean expand); - // TODO: Figure out way to remove these. NavigationBarView getNavigationBarView(); @@ -500,8 +491,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn boolean isKeyguardSecure(); - NotificationGutsManager getGutsManager(); - void updateNotificationPanelTouchState(); void makeExpandedVisible(boolean force); 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 18a08f73e596..4562e6930f91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -279,6 +279,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // 1020-1040 reserved for BaseStatusBar /** + * TODO(b/249277686) delete this * The delay to reset the hint text when the hint animation is finished running. */ private static final int HINT_RESET_DELAY_MS = 1200; @@ -1784,11 +1785,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return mWakeUpComingFromTouch; } - @Override - public boolean isFalsingThresholdNeeded() { - return true; - } - /** * To be called when there's a state change in StatusBarKeyguardViewManager. */ @@ -3392,22 +3388,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } } - @Override - public void onUnlockHintStarted() { - mFalsingCollector.onUnlockHintStarted(); - mKeyguardIndicationController.showActionToUnlock(); - } - - @Override - public void onHintFinished() { - // Delay the reset a bit so the user can read the text. - mKeyguardIndicationController.hideTransientIndicationDelayed(HINT_RESET_DELAY_MS); - } - - @Override - public void onTrackingStopped(boolean expand) { - } - // TODO: Figure out way to remove these. @Override public NavigationBarView getNavigationBarView() { @@ -4138,11 +4118,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // End Extra BaseStatusBarMethods. - @Override - public NotificationGutsManager getGutsManager() { - return mGutsManager; - } - boolean isTransientShown() { return mTransientShown; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt index 34cd1ceeffc7..7dcdc0bdb383 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt @@ -33,7 +33,7 @@ class ConfigurationControllerImpl @Inject constructor(context: Context) : Config private val lastConfig = Configuration() private var density: Int = 0 private var smallestScreenWidth: Int = 0 - private var maxBounds: Rect? = null + private var maxBounds = Rect() private var fontScale: Float = 0.toFloat() private val inCarMode: Boolean private var uiMode: Int = 0 @@ -47,6 +47,7 @@ class ConfigurationControllerImpl @Inject constructor(context: Context) : Config fontScale = currentConfig.fontScale density = currentConfig.densityDpi smallestScreenWidth = currentConfig.smallestScreenWidthDp + maxBounds.set(currentConfig.windowConfiguration.maxBounds) inCarMode = currentConfig.uiMode and Configuration.UI_MODE_TYPE_MASK == Configuration.UI_MODE_TYPE_CAR uiMode = currentConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK @@ -92,7 +93,11 @@ class ConfigurationControllerImpl @Inject constructor(context: Context) : Config val maxBounds = newConfig.windowConfiguration.maxBounds if (maxBounds != this.maxBounds) { - this.maxBounds = maxBounds + // Update our internal rect to have the same bounds, instead of using + // `this.maxBounds = maxBounds` directly. Setting it directly means that `maxBounds` + // would be a direct reference to windowConfiguration.maxBounds, so the if statement + // above would always fail. See b/245799099 for more information. + this.maxBounds.set(maxBounds) listeners.filterForEach({ this.listeners.contains(it) }) { it.onMaxBoundsChanged() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 7a49a495155b..13566ef8c630 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -48,6 +48,9 @@ import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; +import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import java.io.PrintWriter; import java.util.ArrayList; @@ -70,7 +73,7 @@ public class KeyguardStatusBarView extends RelativeLayout { private ImageView mMultiUserAvatar; private BatteryMeterView mBatteryView; private StatusIconContainer mStatusIconContainer; - private ViewGroup mUserSwitcherContainer; + private StatusBarUserSwitcherContainer mUserSwitcherContainer; private boolean mKeyguardUserSwitcherEnabled; private boolean mKeyguardUserAvatarEnabled; @@ -121,8 +124,12 @@ public class KeyguardStatusBarView extends RelativeLayout { loadDimens(); } - public ViewGroup getUserSwitcherContainer() { - return mUserSwitcherContainer; + /** + * Should only be called from {@link KeyguardStatusBarViewController} + * @param viewModel view model for the status bar user chip + */ + void init(StatusBarUserChipViewModel viewModel) { + StatusBarUserChipViewBinder.bind(mUserSwitcherContainer, viewModel); } @Override @@ -304,10 +311,7 @@ public class KeyguardStatusBarView extends RelativeLayout { lp = (LayoutParams) mStatusIconArea.getLayoutParams(); lp.removeRule(RelativeLayout.RIGHT_OF); lp.width = LayoutParams.WRAP_CONTENT; - - LinearLayout.LayoutParams llp = - (LinearLayout.LayoutParams) mSystemIconsContainer.getLayoutParams(); - llp.setMarginStart(getResources().getDimensionPixelSize( + lp.setMarginStart(getResources().getDimensionPixelSize( R.dimen.system_icons_super_container_margin_start)); return true; } @@ -339,10 +343,7 @@ public class KeyguardStatusBarView extends RelativeLayout { lp = (LayoutParams) mStatusIconArea.getLayoutParams(); lp.addRule(RelativeLayout.RIGHT_OF, R.id.cutout_space_view); lp.width = LayoutParams.MATCH_PARENT; - - LinearLayout.LayoutParams llp = - (LinearLayout.LayoutParams) mSystemIconsContainer.getLayoutParams(); - llp.setMarginStart(0); + lp.setMarginStart(0); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 14cebf4b9f4b..d4dc1dc197a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -59,13 +59,11 @@ import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.statusbar.phone.fragment.StatusBarIconBlocklistKt; import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventAnimator; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserInfoTracker; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherFeatureController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import com.android.systemui.util.ViewController; import com.android.systemui.util.settings.SecureSettings; @@ -110,9 +108,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat private final SysuiStatusBarStateController mStatusBarStateController; private final StatusBarContentInsetsProvider mInsetsProvider; private final UserManager mUserManager; - private final StatusBarUserSwitcherFeatureController mFeatureController; - private final StatusBarUserSwitcherController mUserSwitcherController; - private final StatusBarUserInfoTracker mStatusBarUserInfoTracker; + private final StatusBarUserChipViewModel mStatusBarUserChipViewModel; private final SecureSettings mSecureSettings; private final CommandQueue mCommandQueue; private final Executor mMainExecutor; @@ -276,9 +272,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat SysuiStatusBarStateController statusBarStateController, StatusBarContentInsetsProvider statusBarContentInsetsProvider, UserManager userManager, - StatusBarUserSwitcherFeatureController featureController, - StatusBarUserSwitcherController userSwitcherController, - StatusBarUserInfoTracker statusBarUserInfoTracker, + StatusBarUserChipViewModel userChipViewModel, SecureSettings secureSettings, CommandQueue commandQueue, @Main Executor mainExecutor, @@ -301,9 +295,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mStatusBarStateController = statusBarStateController; mInsetsProvider = statusBarContentInsetsProvider; mUserManager = userManager; - mFeatureController = featureController; - mUserSwitcherController = userSwitcherController; - mStatusBarUserInfoTracker = statusBarUserInfoTracker; + mStatusBarUserChipViewModel = userChipViewModel; mSecureSettings = secureSettings; mCommandQueue = commandQueue; mMainExecutor = mainExecutor; @@ -328,8 +320,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat R.dimen.header_notifications_collide_distance); mView.setKeyguardUserAvatarEnabled( - !mFeatureController.isStatusBarUserSwitcherFeatureEnabled()); - mFeatureController.addCallback(enabled -> mView.setKeyguardUserAvatarEnabled(!enabled)); + !mStatusBarUserChipViewModel.getChipEnabled()); mSystemEventAnimator = new StatusBarSystemEventAnimator(mView, r); mDisableStateTracker = new DisableStateTracker( @@ -344,11 +335,11 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat super.onInit(); mCarrierTextController.init(); mBatteryMeterViewController.init(); - mUserSwitcherController.init(); } @Override protected void onViewAttached() { + mView.init(mStatusBarUserChipViewModel); mConfigurationController.addCallback(mConfigurationListener); mAnimationScheduler.addCallback(mAnimationCallback); mUserInfoController.addCallback(mOnUserInfoChangedListener); @@ -394,9 +385,6 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Sets whether user switcher is enabled. */ public void setKeyguardUserSwitcherEnabled(boolean enabled) { mView.setKeyguardUserSwitcherEnabled(enabled); - // We don't have a listener for when the user switcher setting changes, so this is - // where we re-check the state - mStatusBarUserInfoTracker.checkEnabled(); } /** Sets whether this controller should listen to battery updates. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index 7aeb08dd5ddb..28bc64de2cb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -38,6 +38,9 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; +import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import com.android.systemui.util.leak.RotationUtils; import java.util.Objects; @@ -73,6 +76,11 @@ public class PhoneStatusBarView extends FrameLayout { mTouchEventHandler = handler; } + void init(StatusBarUserChipViewModel viewModel) { + StatusBarUserSwitcherContainer container = findViewById(R.id.user_switcher_container); + StatusBarUserChipViewBinder.bind(container, viewModel); + } + @Override public void onFinishInflate() { super.onFinishInflate(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index f9c4c8f3b4fe..a6c2b2c2771c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -23,11 +23,11 @@ import android.view.ViewGroup import android.view.ViewTreeObserver import com.android.systemui.R import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.unfold.SysUIUnfoldComponent import com.android.systemui.unfold.UNFOLD_STATUS_BAR import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel import com.android.systemui.util.ViewController import com.android.systemui.util.kotlin.getOrNull import com.android.systemui.util.view.ViewUtil @@ -40,7 +40,7 @@ class PhoneStatusBarViewController private constructor( view: PhoneStatusBarView, @Named(UNFOLD_STATUS_BAR) private val progressProvider: ScopedUnfoldTransitionProgressProvider?, private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?, - private val userSwitcherController: StatusBarUserSwitcherController, + private val userChipViewModel: StatusBarUserChipViewModel, private val viewUtil: ViewUtil, touchEventHandler: PhoneStatusBarView.TouchEventHandler, private val configurationController: ConfigurationController @@ -91,10 +91,10 @@ class PhoneStatusBarViewController private constructor( init { mView.setTouchEventHandler(touchEventHandler) + mView.init(userChipViewModel) } override fun onInit() { - userSwitcherController.init() } fun setImportantForAccessibility(mode: Int) { @@ -156,9 +156,9 @@ class PhoneStatusBarViewController private constructor( private val unfoldComponent: Optional<SysUIUnfoldComponent>, @Named(UNFOLD_STATUS_BAR) private val progressProvider: Optional<ScopedUnfoldTransitionProgressProvider>, - private val userSwitcherController: StatusBarUserSwitcherController, + private val userChipViewModel: StatusBarUserChipViewModel, private val viewUtil: ViewUtil, - private val configurationController: ConfigurationController + private val configurationController: ConfigurationController, ) { fun create( view: PhoneStatusBarView, @@ -168,7 +168,7 @@ class PhoneStatusBarViewController private constructor( view, progressProvider.getOrNull(), unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController(), - userSwitcherController, + userChipViewModel, viewUtil, touchEventHandler, configurationController diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java index 41f1f9589ce4..efec27099dcd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java @@ -29,8 +29,6 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarViewController; import com.android.systemui.statusbar.phone.StatusBarBoundsProvider; import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherControllerImpl; import com.android.systemui.statusbar.policy.Clock; import com.android.systemui.statusbar.window.StatusBarWindowController; @@ -39,7 +37,6 @@ import java.util.Set; import javax.inject.Named; -import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.Multibinds; @@ -126,12 +123,6 @@ public interface StatusBarFragmentModule { } /** */ - @Binds - @StatusBarFragmentScope - StatusBarUserSwitcherController bindStatusBarUserSwitcherController( - StatusBarUserSwitcherControllerImpl controller); - - /** */ @Provides @StatusBarFragmentScope static PhoneStatusBarViewController providePhoneStatusBarViewController( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt deleted file mode 100644 index f6b8cb0782cd..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt +++ /dev/null @@ -1,134 +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.statusbar.phone.userswitcher - -import android.graphics.drawable.Drawable -import android.os.UserManager -import com.android.systemui.Dumpable -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.policy.CallbackController -import com.android.systemui.statusbar.policy.UserInfoController -import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener -import java.io.PrintWriter -import java.util.concurrent.Executor -import javax.inject.Inject - -/** - * Since every user switcher chip will user the exact same information and logic on whether or not - * to show, and what data to show, it makes sense to create a single tracker here - */ -@SysUISingleton -class StatusBarUserInfoTracker @Inject constructor( - private val userInfoController: UserInfoController, - private val userManager: UserManager, - private val dumpManager: DumpManager, - @Main private val mainExecutor: Executor, - @Background private val backgroundExecutor: Executor -) : CallbackController<CurrentUserChipInfoUpdatedListener>, Dumpable { - var currentUserName: String? = null - private set - var currentUserAvatar: Drawable? = null - private set - var userSwitcherEnabled = false - private set - private var listening = false - - private val listeners = mutableListOf<CurrentUserChipInfoUpdatedListener>() - - private val userInfoChangedListener = OnUserInfoChangedListener { name, picture, _ -> - currentUserAvatar = picture - currentUserName = name - notifyListenersUserInfoChanged() - } - - init { - dumpManager.registerDumpable(TAG, this) - } - - override fun addCallback(listener: CurrentUserChipInfoUpdatedListener) { - if (listeners.isEmpty()) { - startListening() - } - - if (!listeners.contains(listener)) { - listeners.add(listener) - } - } - - override fun removeCallback(listener: CurrentUserChipInfoUpdatedListener) { - listeners.remove(listener) - - if (listeners.isEmpty()) { - stopListening() - } - } - - private fun notifyListenersUserInfoChanged() { - listeners.forEach { - it.onCurrentUserChipInfoUpdated() - } - } - - private fun notifyListenersSettingChanged() { - listeners.forEach { - it.onStatusBarUserSwitcherSettingChanged(userSwitcherEnabled) - } - } - - private fun startListening() { - listening = true - userInfoController.addCallback(userInfoChangedListener) - } - - private fun stopListening() { - listening = false - userInfoController.removeCallback(userInfoChangedListener) - } - - /** - * Force a check to [UserManager.isUserSwitcherEnabled], and update listeners if the value has - * changed - */ - fun checkEnabled() { - backgroundExecutor.execute { - // Check on a background thread to avoid main thread Binder calls - val wasEnabled = userSwitcherEnabled - userSwitcherEnabled = userManager.isUserSwitcherEnabled - - if (wasEnabled != userSwitcherEnabled) { - mainExecutor.execute { - notifyListenersSettingChanged() - } - } - } - } - - override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.println(" userSwitcherEnabled=$userSwitcherEnabled") - pw.println(" listening=$listening") - } -} - -interface CurrentUserChipInfoUpdatedListener { - fun onCurrentUserChipInfoUpdated() - fun onStatusBarUserSwitcherSettingChanged(enabled: Boolean) {} -} - -private const val TAG = "StatusBarUserInfoTracker" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt deleted file mode 100644 index e498ae451400..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt +++ /dev/null @@ -1,112 +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.statusbar.phone.userswitcher - -import android.content.Intent -import android.os.UserHandle -import android.view.View -import com.android.systemui.animation.Expandable -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager - -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.user.UserSwitcherActivity -import com.android.systemui.util.ViewController - -import javax.inject.Inject - -/** - * ViewController for [StatusBarUserSwitcherContainer] - */ -class StatusBarUserSwitcherControllerImpl @Inject constructor( - view: StatusBarUserSwitcherContainer, - private val tracker: StatusBarUserInfoTracker, - private val featureController: StatusBarUserSwitcherFeatureController, - private val userSwitcherDialogController: UserSwitchDialogController, - private val featureFlags: FeatureFlags, - private val activityStarter: ActivityStarter, - private val falsingManager: FalsingManager -) : ViewController<StatusBarUserSwitcherContainer>(view), - StatusBarUserSwitcherController { - private val listener = object : CurrentUserChipInfoUpdatedListener { - override fun onCurrentUserChipInfoUpdated() { - updateChip() - } - - override fun onStatusBarUserSwitcherSettingChanged(enabled: Boolean) { - updateEnabled() - } - } - - private val featureFlagListener = object : OnUserSwitcherPreferenceChangeListener { - override fun onUserSwitcherPreferenceChange(enabled: Boolean) { - updateEnabled() - } - } - - public override fun onViewAttached() { - tracker.addCallback(listener) - featureController.addCallback(featureFlagListener) - mView.setOnClickListener { view: View -> - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { - return@setOnClickListener - } - - if (featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - val intent = Intent(context, UserSwitcherActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - - activityStarter.startActivity(intent, true /* dismissShade */, - null /* ActivityLaunchAnimator.Controller */, - true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM) - } else { - userSwitcherDialogController.showDialog(view.context, Expandable.fromView(view)) - } - } - - updateEnabled() - } - - override fun onViewDetached() { - tracker.removeCallback(listener) - featureController.removeCallback(featureFlagListener) - mView.setOnClickListener(null) - } - - private fun updateChip() { - mView.text.text = tracker.currentUserName - mView.avatar.setImageDrawable(tracker.currentUserAvatar) - } - - private fun updateEnabled() { - if (featureController.isStatusBarUserSwitcherFeatureEnabled() && - tracker.userSwitcherEnabled) { - mView.visibility = View.VISIBLE - updateChip() - } else { - mView.visibility = View.GONE - } - } -} - -interface StatusBarUserSwitcherController { - fun init() -} - -private const val TAG = "SbUserSwitcherController" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt deleted file mode 100644 index 7bae9ff72760..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt +++ /dev/null @@ -1,63 +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.statusbar.phone.userswitcher - -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.statusbar.policy.CallbackController - -import javax.inject.Inject - -@SysUISingleton -class StatusBarUserSwitcherFeatureController @Inject constructor( - private val flags: FeatureFlags -) : CallbackController<OnUserSwitcherPreferenceChangeListener> { - private val listeners = mutableListOf<OnUserSwitcherPreferenceChangeListener>() - - init { - flags.addListener(Flags.STATUS_BAR_USER_SWITCHER) { - it.requestNoRestart() - notifyListeners() - } - } - - fun isStatusBarUserSwitcherFeatureEnabled(): Boolean { - return flags.isEnabled(Flags.STATUS_BAR_USER_SWITCHER) - } - - override fun addCallback(listener: OnUserSwitcherPreferenceChangeListener) { - if (!listeners.contains(listener)) { - listeners.add(listener) - } - } - - override fun removeCallback(listener: OnUserSwitcherPreferenceChangeListener) { - listeners.remove(listener) - } - - private fun notifyListeners() { - val enabled = flags.isEnabled(Flags.STATUS_BAR_USER_SWITCHER) - listeners.forEach { - it.onUserSwitcherPreferenceChange(enabled) - } - } -} - -interface OnUserSwitcherPreferenceChangeListener { - fun onUserSwitcherPreferenceChange(enabled: Boolean) -} 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 3c2ac7b7a124..2ee52325ca4a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -156,6 +156,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(" mPowerSave="); pw.println(mPowerSave); pw.print(" mStateUnknown="); pw.println(mStateUnknown); } 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 38b3769c1071..acdf0d2bc32b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -146,7 +146,13 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa } private String getDeviceString(CachedBluetoothDevice device) { - return device.getName() + " " + device.getBondState() + " " + device.isConnected(); + return device.getName() + + " bondState=" + device.getBondState() + + " connected=" + device.isConnected() + + " active[A2DP]=" + device.isActiveDevice(BluetoothProfile.A2DP) + + " active[HEADSET]=" + device.isActiveDevice(BluetoothProfile.HEADSET) + + " active[HEARING_AID]=" + device.isActiveDevice(BluetoothProfile.HEARING_AID) + + " active[LE_AUDIO]=" + device.isActiveDevice(BluetoothProfile.LE_AUDIO); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt index ae73df201f8d..773ac55130d4 100644 --- a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt @@ -27,6 +27,6 @@ data class RippleAnimationConfig( companion object { const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f const val RIPPLE_DEFAULT_COLOR: Int = 0xffffffff.toInt() - const val RIPPLE_DEFAULT_ALPHA: Int = 45 // full opacity is 255. + const val RIPPLE_DEFAULT_ALPHA: Int = 115 // full opacity is 255. } } diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt index 299469494295..2ad824348794 100644 --- a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt @@ -111,7 +111,7 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a /** * Set the color to be used for the ripple. * - * The alpha value of the color will be applied to the ripple. The alpha range is [0-100]. + * The alpha value of the color will be applied to the ripple. The alpha range is [0-255]. */ fun setColor(color: Int, alpha: Int = RippleAnimationConfig.RIPPLE_DEFAULT_ALPHA) { rippleShader.color = ColorUtils.setAlphaComponent(color, alpha) 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 44e5ce865241..fb17b693e17e 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -174,7 +174,7 @@ open class ChipbarCoordinator @Inject constructor( chipInnerView, ViewHierarchyAnimator.Hotspot.TOP, Interpolators.EMPHASIZED_DECELERATE, - duration = ANIMATION_DURATION, + duration = ANIMATION_IN_DURATION, includeMargins = true, includeFadeIn = true, // We can only request focus once the animation finishes. @@ -187,7 +187,7 @@ open class ChipbarCoordinator @Inject constructor( view.requireViewById<ViewGroup>(R.id.chipbar_inner), ViewHierarchyAnimator.Hotspot.TOP, Interpolators.EMPHASIZED_ACCELERATE, - ANIMATION_DURATION, + ANIMATION_OUT_DURATION, includeMargins = true, onAnimationEnd, ) @@ -208,4 +208,5 @@ open class ChipbarCoordinator @Inject constructor( } } -private const val ANIMATION_DURATION = 500L +private const val ANIMATION_IN_DURATION = 500L +private const val ANIMATION_OUT_DURATION = 250L diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index ed53de7dbee7..4c9b8e4639ca 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -23,6 +23,7 @@ import android.os.UserHandle import android.os.UserManager import android.provider.Settings import androidx.annotation.VisibleForTesting +import com.android.systemui.R import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -79,6 +80,9 @@ interface UserRepository { /** Whether we've scheduled the creation of a guest user. */ val isGuestUserCreationScheduled: AtomicBoolean + /** Whether to enable the status bar user chip (which launches the user switcher) */ + val isStatusBarUserChipEnabled: Boolean + /** The user of the secondary service. */ var secondaryUserId: Int @@ -127,6 +131,9 @@ constructor( override val isGuestUserCreationScheduled = AtomicBoolean() + override val isStatusBarUserChipEnabled: Boolean = + appContext.resources.getBoolean(R.bool.flag_user_switcher_chip) + override var secondaryUserId: Int = UserHandle.USER_NULL override var isRefreshUsersPaused: Boolean = false 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 6b81bf2cfb08..83f0711caa38 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 @@ -34,15 +34,20 @@ import android.util.Log import com.android.internal.util.UserIcons import com.android.systemui.R import com.android.systemui.SystemUISecondaryUserService +import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.Text 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.Flags 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.UserSwitcherActivity +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.domain.model.ShowDialogRequestModel @@ -61,6 +66,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -79,6 +85,7 @@ constructor( private val repository: UserRepository, private val activityStarter: ActivityStarter, private val keyguardInteractor: KeyguardInteractor, + private val featureFlags: FeatureFlags, private val manager: UserManager, @Application private val applicationScope: CoroutineScope, telephonyInteractor: TelephonyInteractor, @@ -108,12 +115,16 @@ constructor( private val callbackMutex = Mutex() private val callbacks = mutableSetOf<UserCallback>() + private val userInfos = + combine(repository.userSwitcherSettings, repository.userInfos) { settings, userInfos -> + userInfos.filter { !it.isGuest || canCreateGuestUser(settings) } + } /** List of current on-device users to select from. */ val users: Flow<List<UserModel>> get() = combine( - repository.userInfos, + userInfos, repository.selectedUserInfo, repository.userSwitcherSettings, ) { userInfos, selectedUserInfo, settings -> @@ -147,22 +158,13 @@ constructor( get() = combine( repository.selectedUserInfo, - repository.userInfos, + userInfos, repository.userSwitcherSettings, keyguardInteractor.isKeyguardShowing, ) { _, userInfos, settings, isDeviceLocked -> buildList { val hasGuestUser = userInfos.any { it.isGuest } - if ( - !hasGuestUser && - (guestUserInteractor.isGuestUserAutoCreated || - UserActionsUtil.canCreateGuest( - manager, - repository, - settings.isUserSwitcherEnabled, - settings.isAddUsersFromLockscreen, - )) - ) { + if (!hasGuestUser && canCreateGuestUser(settings)) { add(UserActionModel.ENTER_GUEST_MODE) } @@ -211,7 +213,7 @@ constructor( val userRecords: StateFlow<ArrayList<UserRecord>> = combine( - repository.userInfos, + userInfos, repository.selectedUserInfo, actions, repository.userSwitcherSettings, @@ -259,6 +261,9 @@ constructor( /** Whether the guest user is currently being reset. */ val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting + /** Whether to enable the user chip in the status bar */ + val isStatusBarUserChipEnabled: Boolean = repository.isStatusBarUserChipEnabled + private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null) val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow() @@ -471,6 +476,26 @@ constructor( } } + fun showUserSwitcher(context: Context, expandable: Expandable) { + if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { + showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog) + return + } + + val intent = + Intent(context, UserSwitcherActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + } + + activityStarter.startActivity( + intent, + true /* dismissShade */, + expandable.activityLaunchController(), + true /* showOverlockscreenwhenlocked */, + UserHandle.SYSTEM, + ) + } + private fun showDialog(request: ShowDialogRequestModel) { _dialogShowRequests.value = request } @@ -687,6 +712,16 @@ constructor( ) } + private fun canCreateGuestUser(settings: UserSwitcherSettingsModel): Boolean { + return guestUserInteractor.isGuestUserAutoCreated || + UserActionsUtil.canCreateGuest( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + } + companion object { private const val TAG = "UserInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt index 177356e6b573..85c29647719b 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -43,4 +43,7 @@ sealed class ShowDialogRequestModel( val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit, override val dialogShower: UserSwitchDialogController.DialogShower?, ) : ShowDialogRequestModel(dialogShower) + + /** Show the user switcher dialog */ + object ShowUserSwitcherDialog : ShowDialogRequestModel() } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt new file mode 100644 index 000000000000..8e40f68e27e9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt @@ -0,0 +1,65 @@ +/* + * 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.user.ui.binder + +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.animation.Expandable +import com.android.systemui.common.ui.binder.TextViewBinder +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@OptIn(InternalCoroutinesApi::class) +object StatusBarUserChipViewBinder { + /** Binds the status bar user chip view model to the given view */ + @JvmStatic + fun bind( + view: StatusBarUserSwitcherContainer, + viewModel: StatusBarUserChipViewModel, + ) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.isChipVisible.collect { isVisible -> view.isVisible = isVisible } + } + + launch { + viewModel.userName.collect { name -> TextViewBinder.bind(view.text, name) } + } + + launch { + viewModel.userAvatar.collect { avatar -> view.avatar.setImageDrawable(avatar) } + } + + bindButton(view, viewModel) + } + } + } + + private fun bindButton( + view: StatusBarUserSwitcherContainer, + viewModel: StatusBarUserChipViewModel, + ) { + view.setOnClickListener { viewModel.onClick(Expandable.fromView(view)) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt new file mode 100644 index 000000000000..ed2589889435 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt @@ -0,0 +1,68 @@ +package com.android.systemui.user.ui.dialog + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.view.LayoutInflater +import com.android.internal.logging.UiEventLogger +import com.android.systemui.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.QSUserSwitcherEvent +import com.android.systemui.qs.tiles.UserDetailView +import com.android.systemui.statusbar.phone.SystemUIDialog + +/** + * Extracted from the old UserSwitchDialogController. This is the dialog version of the full-screen + * user switcher. See config_enableFullscreenUserSwitcher + */ +class UserSwitchDialog( + context: Context, + adapter: UserDetailView.Adapter, + uiEventLogger: UiEventLogger, + falsingManager: FalsingManager, + activityStarter: ActivityStarter, + dialogLaunchAnimator: DialogLaunchAnimator, +) : SystemUIDialog(context) { + init { + setShowForAllUsers(true) + setCanceledOnTouchOutside(true) + setTitle(R.string.qs_user_switch_dialog_title) + setPositiveButton(R.string.quick_settings_done) { _, _ -> + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_CLOSE) + } + setNeutralButton( + R.string.quick_settings_more_user_settings, + { _, _ -> + if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_MORE_SETTINGS) + val controller = + dialogLaunchAnimator.createActivityLaunchController( + getButton(BUTTON_NEUTRAL) + ) + + if (controller == null) { + dismiss() + } + + activityStarter.postStartActivityDismissingKeyguard( + USER_SETTINGS_INTENT, + 0, + controller + ) + } + }, + false /* dismissOnClick */ + ) + val gridFrame = + LayoutInflater.from(this.context).inflate(R.layout.qs_user_dialog_content, null) + setView(gridFrame) + + adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid)) + } + + companion object { + private val USER_SETTINGS_INTENT = Intent(Settings.ACTION_USER_SETTINGS) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index 58a4473186b3..41410542204c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -20,6 +20,7 @@ package com.android.systemui.user.ui.dialog import android.app.Dialog import android.content.Context import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.logging.UiEventLogger import com.android.settingslib.users.UserCreatingDialog import com.android.systemui.CoreStartable import com.android.systemui.animation.DialogCuj @@ -27,11 +28,14 @@ import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.tiles.UserDetailView import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel import dagger.Lazy import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -47,6 +51,9 @@ constructor( private val broadcastSender: Lazy<BroadcastSender>, private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>, private val interactor: Lazy<UserInteractor>, + private val userDetailAdapterProvider: Provider<UserDetailView.Adapter>, + private val eventLogger: Lazy<UiEventLogger>, + private val activityStarter: Lazy<ActivityStarter>, ) : CoreStartable { private var currentDialog: Dialog? = null @@ -108,6 +115,21 @@ constructor( INTERACTION_JANK_EXIT_GUEST_MODE_TAG, ), ) + is ShowDialogRequestModel.ShowUserSwitcherDialog -> + Pair( + UserSwitchDialog( + context = context.get(), + adapter = userDetailAdapterProvider.get(), + uiEventLogger = eventLogger.get(), + falsingManager = falsingManager.get(), + activityStarter = activityStarter.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), + ), + DialogCuj( + InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, + INTERACTION_JANK_EXIT_GUEST_MODE_TAG, + ), + ) } currentDialog = dialog diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt new file mode 100644 index 000000000000..3300e8e5b2a5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt @@ -0,0 +1,59 @@ +/* + * 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.user.ui.viewmodel + +import android.content.Context +import android.graphics.drawable.Drawable +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Text +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.user.domain.interactor.UserInteractor +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest + +@OptIn(ExperimentalCoroutinesApi::class) +class StatusBarUserChipViewModel +@Inject +constructor( + @Application private val context: Context, + interactor: UserInteractor, +) { + /** Whether the status bar chip ui should be available */ + val chipEnabled: Boolean = interactor.isStatusBarUserChipEnabled + + /** Whether or not the chip should be showing, based on the number of users */ + val isChipVisible: Flow<Boolean> = + if (!chipEnabled) { + flowOf(false) + } else { + interactor.users.mapLatest { users -> users.size > 1 } + } + + /** The display name of the current user */ + val userName: Flow<Text> = interactor.selectedUser.mapLatest { userModel -> userModel.name } + + /** Avatar for the current user */ + val userAvatar: Flow<Drawable> = + interactor.selectedUser.mapLatest { userModel -> userModel.image } + + /** Action to execute on click. Should launch the user switcher */ + val onClick: (Expandable) -> Unit = { interactor.showUserSwitcher(context, it) } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java index 6b5556b3ea91..0f3eddf2eb7c 100644 --- a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java +++ b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java @@ -19,7 +19,6 @@ package com.android.systemui.util; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.Context; import android.provider.DeviceConfig; import android.provider.Settings; @@ -53,8 +52,8 @@ public class DeviceConfigProxy { /** * Wrapped version of {@link DeviceConfig#enforceReadPermission}. */ - public void enforceReadPermission(Context context, String namespace) { - DeviceConfig.enforceReadPermission(context, namespace); + public void enforceReadPermission(String namespace) { + DeviceConfig.enforceReadPermission(namespace); } /** diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt index 131cf7d33e3a..88396628017b 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt @@ -84,10 +84,9 @@ private fun faceModel(user: Int) = KeyguardFaceListenModel( userId = user, listening = false, authInterruptActive = false, - becauseCannotSkipBouncer = false, biometricSettingEnabledForUser = false, bouncerFullyShown = false, - faceAuthenticated = false, + faceAndFpNotAuthenticated = false, faceDisabled = false, faceLockedOut = false, fpLockedOut = false, @@ -101,4 +100,6 @@ private fun faceModel(user: Int) = KeyguardFaceListenModel( secureCameraLaunched = false, switchingUser = false, udfpsBouncerShowing = false, + udfpsFingerDown = false, + userNotTrustedOrDetectionIsNeeded = false ) diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 5514fd0f9311..7231b3427a71 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -29,6 +29,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOM import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT; import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED; import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; +import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser; import static com.google.common.truth.Truth.assertThat; @@ -36,6 +37,7 @@ 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.anyObject; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; @@ -89,6 +91,7 @@ import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.service.dreams.IDreamManager; +import android.service.trust.TrustAgentService; import android.telephony.ServiceState; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -353,7 +356,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @After public void tearDown() { - mMockitoSession.finishMocking(); + if (mMockitoSession != null) { + mMockitoSession.finishMocking(); + } cleanupKeyguardUpdateMonitor(); } @@ -1316,9 +1321,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { Arrays.asList("Unlocked by wearable")); // THEN the showTrustGrantedMessage should be called with the first message - verify(mTestCallback).onTrustGrantedWithFlags( - eq(0), - eq(KeyguardUpdateMonitor.getCurrentUser()), + verify(mTestCallback).onTrustGrantedForCurrentUser( + anyBoolean(), + eq(new TrustGrantFlags(0)), eq("Unlocked by wearable")); } @@ -1376,6 +1381,29 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testShouldListenForFace_whenFpIsAlreadyAuthenticated_returnsFalse() + throws RemoteException { + // Face auth should run when the following is true. + bouncerFullyVisibleAndNotGoingToSleep(); + keyguardNotGoingAway(); + currentUserIsPrimary(); + strongAuthNotRequired(); + biometricsEnabledForCurrentUser(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + userNotCurrentlySwitching(); + + mTestableLooper.processAllMessages(); + + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + + successfulFingerprintAuth(); + mTestableLooper.processAllMessages(); + + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); + } + + @Test public void testShouldListenForFace_whenUserIsNotPrimary_returnsFalse() throws RemoteException { cleanupKeyguardUpdateMonitor(); // This disables face auth @@ -1729,6 +1757,155 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); } + + @Test + public void testOnTrustGrantedForCurrentUser_dismissKeyguardRequested_deviceInteractive() { + // GIVEN device is interactive + deviceIsInteractive(); + + // GIVEN callback is registered + KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); + mKeyguardUpdateMonitor.registerCallback(callback); + + // WHEN onTrustChanged with TRUST_DISMISS_KEYGUARD flag + mKeyguardUpdateMonitor.onTrustChanged( + true /* enabled */, + getCurrentUser() /* userId */, + TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD /* flags */, + null /* trustGrantedMessages */); + + // THEN onTrustGrantedForCurrentUser callback called + verify(callback).onTrustGrantedForCurrentUser( + eq(true) /* dismissKeyguard */, + eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD)), + eq(null) /* message */ + ); + } + + @Test + public void testOnTrustGrantedForCurrentUser_dismissKeyguardRequested_doesNotDismiss() { + // GIVEN device is NOT interactive + + // GIVEN callback is registered + KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); + mKeyguardUpdateMonitor.registerCallback(callback); + + // WHEN onTrustChanged with TRUST_DISMISS_KEYGUARD flag + mKeyguardUpdateMonitor.onTrustChanged( + true /* enabled */, + getCurrentUser() /* userId */, + TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD /* flags */, + null /* trustGrantedMessages */); + + // THEN onTrustGrantedForCurrentUser callback called + verify(callback).onTrustGrantedForCurrentUser( + eq(false) /* dismissKeyguard */, + eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD)), + eq(null) /* message */ + ); + } + + @Test + public void testOnTrustGrantedForCurrentUser_dismissKeyguardRequested_temporaryAndRenewable() { + // GIVEN device is interactive + deviceIsInteractive(); + + // GIVEN callback is registered + KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); + mKeyguardUpdateMonitor.registerCallback(callback); + + // WHEN onTrustChanged for a different user + mKeyguardUpdateMonitor.onTrustChanged( + true /* enabled */, + 546 /* userId, not the current userId */, + 0 /* flags */, + null /* trustGrantedMessages */); + + // THEN onTrustGrantedForCurrentUser callback called + verify(callback, never()).onTrustGrantedForCurrentUser( + anyBoolean() /* dismissKeyguard */, + anyObject() /* flags */, + anyString() /* message */ + ); + } + + @Test + public void testOnTrustGranted_differentUser_noCallback() { + // GIVEN device is interactive + + // GIVEN callback is registered + KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); + mKeyguardUpdateMonitor.registerCallback(callback); + + // WHEN onTrustChanged with TRUST_DISMISS_KEYGUARD AND TRUST_TEMPORARY_AND_RENEWABLE + // flags (temporary & rewable is active unlock) + mKeyguardUpdateMonitor.onTrustChanged( + true /* enabled */, + getCurrentUser() /* userId */, + TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD + | TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE /* flags */, + null /* trustGrantedMessages */); + + // THEN onTrustGrantedForCurrentUser callback called + verify(callback).onTrustGrantedForCurrentUser( + eq(true) /* dismissKeyguard */, + eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD + | TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE)), + eq(null) /* message */ + ); + } + + @Test + public void testOnTrustGrantedForCurrentUser_bouncerShowing_initiatedByUser() { + // GIVEN device is interactive & bouncer is showing + deviceIsInteractive(); + bouncerFullyVisible(); + + // GIVEN callback is registered + KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); + mKeyguardUpdateMonitor.registerCallback(callback); + + // WHEN onTrustChanged with INITIATED_BY_USER flag + mKeyguardUpdateMonitor.onTrustChanged( + true /* enabled */, + getCurrentUser() /* userId, not the current userId */, + TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER /* flags */, + null /* trustGrantedMessages */); + + // THEN onTrustGrantedForCurrentUser callback called + verify(callback, never()).onTrustGrantedForCurrentUser( + eq(true) /* dismissKeyguard */, + eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER)), + anyString() /* message */ + ); + } + + @Test + public void testOnTrustGrantedForCurrentUser_bouncerShowing_temporaryRenewable() { + // GIVEN device is NOT interactive & bouncer is showing + bouncerFullyVisible(); + + // GIVEN callback is registered + KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); + mKeyguardUpdateMonitor.registerCallback(callback); + + // WHEN onTrustChanged with INITIATED_BY_USER flag + mKeyguardUpdateMonitor.onTrustChanged( + true /* enabled */, + getCurrentUser() /* userId, not the current userId */, + TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER + | TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE /* flags */, + null /* trustGrantedMessages */); + + // THEN onTrustGrantedForCurrentUser callback called + verify(callback, never()).onTrustGrantedForCurrentUser( + eq(true) /* dismissKeyguard */, + eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER + | TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE)), + anyString() /* message */ + ); + } + private void cleanupKeyguardUpdateMonitor() { if (mKeyguardUpdateMonitor != null) { mKeyguardUpdateMonitor.removeCallback(mTestCallback); @@ -1780,6 +1957,15 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .onAuthenticationAcquired(FINGERPRINT_ACQUIRED_START); } + private void successfulFingerprintAuth() { + mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback + .onAuthenticationSucceeded( + new FingerprintManager.AuthenticationResult(null, + null, + mCurrentUserId, + true)); + } + private void triggerSuccessfulFaceAuth() { mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN); verify(mFaceManager).authenticate(any(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java index 75629f451526..3c61382d9446 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java @@ -94,6 +94,11 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { mKeyguardStateControllerCallbackCaptor; protected KeyguardStateController.Callback mKeyguardStateControllerCallback; + private @Captor ArgumentCaptor<StatusBarKeyguardViewManager.KeyguardViewManagerCallback> + mKeyguardViewManagerCallbackArgumentCaptor; + protected StatusBarKeyguardViewManager.KeyguardViewManagerCallback mKeyguardViewManagerCallback; + + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -143,15 +148,22 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { } public UdfpsKeyguardViewController createUdfpsKeyguardViewController() { - return createUdfpsKeyguardViewController(false); + return createUdfpsKeyguardViewController(false, false); + } + + public void captureKeyGuardViewManagerCallback() { + verify(mStatusBarKeyguardViewManager).addCallback( + mKeyguardViewManagerCallbackArgumentCaptor.capture()); + mKeyguardViewManagerCallback = mKeyguardViewManagerCallbackArgumentCaptor.getValue(); } protected UdfpsKeyguardViewController createUdfpsKeyguardViewController( - boolean useModernBouncer) { + boolean useModernBouncer, boolean useExpandedOverlay) { mFeatureFlags.set(Flags.MODERN_BOUNCER, useModernBouncer); + mFeatureFlags.set(Flags.UDFPS_NEW_TOUCH_DETECTION, useExpandedOverlay); when(mStatusBarKeyguardViewManager.getPrimaryBouncer()).thenReturn( useModernBouncer ? null : mBouncer); - return new UdfpsKeyguardViewController( + UdfpsKeyguardViewController controller = new UdfpsKeyguardViewController( mView, mStatusBarStateController, mShadeExpansionStateManager, @@ -168,5 +180,6 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { mActivityLaunchAnimator, mFeatureFlags, mPrimaryBouncerInteractor); + return controller; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java index 16728b6f2ab9..babe5334e3eb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java @@ -18,6 +18,7 @@ package com.android.systemui.biometrics; import static org.junit.Assert.assertFalse; 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.Mockito.atLeast; @@ -30,6 +31,7 @@ import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; +import android.view.MotionEvent; import androidx.test.filters.SmallTest; @@ -52,7 +54,8 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController @Override public UdfpsKeyguardViewController createUdfpsKeyguardViewController() { - return createUdfpsKeyguardViewController(/* useModernBouncer */ false); + return createUdfpsKeyguardViewController(/* useModernBouncer */ false, + /* useExpandedOverlay */ false); } @Test @@ -422,4 +425,37 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController verify(mBouncer).addBouncerExpansionCallback(mBouncerExpansionCallbackCaptor.capture()); mBouncerExpansionCallback = mBouncerExpansionCallbackCaptor.getValue(); } + + @Test + // TODO(b/259264861): Tracking Bug + public void testUdfpsExpandedOverlayOn() { + // GIVEN view is attached and useExpandedOverlay is true + mController = createUdfpsKeyguardViewController(false, true); + mController.onViewAttached(); + captureKeyGuardViewManagerCallback(); + + // WHEN a touch is received + mKeyguardViewManagerCallback.onTouch( + MotionEvent.obtain(0, 0, 0, 0, 0, 0)); + + // THEN udfpsController onTouch is not called + assertTrue(mView.mUseExpandedOverlay); + verify(mUdfpsController, never()).onTouch(any()); + } + + @Test + // TODO(b/259264861): Tracking Bug + public void testUdfpsExpandedOverlayOff() { + // GIVEN view is attached and useExpandedOverlay is false + mController.onViewAttached(); + captureKeyGuardViewManagerCallback(); + + // WHEN a touch is received + mKeyguardViewManagerCallback.onTouch( + MotionEvent.obtain(0, 0, 0, 0, 0, 0)); + + // THEN udfpsController onTouch is called + assertFalse(mView.mUseExpandedOverlay); + verify(mUdfpsController).onTouch(any()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt index 68e744e53843..517e27a3ce2f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt @@ -72,7 +72,10 @@ class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControlle mock(KeyguardBypassController::class.java), mKeyguardUpdateMonitor ) - return createUdfpsKeyguardViewController(/* useModernBouncer */ true) + return createUdfpsKeyguardViewController( + /* useModernBouncer */ true, /* useExpandedOverlay */ + false + ) } /** After migration, replaces LockIconViewControllerTest version */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt index dedc7239bf85..98ff8d1d8845 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt @@ -46,6 +46,7 @@ import com.android.systemui.util.time.FakeSystemClock import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -480,6 +481,19 @@ class ControlsListingControllerImplTest : SysuiTestCase() { assertNull(controller.getCurrentServices()[0].panelActivity) } + @Test + fun testListingsNotModifiedByCallback() { + // This test checks that if the list passed to the callback is modified, it has no effect + // in the resulting services + val list = mutableListOf<ServiceInfo>() + serviceListingCallbackCaptor.value.onServicesReloaded(list) + + list.add(ServiceInfo(ComponentName("a", "b"))) + executor.runAllReady() + + assertTrue(controller.getCurrentServices().isEmpty()) + } + private fun ServiceInfo( componentName: ComponentName, panelActivityComponentName: ComponentName? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt new file mode 100644 index 000000000000..1e7b1f26ba8c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt @@ -0,0 +1,69 @@ +/* + * 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.systemui.flags + +import android.test.suitebuilder.annotation.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +/** + * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! + */ +@SmallTest +class FeatureFlagsDebugRestarterTest : SysuiTestCase() { + private lateinit var restarter: FeatureFlagsDebugRestarter + + @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle + @Mock private lateinit var systemExitRestarter: SystemExitRestarter + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + restarter = FeatureFlagsDebugRestarter(wakefulnessLifecycle, systemExitRestarter) + } + + @Test + fun testRestart_ImmediateWhenAsleep() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) + restarter.restart() + verify(systemExitRestarter).restart() + } + + @Test + fun testRestart_WaitsForSceenOff() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) + + restarter.restart() + verify(systemExitRestarter, never()).restart() + + val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java) + verify(wakefulnessLifecycle).addObserver(captor.capture()) + + captor.value.onFinishedGoingToSleep() + + verify(systemExitRestarter).restart() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt new file mode 100644 index 000000000000..68ca48dd327d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt @@ -0,0 +1,145 @@ +/* + * 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.systemui.flags + +import android.test.suitebuilder.annotation.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP +import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +/** + * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! + */ +@SmallTest +class FeatureFlagsReleaseRestarterTest : SysuiTestCase() { + private lateinit var restarter: FeatureFlagsReleaseRestarter + + @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle + @Mock private lateinit var batteryController: BatteryController + @Mock private lateinit var systemExitRestarter: SystemExitRestarter + private val executor = FakeExecutor(FakeSystemClock()) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + restarter = + FeatureFlagsReleaseRestarter( + wakefulnessLifecycle, + batteryController, + executor, + systemExitRestarter + ) + } + + @Test + fun testRestart_ScheduledWhenReady() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) + whenever(batteryController.isPluggedIn).thenReturn(true) + + assertThat(executor.numPending()).isEqualTo(0) + restarter.restart() + assertThat(executor.numPending()).isEqualTo(1) + } + + @Test + fun testRestart_RestartsWhenIdle() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) + whenever(batteryController.isPluggedIn).thenReturn(true) + + restarter.restart() + verify(systemExitRestarter, never()).restart() + executor.advanceClockToLast() + executor.runAllReady() + verify(systemExitRestarter).restart() + } + + @Test + fun testRestart_NotScheduledWhenAwake() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) + whenever(batteryController.isPluggedIn).thenReturn(true) + + assertThat(executor.numPending()).isEqualTo(0) + restarter.restart() + assertThat(executor.numPending()).isEqualTo(0) + } + + @Test + fun testRestart_NotScheduledWhenNotPluggedIn() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) + whenever(batteryController.isPluggedIn).thenReturn(false) + + assertThat(executor.numPending()).isEqualTo(0) + restarter.restart() + assertThat(executor.numPending()).isEqualTo(0) + } + + @Test + fun testRestart_NotDoubleSheduled() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) + whenever(batteryController.isPluggedIn).thenReturn(true) + + assertThat(executor.numPending()).isEqualTo(0) + restarter.restart() + restarter.restart() + assertThat(executor.numPending()).isEqualTo(1) + } + + @Test + fun testWakefulnessLifecycle_CanRestart() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) + whenever(batteryController.isPluggedIn).thenReturn(true) + assertThat(executor.numPending()).isEqualTo(0) + restarter.restart() + + val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java) + verify(wakefulnessLifecycle).addObserver(captor.capture()) + + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) + + captor.value.onFinishedGoingToSleep() + assertThat(executor.numPending()).isEqualTo(1) + } + + @Test + fun testBatteryController_CanRestart() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) + whenever(batteryController.isPluggedIn).thenReturn(false) + assertThat(executor.numPending()).isEqualTo(0) + restarter.restart() + + val captor = + ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback::class.java) + verify(batteryController).addCallback(captor.capture()) + + whenever(batteryController.isPluggedIn).thenReturn(true) + + captor.value.onBatteryLevelChanged(0, true, true) + assertThat(executor.numPending()).isEqualTo(1) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt new file mode 100644 index 000000000000..8395f02cbc41 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -0,0 +1,429 @@ +/* + * 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.keyguard + +import android.content.ContentValues +import android.content.pm.PackageManager +import android.content.pm.ProviderInfo +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.SystemUIAppComponentFactoryBase +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { + + @Mock private lateinit var lockPatternUtils: LockPatternUtils + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var activityStarter: ActivityStarter + + private lateinit var underTest: KeyguardQuickAffordanceProvider + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = KeyguardQuickAffordanceProvider() + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock<UserFileManager>().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + appContext = context, + scope = scope, + selectionManager = selectionManager, + configs = + setOf( + FakeKeyguardQuickAffordanceConfig( + key = AFFORDANCE_1, + pickerIconResourceId = 1, + ), + FakeKeyguardQuickAffordanceConfig( + key = AFFORDANCE_2, + pickerIconResourceId = 2, + ), + ), + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), + ) + underTest.interactor = + KeyguardQuickAffordanceInteractor( + keyguardInteractor = + KeyguardInteractor( + repository = FakeKeyguardRepository(), + ), + registry = mock(), + lockPatternUtils = lockPatternUtils, + keyguardStateController = keyguardStateController, + userTracker = userTracker, + activityStarter = activityStarter, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) + }, + repository = { quickAffordanceRepository }, + ) + + underTest.attachInfoForTesting( + context, + ProviderInfo().apply { authority = Contract.AUTHORITY }, + ) + context.contentResolver.addProvider(Contract.AUTHORITY, underTest) + context.testablePermissions.setPermission( + Contract.PERMISSION, + PackageManager.PERMISSION_GRANTED, + ) + } + + @Test + fun `onAttachInfo - reportsContext`() { + val callback: SystemUIAppComponentFactoryBase.ContextAvailableCallback = mock() + underTest.setContextAvailableCallback(callback) + + underTest.attachInfo(context, null) + + verify(callback).onContextAvailable(context) + } + + @Test + fun getType() { + assertThat(underTest.getType(Contract.AffordanceTable.URI)) + .isEqualTo( + "vnd.android.cursor.dir/vnd." + + "${Contract.AUTHORITY}.${Contract.AffordanceTable.TABLE_NAME}" + ) + assertThat(underTest.getType(Contract.SlotTable.URI)) + .isEqualTo( + "vnd.android.cursor.dir/vnd.${Contract.AUTHORITY}.${Contract.SlotTable.TABLE_NAME}" + ) + assertThat(underTest.getType(Contract.SelectionTable.URI)) + .isEqualTo( + "vnd.android.cursor.dir/vnd." + + "${Contract.AUTHORITY}.${Contract.SelectionTable.TABLE_NAME}" + ) + } + + @Test + fun `insert and query selection`() = + runBlocking(IMMEDIATE) { + val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + val affordanceId = AFFORDANCE_2 + + insertSelection( + slotId = slotId, + affordanceId = affordanceId, + ) + + assertThat(querySelections()) + .isEqualTo( + listOf( + Selection( + slotId = slotId, + affordanceId = affordanceId, + ) + ) + ) + } + + @Test + fun `query slots`() = + runBlocking(IMMEDIATE) { + assertThat(querySlots()) + .isEqualTo( + listOf( + Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + capacity = 1, + ), + Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + capacity = 1, + ), + ) + ) + } + + @Test + fun `query affordances`() = + runBlocking(IMMEDIATE) { + assertThat(queryAffordances()) + .isEqualTo( + listOf( + Affordance( + id = AFFORDANCE_1, + name = AFFORDANCE_1, + iconResourceId = 1, + ), + Affordance( + id = AFFORDANCE_2, + name = AFFORDANCE_2, + iconResourceId = 2, + ), + ) + ) + } + + @Test + fun `delete and query selection`() = + runBlocking(IMMEDIATE) { + insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = AFFORDANCE_1, + ) + insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + affordanceId = AFFORDANCE_2, + ) + + context.contentResolver.delete( + Contract.SelectionTable.URI, + "${Contract.SelectionTable.Columns.SLOT_ID} = ? AND" + + " ${Contract.SelectionTable.Columns.AFFORDANCE_ID} = ?", + arrayOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + AFFORDANCE_2, + ), + ) + + assertThat(querySelections()) + .isEqualTo( + listOf( + Selection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = AFFORDANCE_1, + ) + ) + ) + } + + @Test + fun `delete all selections in a slot`() = + runBlocking(IMMEDIATE) { + insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = AFFORDANCE_1, + ) + insertSelection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + affordanceId = AFFORDANCE_2, + ) + + context.contentResolver.delete( + Contract.SelectionTable.URI, + Contract.SelectionTable.Columns.SLOT_ID, + arrayOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + ), + ) + + assertThat(querySelections()) + .isEqualTo( + listOf( + Selection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = AFFORDANCE_1, + ) + ) + ) + } + + private fun insertSelection( + slotId: String, + affordanceId: String, + ) { + context.contentResolver.insert( + Contract.SelectionTable.URI, + ContentValues().apply { + put(Contract.SelectionTable.Columns.SLOT_ID, slotId) + put(Contract.SelectionTable.Columns.AFFORDANCE_ID, affordanceId) + } + ) + } + + private fun querySelections(): List<Selection> { + return context.contentResolver + .query( + Contract.SelectionTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val slotIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.SLOT_ID) + val affordanceIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_ID) + if (slotIdColumnIndex == -1 || affordanceIdColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + Selection( + slotId = cursor.getString(slotIdColumnIndex), + affordanceId = cursor.getString(affordanceIdColumnIndex), + ) + ) + } + } + } + ?: emptyList() + } + + private fun querySlots(): List<Slot> { + return context.contentResolver + .query( + Contract.SlotTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = cursor.getColumnIndex(Contract.SlotTable.Columns.ID) + val capacityColumnIndex = + cursor.getColumnIndex(Contract.SlotTable.Columns.CAPACITY) + if (idColumnIndex == -1 || capacityColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + Slot( + id = cursor.getString(idColumnIndex), + capacity = cursor.getInt(capacityColumnIndex), + ) + ) + } + } + } + ?: emptyList() + } + + private fun queryAffordances(): List<Affordance> { + return context.contentResolver + .query( + Contract.AffordanceTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = cursor.getColumnIndex(Contract.AffordanceTable.Columns.ID) + val nameColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.NAME) + val iconColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.ICON) + if (idColumnIndex == -1 || nameColumnIndex == -1 || iconColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + Affordance( + id = cursor.getString(idColumnIndex), + name = cursor.getString(nameColumnIndex), + iconResourceId = cursor.getInt(iconColumnIndex), + ) + ) + } + } + } + ?: emptyList() + } + + data class Slot( + val id: String, + val capacity: Int, + ) + + data class Affordance( + val id: String, + val name: String, + val iconResourceId: Int, + ) + + data class Selection( + val slotId: String, + val affordanceId: String, + ) + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private const val AFFORDANCE_1 = "affordance_1" + private const val AFFORDANCE_2 = "affordance_2" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt new file mode 100644 index 000000000000..623becf166d3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt @@ -0,0 +1,61 @@ +/* + * 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.keyguard.data.quickaffordance + +import android.app.StatusBarManager +import android.content.Context +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.camera.CameraGestureHelper +import org.junit.Assert.assertEquals +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.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class CameraQuickAffordanceConfigTest : SysuiTestCase() { + + @Mock private lateinit var cameraGestureHelper: CameraGestureHelper + @Mock private lateinit var context: Context + private lateinit var underTest: CameraQuickAffordanceConfig + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = CameraQuickAffordanceConfig( + context, + cameraGestureHelper, + ) + } + + @Test + fun `affordance triggered -- camera launch called`() { + //when + val result = underTest.onTriggered(null) + + //then + verify(cameraGestureHelper) + .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt new file mode 100644 index 000000000000..8ef921eaa50a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt @@ -0,0 +1,191 @@ +/* + * 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.keyguard.data.quickaffordance + +import android.content.Context +import android.content.res.Resources +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { + + @Mock private lateinit var sharedPrefs: FakeSharedPreferences + + private lateinit var underTest: KeyguardQuickAffordanceLegacySettingSyncer + + private lateinit var testScope: TestScope + private lateinit var testDispatcher: TestDispatcher + private lateinit var selectionManager: KeyguardQuickAffordanceSelectionManager + private lateinit var settings: FakeSettings + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + val context: Context = mock() + sharedPrefs = FakeSharedPreferences() + whenever(context.getSharedPreferences(anyString(), any())).thenReturn(sharedPrefs) + val resources: Resources = mock() + whenever(resources.getStringArray(R.array.config_keyguardQuickAffordanceDefaults)) + .thenReturn(emptyArray()) + whenever(context.resources).thenReturn(resources) + + testDispatcher = UnconfinedTestDispatcher() + testScope = TestScope(testDispatcher) + selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = FakeUserTracker(), + ) + settings = FakeSettings() + settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0) + settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET, 0) + settings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER, 0) + + underTest = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = testScope, + backgroundDispatcher = testDispatcher, + secureSettings = settings, + selectionsManager = selectionManager, + ) + } + + @Test + fun `Setting a setting selects the affordance`() = + testScope.runTest { + val job = underTest.startSyncing() + + settings.putInt( + Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, + 1, + ) + + assertThat( + selectionManager + .getSelections() + .getOrDefault( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + emptyList() + ) + ) + .contains(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) + + job.cancel() + } + + @Test + fun `Clearing a setting selects the affordance`() = + testScope.runTest { + val job = underTest.startSyncing() + + settings.putInt( + Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, + 1, + ) + settings.putInt( + Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, + 0, + ) + + assertThat( + selectionManager + .getSelections() + .getOrDefault( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + emptyList() + ) + ) + .doesNotContain(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) + + job.cancel() + } + + @Test + fun `Selecting an affordance sets its setting`() = + testScope.runTest { + val job = underTest.startSyncing() + + selectionManager.setSelections( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET) + ) + + advanceUntilIdle() + assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(1) + + job.cancel() + } + + @Test + fun `Unselecting an affordance clears its setting`() = + testScope.runTest { + val job = underTest.startSyncing() + + selectionManager.setSelections( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET) + ) + selectionManager.setSelections( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + emptyList() + ) + + assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(0) + + job.cancel() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt index d2422ad7b53b..d8ee9f113d33 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt @@ -17,111 +17,312 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.SharedPreferences +import android.content.pm.UserInfo import androidx.test.filters.SmallTest +import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.UserFileManager +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.MockitoAnnotations @SmallTest @RunWith(JUnit4::class) class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { + @Mock private lateinit var userFileManager: UserFileManager + private lateinit var underTest: KeyguardQuickAffordanceSelectionManager + private lateinit var userTracker: FakeUserTracker + private lateinit var sharedPrefs: MutableMap<Int, SharedPreferences> + @Before fun setUp() { - underTest = KeyguardQuickAffordanceSelectionManager() + MockitoAnnotations.initMocks(this) + sharedPrefs = mutableMapOf() + whenever(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())).thenAnswer { + val userId = it.arguments[2] as Int + sharedPrefs.getOrPut(userId) { FakeSharedPreferences() } + } + userTracker = FakeUserTracker() + + underTest = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = userFileManager, + userTracker = userTracker, + ) } @Test - fun setSelections() = - runBlocking(IMMEDIATE) { - var affordanceIdsBySlotId: Map<String, List<String>>? = null - val job = underTest.selections.onEach { affordanceIdsBySlotId = it }.launchIn(this) - val slotId1 = "slot1" - val slotId2 = "slot2" - val affordanceId1 = "affordance1" - val affordanceId2 = "affordance2" - val affordanceId3 = "affordance3" - - underTest.setSelections( - slotId = slotId1, - affordanceIds = listOf(affordanceId1), - ) - assertSelections( - affordanceIdsBySlotId, - mapOf( - slotId1 to listOf(affordanceId1), - ), - ) + fun setSelections() = runTest { + overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>()) + val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" - underTest.setSelections( - slotId = slotId2, - affordanceIds = listOf(affordanceId2), + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId1), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId1), + ), + ) + + underTest.setSelections( + slotId = slotId2, + affordanceIds = listOf(affordanceId2), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId1), + slotId2 to listOf(affordanceId2), ) - assertSelections( - affordanceIdsBySlotId, - mapOf( - slotId1 to listOf(affordanceId1), - slotId2 to listOf(affordanceId2), - ) + ) + + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId1, affordanceId3), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId1, affordanceId3), + slotId2 to listOf(affordanceId2), ) + ) - underTest.setSelections( - slotId = slotId1, - affordanceIds = listOf(affordanceId1, affordanceId3), + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId3), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId3), + slotId2 to listOf(affordanceId2), ) - assertSelections( - affordanceIdsBySlotId, - mapOf( - slotId1 to listOf(affordanceId1, affordanceId3), - slotId2 to listOf(affordanceId2), - ) + ) + + underTest.setSelections( + slotId = slotId2, + affordanceIds = listOf(), + ) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId3), + slotId2 to listOf(), ) + ) - underTest.setSelections( - slotId = slotId1, - affordanceIds = listOf(affordanceId3), + job.cancel() + } + + @Test + fun `remembers selections by user`() = runTest { + val slot1 = "slot_1" + val slot2 = "slot_2" + val affordance1 = "affordance_1" + val affordance2 = "affordance_2" + val affordance3 = "affordance_3" + + val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + + val userInfos = + listOf( + UserInfo(/* id= */ 0, "zero", /* flags= */ 0), + UserInfo(/* id= */ 1, "one", /* flags= */ 0), ) - assertSelections( - affordanceIdsBySlotId, + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 0, + ) + underTest.setSelections( + slotId = slot1, + affordanceIds = listOf(affordance1), + ) + underTest.setSelections( + slotId = slot2, + affordanceIds = listOf(affordance2), + ) + + // Switch to user 1 + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 1, + ) + // We never set selections on user 1, so it should be empty. + assertSelections( + observed = affordanceIdsBySlotId.last(), + expected = emptyMap(), + ) + // Now, let's set selections on user 1. + underTest.setSelections( + slotId = slot1, + affordanceIds = listOf(affordance2), + ) + underTest.setSelections( + slotId = slot2, + affordanceIds = listOf(affordance3), + ) + assertSelections( + observed = affordanceIdsBySlotId.last(), + expected = mapOf( - slotId1 to listOf(affordanceId3), - slotId2 to listOf(affordanceId2), - ) - ) + slot1 to listOf(affordance2), + slot2 to listOf(affordance3), + ), + ) - underTest.setSelections( - slotId = slotId2, - affordanceIds = listOf(), - ) - assertSelections( - affordanceIdsBySlotId, + // Switch back to user 0. + userTracker.set( + userInfos = userInfos, + selectedUserIndex = 0, + ) + // Assert that we still remember the old selections for user 0. + assertSelections( + observed = affordanceIdsBySlotId.last(), + expected = mapOf( - slotId1 to listOf(affordanceId3), - slotId2 to listOf(), - ) - ) + slot1 to listOf(affordance1), + slot2 to listOf(affordance2), + ), + ) - job.cancel() - } + job.cancel() + } + + @Test + fun `selections respects defaults`() = runTest { + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" + overrideResource( + R.array.config_keyguardQuickAffordanceDefaults, + arrayOf( + "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}", + "$slotId2:${listOf(affordanceId2).joinToString(",")}", + ), + ) + val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId1, affordanceId3), + slotId2 to listOf(affordanceId2), + ), + ) + + job.cancel() + } + + @Test + fun `selections ignores defaults after selecting an affordance`() = runTest { + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" + overrideResource( + R.array.config_keyguardQuickAffordanceDefaults, + arrayOf( + "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}", + "$slotId2:${listOf(affordanceId2).joinToString(",")}", + ), + ) + val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + + underTest.setSelections(slotId1, listOf(affordanceId2)) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(affordanceId2), + slotId2 to listOf(affordanceId2), + ), + ) + + job.cancel() + } + + @Test + fun `selections ignores defaults after clearing a slot`() = runTest { + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" + overrideResource( + R.array.config_keyguardQuickAffordanceDefaults, + arrayOf( + "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}", + "$slotId2:${listOf(affordanceId2).joinToString(",")}", + ), + ) + val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } - private suspend fun assertSelections( + underTest.setSelections(slotId1, listOf()) + assertSelections( + affordanceIdsBySlotId.last(), + mapOf( + slotId1 to listOf(), + slotId2 to listOf(affordanceId2), + ), + ) + + job.cancel() + } + + private fun assertSelections( observed: Map<String, List<String>>?, expected: Map<String, List<String>>, ) { assertThat(underTest.getSelections()).isEqualTo(expected) assertThat(observed).isEqualTo(expected) } - - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index 5a7f2bb5cb37..d8a360567a07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -18,13 +18,20 @@ package com.android.systemui.keyguard.data.repository import androidx.test.filters.SmallTest +import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation -import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.UserFileManager +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -36,6 +43,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -51,11 +60,36 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { fun setUp() { config1 = FakeKeyguardQuickAffordanceConfig("built_in:1") config2 = FakeKeyguardQuickAffordanceConfig("built_in:2") + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock<UserFileManager>().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = FakeUserTracker(), + ) + underTest = KeyguardQuickAffordanceRepository( - scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + appContext = context, + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), configs = setOf(config1, config2), ) } @@ -119,16 +153,32 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { @Test fun getSlotPickerRepresentations() { + val slot1 = "slot1" + val slot2 = "slot2" + val slot3 = "slot3" + context.orCreateTestableResources.addOverride( + R.array.config_keyguardQuickAffordanceSlots, + arrayOf( + "$slot1:2", + "$slot2:4", + "$slot3:5", + ), + ) + assertThat(underTest.getSlotPickerRepresentations()) .isEqualTo( listOf( KeyguardSlotPickerRepresentation( - id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, - maxSelectedAffordances = 1, + id = slot1, + maxSelectedAffordances = 2, + ), + KeyguardSlotPickerRepresentation( + id = slot2, + maxSelectedAffordances = 4, ), KeyguardSlotPickerRepresentation( - id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, - maxSelectedAffordances = 1, + id = slot3, + maxSelectedAffordances = 5, ), ) ) 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 8b6603d79244..1e1d3f19d83c 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 @@ -30,17 +30,22 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runBlockingTest @@ -50,6 +55,8 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameters +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.eq import org.mockito.ArgumentMatchers.same import org.mockito.Mock @@ -201,7 +208,6 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var keyguardStateController: KeyguardStateController - @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller @Mock private lateinit var expandable: Expandable @@ -214,12 +220,14 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @JvmField @Parameter(3) var needsToUnlockFirst: Boolean = false @JvmField @Parameter(4) var startActivity: Boolean = false private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig + private lateinit var userTracker: UserTracker @Before fun setUp() { MockitoAnnotations.initMocks(this) whenever(expandable.activityLaunchController()).thenReturn(animationController) + userTracker = FakeUserTracker() homeControls = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) val quickAccessWallet = @@ -228,11 +236,35 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { ) val qrCodeScanner = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock<UserFileManager>().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( - scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + appContext = context, + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), ) underTest = @@ -318,7 +350,6 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { needStrongAuthAfterBoot: Boolean = true, keyguardIsUnlocked: Boolean = false, ) { - whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(any())) .thenReturn( if (needStrongAuthAfterBoot) { 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 33645354d9f3..c47e6f52c596 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 @@ -27,6 +27,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository @@ -35,11 +36,14 @@ import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAff import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -53,6 +57,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.MockitoAnnotations @@ -89,12 +95,36 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) qrCodeScanner = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) - + val scope = CoroutineScope(IMMEDIATE) + + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock<UserFileManager>().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( - scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + appContext = context, + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), ) featureFlags = 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 78148c4d3d1b..ecc63ecc879d 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 @@ -28,6 +28,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository @@ -38,10 +39,13 @@ import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAff import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlin.math.max import kotlin.math.min @@ -56,6 +60,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.verifyZeroInteractions @@ -115,11 +120,35 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) + val scope = CoroutineScope(IMMEDIATE) + val selectionManager = + KeyguardQuickAffordanceSelectionManager( + context = context, + userFileManager = + mock<UserFileManager>().apply { + whenever( + getSharedPreferences( + anyString(), + anyInt(), + anyInt(), + ) + ) + .thenReturn(FakeSharedPreferences()) + }, + userTracker = userTracker, + ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( - scope = CoroutineScope(IMMEDIATE), - backgroundDispatcher = IMMEDIATE, - selectionManager = KeyguardQuickAffordanceSelectionManager(), + appContext = context, + scope = scope, + selectionManager = selectionManager, + legacySettingSyncer = + KeyguardQuickAffordanceLegacySettingSyncer( + scope = scope, + backgroundDispatcher = IMMEDIATE, + secureSettings = FakeSettings(), + selectionsManager = selectionManager, + ), configs = setOf( homeControlsQuickAffordanceConfig, 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 11eb26b1da02..52b694fac07c 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 @@ -20,6 +20,8 @@ import android.app.Notification import android.app.Notification.MediaStyle import android.app.PendingIntent import android.app.smartspace.SmartspaceAction +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager import android.app.smartspace.SmartspaceTarget import android.content.Intent import android.graphics.Bitmap @@ -60,7 +62,6 @@ import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -106,6 +107,7 @@ class MediaDataManagerTest : SysuiTestCase() { lateinit var metadataBuilder: MediaMetadata.Builder lateinit var backgroundExecutor: FakeExecutor lateinit var foregroundExecutor: FakeExecutor + lateinit var uiExecutor: FakeExecutor @Mock lateinit var dumpManager: DumpManager @Mock lateinit var broadcastDispatcher: BroadcastDispatcher @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener @@ -117,6 +119,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var listener: MediaDataManager.Listener @Mock lateinit var pendingIntent: PendingIntent @Mock lateinit var activityStarter: ActivityStarter + @Mock lateinit var smartspaceManager: SmartspaceManager lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget @Mock private lateinit var mediaRecommendationItem: SmartspaceAction @@ -131,6 +134,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock private lateinit var tunerService: TunerService @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable> @Captor lateinit var callbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit> + @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig> private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) @@ -145,6 +149,7 @@ class MediaDataManagerTest : SysuiTestCase() { fun setup() { foregroundExecutor = FakeExecutor(clock) backgroundExecutor = FakeExecutor(clock) + uiExecutor = FakeExecutor(clock) smartspaceMediaDataProvider = SmartspaceMediaDataProvider() Settings.Secure.putInt( context.contentResolver, @@ -155,6 +160,7 @@ class MediaDataManagerTest : SysuiTestCase() { MediaDataManager( context = context, backgroundExecutor = backgroundExecutor, + uiExecutor = uiExecutor, foregroundExecutor = foregroundExecutor, mediaControllerFactory = mediaControllerFactory, broadcastDispatcher = broadcastDispatcher, @@ -172,7 +178,8 @@ class MediaDataManagerTest : SysuiTestCase() { systemClock = clock, tunerService = tunerService, mediaFlags = mediaFlags, - logger = logger + logger = logger, + smartspaceManager = smartspaceManager, ) verify(tunerService) .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) @@ -191,6 +198,7 @@ class MediaDataManagerTest : SysuiTestCase() { putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) } + verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor)) whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller) whenever(controller.transportControls).thenReturn(transportControls) whenever(controller.playbackInfo).thenReturn(playbackInfo) @@ -767,15 +775,14 @@ class MediaDataManagerTest : SysuiTestCase() { .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) } - @Ignore("b/233283726") @Test fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() { smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) verify(logger).getNewInstanceId() smartspaceMediaDataProvider.onTargetsAvailable(listOf()) - foregroundExecutor.advanceClockToLast() - foregroundExecutor.runAllReady() + uiExecutor.advanceClockToLast() + uiExecutor.runAllReady() verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false)) verifyNoMoreInteractions(logger) @@ -798,7 +805,6 @@ class MediaDataManagerTest : SysuiTestCase() { .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) } - @Ignore("b/229838140") @Test fun testMediaRecommendationDisabled_removesSmartspaceData() { // GIVEN a media recommendation card is present @@ -815,7 +821,9 @@ class MediaDataManagerTest : SysuiTestCase() { tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0") // THEN listeners are notified + uiExecutor.advanceClockToLast() foregroundExecutor.advanceClockToLast() + uiExecutor.runAllReady() foregroundExecutor.runAllReady() verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(true)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt index 2c2ddbb9b8c5..645b1cde632f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt @@ -16,10 +16,8 @@ package com.android.systemui.qs.footer.domain.interactor -import android.content.ComponentName import android.content.Context import android.content.Intent -import android.os.UserHandle import android.provider.Settings import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -30,17 +28,13 @@ import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.globalactions.GlobalActionsDialogLite import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.QSSecurityFooterUtils import com.android.systemui.qs.footer.FooterActionsTestUtils -import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.truth.correspondence.FakeUiEvent import com.android.systemui.truth.correspondence.LogMaker -import com.android.systemui.user.UserSwitcherActivity import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq @@ -156,54 +150,4 @@ class FooterActionsInteractorTest : SysuiTestCase() { // We only unlock the device. verify(activityStarter).postQSRunnableDismissingKeyguard(any()) } - - @Test - fun showUserSwitcher_fullScreenDisabled() { - val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } - val userSwitchDialogController = mock<UserSwitchDialogController>() - val underTest = - utils.footerActionsInteractor( - featureFlags = featureFlags, - userSwitchDialogController = userSwitchDialogController, - ) - - val expandable = mock<Expandable>() - underTest.showUserSwitcher(context, expandable) - - // Dialog is shown. - verify(userSwitchDialogController).showDialog(context, expandable) - } - - @Test - fun showUserSwitcher_fullScreenEnabled() { - val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) } - val activityStarter = mock<ActivityStarter>() - val underTest = - utils.footerActionsInteractor( - featureFlags = featureFlags, - activityStarter = activityStarter, - ) - - // The clicked expandable. - val expandable = mock<Expandable>() - underTest.showUserSwitcher(context, expandable) - - // Dialog is shown. - val intentCaptor = argumentCaptor<Intent>() - verify(activityStarter) - .startActivity( - intentCaptor.capture(), - /* dismissShade= */ eq(true), - /* ActivityLaunchAnimator.Controller= */ nullable(), - /* showOverLockscreenWhenLocked= */ eq(true), - eq(UserHandle.SYSTEM), - ) - assertThat(intentCaptor.value.component) - .isEqualTo( - ComponentName( - context, - UserSwitcherActivity::class.java, - ) - ) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java index 8c9404e336ca..85c8ba7e77b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java @@ -184,7 +184,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { ActionTransition::new, mSmartActionsProvider); Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(), - Uri.parse("Screenshot_123.png")).get().action; + Uri.parse("Screenshot_123.png"), true).get().action; Intent intent = shareAction.actionIntent.getIntent(); assertNotNull(intent); @@ -212,7 +212,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { ActionTransition::new, mSmartActionsProvider); Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(), - Uri.parse("Screenshot_123.png")).get().action; + Uri.parse("Screenshot_123.png"), true).get().action; Intent intent = editAction.actionIntent.getIntent(); assertNotNull(intent); @@ -241,7 +241,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { Notification.Action deleteAction = task.createDeleteAction(mContext, mContext.getResources(), - Uri.parse("Screenshot_123.png")); + Uri.parse("Screenshot_123.png"), true); Intent intent = deleteAction.actionIntent.getIntent(); assertNotNull(intent); 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 7d2251e20021..69a45599668b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -133,6 +133,7 @@ import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; +import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager; @@ -198,6 +199,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private KeyguardBottomAreaView mQsFrame; @Mock private HeadsUpManagerPhone mHeadsUpManager; @Mock private NotificationShelfController mNotificationShelfController; + @Mock private NotificationGutsManager mGutsManager; @Mock private KeyguardStatusBarView mKeyguardStatusBar; @Mock private KeyguardUserSwitcherView mUserSwitcherView; @Mock private ViewStub mUserSwitcherStubView; @@ -453,6 +455,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager, mConversationNotificationManager, mMediaHierarchyManager, mStatusBarKeyguardViewManager, + mGutsManager, mNotificationsQSContainerController, mNotificationStackScrollLayoutController, mKeyguardStatusViewComponentFactory, @@ -754,6 +757,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testOnTouchEvent_expansionResumesAfterBriefTouch() { + mFalsingManager.setIsClassifierEnabled(true); + mFalsingManager.setIsFalseTouch(false); // Start shade collapse with swipe up onTouchEvent(MotionEvent.obtain(0L /* downTime */, 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, @@ -1119,6 +1124,19 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { } @Test + public void testUnlockedSplitShadeTransitioningToKeyguard_closesQS() { + enableSplitShade(true); + mStatusBarStateController.setState(SHADE); + mNotificationPanelViewController.setQsExpanded(true); + + mStatusBarStateController.setState(KEYGUARD); + + + assertThat(mNotificationPanelViewController.isQsExpanded()).isEqualTo(false); + assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isEqualTo(false); + } + + @Test public void testSwitchesToCorrectClockInSinglePaneShade() { mStatusBarStateController.setState(KEYGUARD); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index db7e017e379e..c3207c2f58a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.statusbar.LockscreenShadeTransitionController +import com.android.systemui.statusbar.NotificationInsetsController import com.android.systemui.statusbar.NotificationShadeDepthController import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -94,6 +95,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController @Mock private lateinit var pulsingGestureListener: PulsingGestureListener + @Mock + private lateinit var notificationInsetsController: NotificationInsetsController @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory @Mock lateinit var keyguardBouncerContainer: ViewGroup @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent @@ -124,6 +127,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { centralSurfaces, notificationShadeWindowController, keyguardUnlockAnimationController, + notificationInsetsController, ambientState, pulsingGestureListener, featureFlags, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java index a6c80ab649e5..4bf00c4ccb51 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java @@ -43,6 +43,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.statusbar.DragDownHelper; import com.android.systemui.statusbar.LockscreenShadeTransitionController; +import com.android.systemui.statusbar.NotificationInsetsController; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; @@ -91,6 +92,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { @Mock private FeatureFlags mFeatureFlags; @Mock private KeyguardBouncerViewModel mKeyguardBouncerViewModel; @Mock private KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory; + @Mock private NotificationInsetsController mNotificationInsetsController; @Captor private ArgumentCaptor<NotificationShadeWindowView.InteractionEventHandler> mInteractionEventHandlerCaptor; @@ -125,6 +127,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { mCentralSurfaces, mNotificationShadeWindowController, mKeyguardUnlockAnimationController, + mNotificationInsetsController, mAmbientState, mPulsingGestureListener, mFeatureFlags, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt index f5bed79b5e6f..a7588ddcaeef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt @@ -43,6 +43,7 @@ import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.notNull import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever @@ -178,4 +179,12 @@ class DefaultClockProviderTest : SysuiTestCase() { verify(mockSmallClockView, times(2)).refreshFormat() verify(mockLargeClockView, times(2)).refreshFormat() } + + @Test + fun test_aodClock_always_whiteColor() { + val clock = provider.createClock(DEFAULT_CLOCK_ID) + clock.animations.doze(0.9f) // set AOD mode to active + clock.smallClock.events.onRegionDarknessChanged(true) + verify((clock.smallClock.view as AnimatableClockView), never()).animateAppearOnLockscreen() + } } 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 e8a7ec82aab1..c8a392b11363 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -32,12 +32,12 @@ import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewCont import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_DISCLOSURE; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_LOGOUT; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_OWNER_INFO; -import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_RESTING; +import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRANSIENT; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRUST; -import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED; import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_OFF; import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON; +import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_TURNING_ON; import static com.google.common.truth.Truth.assertThat; @@ -87,6 +87,7 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.keyguard.TrustGrantFlags; import com.android.keyguard.logging.KeyguardLogger; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.R; @@ -829,31 +830,6 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { } @Test - public void updateMonitor_listenerUpdatesIndication() { - createController(); - String restingIndication = "Resting indication"; - reset(mKeyguardUpdateMonitor); - - mController.setVisible(true); - verifyIndicationMessage(INDICATION_TYPE_USER_LOCKED, - mContext.getString(com.android.internal.R.string.lockscreen_storage_locked)); - - reset(mRotateTextViewController); - when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true); - when(mKeyguardUpdateMonitor.isUserUnlocked(anyInt())).thenReturn(true); - mController.setRestingIndication(restingIndication); - verifyHideIndication(INDICATION_TYPE_USER_LOCKED); - verifyIndicationMessage(INDICATION_TYPE_RESTING, restingIndication); - - reset(mRotateTextViewController); - reset(mKeyguardUpdateMonitor); - when(mKeyguardUpdateMonitor.isUserUnlocked(anyInt())).thenReturn(true); - when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false); - mKeyguardStateControllerCallback.onUnlockedChanged(); - verifyIndicationMessage(INDICATION_TYPE_RESTING, restingIndication); - } - - @Test public void onRefreshBatteryInfo_computesChargingTime() throws RemoteException { createController(); BatteryStatus status = new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING, @@ -1068,7 +1044,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { // GIVEN a trust granted message but trust isn't granted final String trustGrantedMsg = "testing trust granted message"; - mController.getKeyguardCallback().onTrustGrantedWithFlags(0, 0, trustGrantedMsg); + mController.getKeyguardCallback().onTrustGrantedForCurrentUser( + false, new TrustGrantFlags(0), trustGrantedMsg); verifyHideIndication(INDICATION_TYPE_TRUST); @@ -1092,7 +1069,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { // WHEN the showTrustGranted method is called final String trustGrantedMsg = "testing trust granted message"; - mController.getKeyguardCallback().onTrustGrantedWithFlags(0, 0, trustGrantedMsg); + mController.getKeyguardCallback().onTrustGrantedForCurrentUser( + false, new TrustGrantFlags(0), trustGrantedMsg); // THEN verify the trust granted message shows verifyIndicationMessage( @@ -1109,7 +1087,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true); // WHEN the showTrustGranted method is called with a null message - mController.getKeyguardCallback().onTrustGrantedWithFlags(0, 0, null); + mController.getKeyguardCallback().onTrustGrantedForCurrentUser( + false, new TrustGrantFlags(0), null); // THEN verify the default trust granted message shows verifyIndicationMessage( @@ -1126,7 +1105,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true); // WHEN the showTrustGranted method is called with an EMPTY string - mController.getKeyguardCallback().onTrustGrantedWithFlags(0, 0, ""); + mController.getKeyguardCallback().onTrustGrantedForCurrentUser( + false, new TrustGrantFlags(0), ""); // THEN verify NO trust message is shown verifyNoMessage(INDICATION_TYPE_TRUST); @@ -1488,6 +1468,44 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { } @Test + public void onFpLockoutStateChanged_whenFpIsLockedOut_showsPersistentMessage() { + createController(); + mController.setVisible(true); + when(mKeyguardUpdateMonitor.isFingerprintLockedOut()).thenReturn(true); + + mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT); + + verifyIndicationShown(INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE, + mContext.getString(R.string.keyguard_unlock)); + } + + @Test + public void onFpLockoutStateChanged_whenFpIsNotLockedOut_showsPersistentMessage() { + createController(); + mController.setVisible(true); + clearInvocations(mRotateTextViewController); + when(mKeyguardUpdateMonitor.isFingerprintLockedOut()).thenReturn(false); + + mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT); + + verifyHideIndication(INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE); + } + + @Test + public void onVisibilityChange_showsPersistentMessage_ifFpIsLockedOut() { + createController(); + mController.setVisible(false); + when(mKeyguardUpdateMonitor.isFingerprintLockedOut()).thenReturn(true); + mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT); + clearInvocations(mRotateTextViewController); + + mController.setVisible(true); + + verifyIndicationShown(INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE, + mContext.getString(R.string.keyguard_unlock)); + } + + @Test public void onBiometricError_whenFaceIsLocked_onMultipleLockOutErrors_showUnavailableMessage() { createController(); onFaceLockoutError("first lockout"); @@ -1501,6 +1519,44 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mContext.getString(R.string.keyguard_face_unlock_unavailable)); } + @Test + public void onBiometricError_screenIsTurningOn_faceLockedOutFpIsNotAvailable_showsMessage() { + createController(); + screenIsTurningOn(); + fingerprintUnlockIsNotPossible(); + + onFaceLockoutError("lockout error"); + verifyNoMoreInteractions(mRotateTextViewController); + + mScreenObserver.onScreenTurnedOn(); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE, + "lockout error"); + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, + mContext.getString(R.string.keyguard_unlock)); + } + + @Test + public void onBiometricError_screenIsTurningOn_faceLockedOutFpIsAvailable_showsMessage() { + createController(); + screenIsTurningOn(); + fingerprintUnlockIsPossible(); + + onFaceLockoutError("lockout error"); + verifyNoMoreInteractions(mRotateTextViewController); + + mScreenObserver.onScreenTurnedOn(); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE, + "lockout error"); + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, + mContext.getString(R.string.keyguard_suggest_fingerprint)); + } + + private void screenIsTurningOn() { + when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_TURNING_ON); + } + private void sendUpdateDisclosureBroadcast() { mBroadcastReceiver.onReceive(mContext, new Intent()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt index 7e2e6f619946..bdedd244abfa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt @@ -13,57 +13,55 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.statusbar.notification.collection.coordinator import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.NotifPipelineFlags +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor -import java.util.function.Consumer -import org.junit.Before +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify +import java.util.function.Consumer +import kotlin.time.Duration.Companion.seconds import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) class KeyguardCoordinatorTest : SysuiTestCase() { - private val notifPipeline: NotifPipeline = mock() + private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() + private val keyguardRepository = FakeKeyguardRepository() + private val notifPipelineFlags: NotifPipelineFlags = mock() + private val notifPipeline: NotifPipeline = mock() private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock() private val statusBarStateController: StatusBarStateController = mock() - private lateinit var onStateChangeListener: Consumer<String> - private lateinit var keyguardFilter: NotifFilter - - @Before - fun setup() { - val keyguardCoordinator = KeyguardCoordinator( - keyguardNotifVisibilityProvider, - sectionHeaderVisibilityProvider, - statusBarStateController - ) - keyguardCoordinator.attach(notifPipeline) - onStateChangeListener = withArgCaptor { - verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) - } - keyguardFilter = withArgCaptor { - verify(notifPipeline).addFinalizeFilter(capture()) - } - } - @Test - fun testSetSectionHeadersVisibleInShade() { + fun testSetSectionHeadersVisibleInShade() = runKeyguardCoordinatorTest { clearInvocations(sectionHeaderVisibilityProvider) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) onStateChangeListener.accept("state change") @@ -71,10 +69,176 @@ class KeyguardCoordinatorTest : SysuiTestCase() { } @Test - fun testSetSectionHeadersNotVisibleOnKeyguard() { + fun testSetSectionHeadersNotVisibleOnKeyguard() = runKeyguardCoordinatorTest { clearInvocations(sectionHeaderVisibilityProvider) whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) onStateChangeListener.accept("state change") verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false) } + + @Test + fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() { + whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) + + // GIVEN: Keyguard is not showing, and a notification is present + keyguardRepository.setKeyguardShowing(false) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN: The keyguard goes away + keyguardRepository.setKeyguardShowing(false) + testScheduler.runCurrent() + + // THEN: The notification is shown regardless + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterAllowsNewNotif() { + whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) + + // GIVEN: Keyguard is showing, no notifications present + keyguardRepository.setKeyguardShowing(true) + runKeyguardCoordinatorTest { + // WHEN: A new notification is posted + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // THEN: The notification is recognized as "unseen" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterSeenGroupSummaryWithUnseenChild() { + whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) + + // GIVEN: Keyguard is not showing, and a notification is present + keyguardRepository.setKeyguardShowing(false) + runKeyguardCoordinatorTest { + // WHEN: A new notification is posted + val fakeSummary = NotificationEntryBuilder().build() + val fakeChild = NotificationEntryBuilder() + .setGroup(context, "group") + .setGroupSummary(context, false) + .build() + GroupEntryBuilder() + .setSummary(fakeSummary) + .addChild(fakeChild) + .build() + + collectionListener.onEntryAdded(fakeSummary) + collectionListener.onEntryAdded(fakeChild) + + // WHEN: Keyguard is now showing, both notifications are marked as seen + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // WHEN: The child notification is now unseen + collectionListener.onEntryUpdated(fakeChild) + + // THEN: The summary is not filtered out, because the child is unseen + assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() { + whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) + + // GIVEN: Keyguard is showing, unseen notification is present + keyguardRepository.setKeyguardShowing(true) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: Keyguard is no longer showing for 5 seconds + keyguardRepository.setKeyguardShowing(false) + testScheduler.runCurrent() + testScheduler.advanceTimeBy(5.seconds.inWholeMilliseconds) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is now recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + } + } + + @Test + fun unseenNotificationIsNotMarkedAsSeenIfTimeThresholdNotMet() { + whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true) + + // GIVEN: Keyguard is showing, unseen notification is present + keyguardRepository.setKeyguardShowing(true) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: Keyguard is no longer showing for <5 seconds + keyguardRepository.setKeyguardShowing(false) + testScheduler.runCurrent() + testScheduler.advanceTimeBy(1.seconds.inWholeMilliseconds) + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is not recognized as "seen" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + private fun runKeyguardCoordinatorTest( + testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit + ) { + val testScope = TestScope(UnconfinedTestDispatcher()) + val keyguardCoordinator = + KeyguardCoordinator( + keyguardNotifVisibilityProvider, + keyguardRepository, + notifPipelineFlags, + testScope.backgroundScope, + sectionHeaderVisibilityProvider, + statusBarStateController, + ) + keyguardCoordinator.attach(notifPipeline) + KeyguardCoordinatorTestScope(keyguardCoordinator, testScope).run { + testScheduler.advanceUntilIdle() + testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { testBlock() } + } + } + + private inner class KeyguardCoordinatorTestScope( + private val keyguardCoordinator: KeyguardCoordinator, + private val scope: TestScope, + ) : CoroutineScope by scope { + val testScheduler: TestCoroutineScheduler + get() = scope.testScheduler + + val onStateChangeListener: Consumer<String> = + withArgCaptor { + verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) + } + + val unseenFilter: NotifFilter + get() = keyguardCoordinator.unseenNotifFilter + + // TODO(254647461): Remove lazy once Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD is enabled and + // removed + val collectionListener: NotifCollectionListener by lazy { + withArgCaptor { verify(notifPipeline).addCollectionListener(capture()) } + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java index b4a5f5ce205f..e488f39bf27d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -38,6 +40,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeStateEvents; import com.android.systemui.shade.ShadeStateEvents.ShadeStateEventsListener; +import com.android.systemui.statusbar.notification.VisibilityLocationProvider; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -73,6 +76,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { @Mock private Pluggable.PluggableListener<NotifStabilityManager> mInvalidateListener; @Mock private HeadsUpManager mHeadsUpManager; @Mock private ShadeStateEvents mShadeStateEvents; + @Mock private VisibilityLocationProvider mVisibilityLocationProvider; @Mock private VisualStabilityProvider mVisualStabilityProvider; @Captor private ArgumentCaptor<WakefulnessLifecycle.Observer> mWakefulnessObserverCaptor; @@ -100,6 +104,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { mHeadsUpManager, mShadeStateEvents, mStatusBarStateController, + mVisibilityLocationProvider, mVisualStabilityProvider, mWakefulnessLifecycle); @@ -355,6 +360,38 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } @Test + public void testMovingVisibleHeadsUpNotAllowed() { + // GIVEN stability enforcing conditions + setPanelExpanded(true); + setSleepy(false); + + // WHEN a notification is alerting and visible + when(mHeadsUpManager.isAlerting(mEntry.getKey())).thenReturn(true); + when(mVisibilityLocationProvider.isInVisibleLocation(any(NotificationEntry.class))) + .thenReturn(true); + + // VERIFY the notification cannot be reordered + assertThat(mNotifStabilityManager.isEntryReorderingAllowed(mEntry)).isFalse(); + assertThat(mNotifStabilityManager.isSectionChangeAllowed(mEntry)).isFalse(); + } + + @Test + public void testMovingInvisibleHeadsUpAllowed() { + // GIVEN stability enforcing conditions + setPanelExpanded(true); + setSleepy(false); + + // WHEN a notification is alerting but not visible + when(mHeadsUpManager.isAlerting(mEntry.getKey())).thenReturn(true); + when(mVisibilityLocationProvider.isInVisibleLocation(any(NotificationEntry.class))) + .thenReturn(false); + + // VERIFY the notification can be reordered + assertThat(mNotifStabilityManager.isEntryReorderingAllowed(mEntry)).isTrue(); + assertThat(mNotifStabilityManager.isSectionChangeAllowed(mEntry)).isTrue(); + } + + @Test public void testNeverSuppressedChanges_noInvalidationCalled() { // GIVEN no notifications are currently being suppressed from grouping nor being sorted diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 90061b078afe..026c82eda42d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -61,6 +61,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotifPipeline; +import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; @@ -122,6 +123,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private UiEventLogger mUiEventLogger; @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController; @Mock private NotificationRemoteInputManager mRemoteInputManager; + @Mock private VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; @Mock private ShadeController mShadeController; @Mock private InteractionJankMonitor mJankMonitor; @Mock private StackStateLogger mStackLogger; @@ -173,6 +175,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mShadeTransitionController, mUiEventLogger, mRemoteInputManager, + mVisibilityLocationProviderDelegator, mShadeController, mJankMonitor, mStackLogger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt index fee3ccb21792..038af8ff5396 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt @@ -14,23 +14,37 @@ package com.android.systemui.statusbar.phone -import androidx.test.filters.SmallTest +import android.content.res.Configuration +import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_LTR +import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_RTL +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.content.res.Configuration.UI_MODE_TYPE_CAR +import android.os.LocaleList import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener +import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.doAnswer import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify +import java.util.Locale @RunWith(AndroidTestingRunner::class) @SmallTest class ConfigurationControllerImplTest : SysuiTestCase() { - private val mConfigurationController = - com.android.systemui.statusbar.phone.ConfigurationControllerImpl(mContext) + private lateinit var mConfigurationController: ConfigurationControllerImpl + + @Before + fun setUp() { + mConfigurationController = ConfigurationControllerImpl(mContext) + } @Test fun testThemeChange() { @@ -57,4 +71,303 @@ class ConfigurationControllerImplTest : SysuiTestCase() { verify(listener).onThemeChanged() verify(listener2, never()).onThemeChanged() } + + @Test + fun configChanged_listenerNotified() { + val config = mContext.resources.configuration + config.densityDpi = 12 + config.smallestScreenWidthDp = 240 + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the config is updated + config.densityDpi = 20 + config.smallestScreenWidthDp = 300 + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.changedConfig?.densityDpi).isEqualTo(20) + assertThat(listener.changedConfig?.smallestScreenWidthDp).isEqualTo(300) + } + + @Test + fun densityChanged_listenerNotified() { + val config = mContext.resources.configuration + config.densityDpi = 12 + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the density is updated + config.densityDpi = 20 + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.densityOrFontScaleChanged).isTrue() + } + + @Test + fun fontChanged_listenerNotified() { + val config = mContext.resources.configuration + config.fontScale = 1.5f + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the font is updated + config.fontScale = 1.4f + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.densityOrFontScaleChanged).isTrue() + } + + @Test + fun isCarAndUiModeChanged_densityListenerNotified() { + val config = mContext.resources.configuration + config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_YES + // Re-create the controller since we calculate car mode on creation + mConfigurationController = ConfigurationControllerImpl(mContext) + + val listener = createAndAddListener() + + // WHEN the ui mode is updated + config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_NO + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.densityOrFontScaleChanged).isTrue() + } + + @Test + fun isNotCarAndUiModeChanged_densityListenerNotNotified() { + val config = mContext.resources.configuration + config.uiMode = UI_MODE_NIGHT_YES + // Re-create the controller since we calculate car mode on creation + mConfigurationController = ConfigurationControllerImpl(mContext) + + val listener = createAndAddListener() + + // WHEN the ui mode is updated + config.uiMode = UI_MODE_NIGHT_NO + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is not notified because it's not car mode + assertThat(listener.densityOrFontScaleChanged).isFalse() + } + + @Test + fun smallestScreenWidthChanged_listenerNotified() { + val config = mContext.resources.configuration + config.smallestScreenWidthDp = 240 + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the width is updated + config.smallestScreenWidthDp = 300 + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.smallestScreenWidthChanged).isTrue() + } + + @Test + fun maxBoundsChange_newConfigObject_listenerNotified() { + val config = mContext.resources.configuration + config.windowConfiguration.setMaxBounds(0, 0, 200, 200) + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN a new configuration object with new bounds is sent + val newConfig = Configuration() + newConfig.windowConfiguration.setMaxBounds(0, 0, 100, 100) + mConfigurationController.onConfigurationChanged(newConfig) + + // THEN the listener is notified + assertThat(listener.maxBoundsChanged).isTrue() + } + + // Regression test for b/245799099 + @Test + fun maxBoundsChange_sameObject_listenerNotified() { + val config = mContext.resources.configuration + config.windowConfiguration.setMaxBounds(0, 0, 200, 200) + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the existing config is updated with new bounds + config.windowConfiguration.setMaxBounds(0, 0, 100, 100) + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.maxBoundsChanged).isTrue() + } + + + @Test + fun localeListChanged_listenerNotified() { + val config = mContext.resources.configuration + config.locales = LocaleList(Locale.CANADA, Locale.GERMANY) + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the locales are updated + config.locales = LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE) + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.localeListChanged).isTrue() + } + + @Test + fun uiModeChanged_listenerNotified() { + val config = mContext.resources.configuration + config.uiMode = UI_MODE_NIGHT_YES + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the ui mode is updated + config.uiMode = UI_MODE_NIGHT_NO + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.uiModeChanged).isTrue() + } + + @Test + fun layoutDirectionUpdated_listenerNotified() { + val config = mContext.resources.configuration + config.screenLayout = SCREENLAYOUT_LAYOUTDIR_LTR + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the layout is updated + config.screenLayout = SCREENLAYOUT_LAYOUTDIR_RTL + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.layoutDirectionChanged).isTrue() + } + + @Test + fun assetPathsUpdated_listenerNotified() { + val config = mContext.resources.configuration + config.assetsSeq = 45 + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN the assets sequence is updated + config.assetsSeq = 46 + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified + assertThat(listener.themeChanged).isTrue() + } + + @Test + fun multipleUpdates_listenerNotifiedOfAll() { + val config = mContext.resources.configuration + config.densityDpi = 14 + config.windowConfiguration.setMaxBounds(0, 0, 2, 2) + config.uiMode = UI_MODE_NIGHT_YES + mConfigurationController.onConfigurationChanged(config) + + val listener = createAndAddListener() + + // WHEN multiple fields are updated + config.densityDpi = 20 + config.windowConfiguration.setMaxBounds(0, 0, 3, 3) + config.uiMode = UI_MODE_NIGHT_NO + mConfigurationController.onConfigurationChanged(config) + + // THEN the listener is notified of all of them + assertThat(listener.densityOrFontScaleChanged).isTrue() + assertThat(listener.maxBoundsChanged).isTrue() + assertThat(listener.uiModeChanged).isTrue() + } + + @Test + fun equivalentConfigObject_listenerNotNotified() { + val config = mContext.resources.configuration + val listener = createAndAddListener() + + // WHEN we update with the new object that has all the same fields + mConfigurationController.onConfigurationChanged(Configuration(config)) + + listener.assertNoMethodsCalled() + } + + private fun createAndAddListener(): TestListener { + val listener = TestListener() + mConfigurationController.addCallback(listener) + // Adding a listener can trigger some callbacks, so we want to reset the values right + // after the listener is added + listener.reset() + return listener + } + + private class TestListener : ConfigurationListener { + var changedConfig: Configuration? = null + var densityOrFontScaleChanged = false + var smallestScreenWidthChanged = false + var maxBoundsChanged = false + var uiModeChanged = false + var themeChanged = false + var localeListChanged = false + var layoutDirectionChanged = false + + override fun onConfigChanged(newConfig: Configuration?) { + changedConfig = newConfig + } + override fun onDensityOrFontScaleChanged() { + densityOrFontScaleChanged = true + } + override fun onSmallestScreenWidthChanged() { + smallestScreenWidthChanged = true + } + override fun onMaxBoundsChanged() { + maxBoundsChanged = true + } + override fun onUiModeChanged() { + uiModeChanged = true + } + override fun onThemeChanged() { + themeChanged = true + } + override fun onLocaleListChanged() { + localeListChanged = true + } + override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { + layoutDirectionChanged = true + } + + fun assertNoMethodsCalled() { + assertThat(densityOrFontScaleChanged).isFalse() + assertThat(smallestScreenWidthChanged).isFalse() + assertThat(maxBoundsChanged).isFalse() + assertThat(uiModeChanged).isFalse() + assertThat(themeChanged).isFalse() + assertThat(localeListChanged).isFalse() + assertThat(layoutDirectionChanged).isFalse() + } + + fun reset() { + changedConfig = null + densityOrFontScaleChanged = false + smallestScreenWidthChanged = false + maxBoundsChanged = false + uiModeChanged = false + themeChanged = false + localeListChanged = false + layoutDirectionChanged = false + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index 6ec5cf82a81e..eb0b9b3a3fb1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -56,14 +56,12 @@ import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserInfoTracker; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController; -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherFeatureController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; @@ -112,16 +110,12 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { private StatusBarContentInsetsProvider mStatusBarContentInsetsProvider; @Mock private UserManager mUserManager; + @Mock + private StatusBarUserChipViewModel mStatusBarUserChipViewModel; @Captor private ArgumentCaptor<ConfigurationListener> mConfigurationListenerCaptor; @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardCallbackCaptor; - @Mock - private StatusBarUserSwitcherFeatureController mStatusBarUserSwitcherFeatureController; - @Mock - private StatusBarUserSwitcherController mStatusBarUserSwitcherController; - @Mock - private StatusBarUserInfoTracker mStatusBarUserInfoTracker; @Mock private SecureSettings mSecureSettings; @Mock private CommandQueue mCommandQueue; @Mock private KeyguardLogger mLogger; @@ -169,9 +163,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mStatusBarStateController, mStatusBarContentInsetsProvider, mUserManager, - mStatusBarUserSwitcherFeatureController, - mStatusBarUserSwitcherController, - mStatusBarUserInfoTracker, + mStatusBarUserChipViewModel, mSecureSettings, mCommandQueue, mFakeExecutor, @@ -479,8 +471,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { @Test public void testNewUserSwitcherDisablesAvatar_newUiOn() { // GIVEN the status bar user switcher chip is enabled - when(mStatusBarUserSwitcherFeatureController.isStatusBarUserSwitcherFeatureEnabled()) - .thenReturn(true); + when(mStatusBarUserChipViewModel.getChipEnabled()).thenReturn(true); // WHEN the controller is created mController = createController(); @@ -492,8 +483,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { @Test public void testNewUserSwitcherDisablesAvatar_newUiOff() { // GIVEN the status bar user switcher chip is disabled - when(mStatusBarUserSwitcherFeatureController.isStatusBarUserSwitcherFeatureEnabled()) - .thenReturn(false); + when(mStatusBarUserChipViewModel.getChipEnabled()).thenReturn(false); // WHEN the controller is created mController = createController(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index a61fba5c4000..320a08315843 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -27,11 +27,11 @@ import androidx.test.platform.app.InstrumentationRegistry import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.shade.NotificationPanelViewController -import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.unfold.SysUIUnfoldComponent import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider +import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel import com.android.systemui.util.mockito.any import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -64,7 +64,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Mock private lateinit var configurationController: ConfigurationController @Mock - private lateinit var userSwitcherController: StatusBarUserSwitcherController + private lateinit var userChipViewModel: StatusBarUserChipViewModel @Mock private lateinit var viewUtil: ViewUtil @@ -79,14 +79,13 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { `when`(notificationPanelViewController.view).thenReturn(panelView) `when`(sysuiUnfoldComponent.getStatusBarMoveFromCenterAnimationController()) .thenReturn(moveFromCenterAnimation) - // create the view on main thread as it requires main looper + // create the view and controller on main thread as it requires main looper InstrumentationRegistry.getInstrumentation().runOnMainSync { val parent = FrameLayout(mContext) // add parent to keep layout params view = LayoutInflater.from(mContext) .inflate(R.layout.status_bar, parent, false) as PhoneStatusBarView + controller = createAndInitController(view) } - - controller = createAndInitController(view) } @Test @@ -106,7 +105,10 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { val view = createViewMock() val argumentCaptor = ArgumentCaptor.forClass(OnPreDrawListener::class.java) unfoldConfig.isEnabled = true - controller = createAndInitController(view) + // create the controller on main thread as it requires main looper + InstrumentationRegistry.getInstrumentation().runOnMainSync { + controller = createAndInitController(view) + } verify(view.viewTreeObserver).addOnPreDrawListener(argumentCaptor.capture()) argumentCaptor.value.onPreDraw() @@ -126,7 +128,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { return PhoneStatusBarViewController.Factory( Optional.of(sysuiUnfoldComponent), Optional.of(progressProvider), - userSwitcherController, + userChipViewModel, viewUtil, configurationController ).create(view, touchEventHandler).also { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt index e86676b81f8e..1759fb794bd6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt @@ -19,9 +19,9 @@ package com.android.systemui.statusbar.phone import android.content.Context import android.content.res.Configuration import android.graphics.Rect -import android.test.suitebuilder.annotation.SmallTest import android.view.Display import android.view.DisplayCutout +import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.policy.ConfigurationController @@ -463,16 +463,10 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val provider = StatusBarContentInsetsProvider(contextMock, configurationController, mock(DumpManager::class.java)) - givenDisplay( - screenBounds = Rect(0, 0, 1080, 2160), - displayUniqueId = "1" - ) + configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160) val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) - givenDisplay( - screenBounds = Rect(0, 0, 800, 600), - displayUniqueId = "2" - ) - configurationController.onConfigurationChanged(configuration) + + configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600) // WHEN: get insets on the second display val secondDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) @@ -487,23 +481,15 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { // get insets and switch back val provider = StatusBarContentInsetsProvider(contextMock, configurationController, mock(DumpManager::class.java)) - givenDisplay( - screenBounds = Rect(0, 0, 1080, 2160), - displayUniqueId = "1" - ) + + configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160) val firstDisplayInsetsFirstCall = provider .getStatusBarContentAreaForRotation(ROTATION_NONE) - givenDisplay( - screenBounds = Rect(0, 0, 800, 600), - displayUniqueId = "2" - ) - configurationController.onConfigurationChanged(configuration) + + configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600) provider.getStatusBarContentAreaForRotation(ROTATION_NONE) - givenDisplay( - screenBounds = Rect(0, 0, 1080, 2160), - displayUniqueId = "1" - ) - configurationController.onConfigurationChanged(configuration) + + configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160) // WHEN: get insets on the first display again val firstDisplayInsetsSecondCall = provider @@ -513,9 +499,70 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { assertThat(firstDisplayInsetsFirstCall).isEqualTo(firstDisplayInsetsSecondCall) } - private fun givenDisplay(screenBounds: Rect, displayUniqueId: String) { - `when`(display.uniqueId).thenReturn(displayUniqueId) - configuration.windowConfiguration.maxBounds = screenBounds + // Regression test for b/245799099 + @Test + fun onMaxBoundsChanged_listenerNotified() { + // Start out with an existing configuration with bounds + configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100) + configurationController.onConfigurationChanged(configuration) + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(DumpManager::class.java)) + val listener = object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + // WHEN the config is updated with new bounds + configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789) + configurationController.onConfigurationChanged(configuration) + + // THEN the listener is notified + assertThat(listener.triggered).isTrue() + } + + @Test + fun onDensityOrFontScaleChanged_listenerNotified() { + configuration.densityDpi = 12 + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(DumpManager::class.java)) + val listener = object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + // WHEN the config is updated + configuration.densityDpi = 20 + configurationController.onConfigurationChanged(configuration) + + // THEN the listener is notified + assertThat(listener.triggered).isTrue() + } + + @Test + fun onThemeChanged_listenerNotified() { + val provider = StatusBarContentInsetsProvider(contextMock, configurationController, + mock(DumpManager::class.java)) + val listener = object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + configurationController.notifyThemeChanged() + + // THEN the listener is notified + assertThat(listener.triggered).isTrue() } private fun assertRects( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt deleted file mode 100644 index eba3b04f3472..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt +++ /dev/null @@ -1,102 +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.statusbar.phone.userswitcher - -import android.content.Intent -import android.os.UserHandle -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper -@SmallTest -class StatusBarUserSwitcherControllerOldImplTest : SysuiTestCase() { - @Mock - private lateinit var tracker: StatusBarUserInfoTracker - - @Mock - private lateinit var featureController: StatusBarUserSwitcherFeatureController - - @Mock - private lateinit var userSwitcherDialogController: UserSwitchDialogController - - @Mock - private lateinit var featureFlags: FeatureFlags - - @Mock - private lateinit var activityStarter: ActivityStarter - - @Mock - private lateinit var falsingManager: FalsingManager - - private lateinit var statusBarUserSwitcherContainer: StatusBarUserSwitcherContainer - private lateinit var controller: StatusBarUserSwitcherControllerImpl - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - statusBarUserSwitcherContainer = StatusBarUserSwitcherContainer(mContext, null) - statusBarUserSwitcherContainer - controller = StatusBarUserSwitcherControllerImpl( - statusBarUserSwitcherContainer, - tracker, - featureController, - userSwitcherDialogController, - featureFlags, - activityStarter, - falsingManager - ) - controller.init() - controller.onViewAttached() - } - - @Test - fun testFalsingManager() { - statusBarUserSwitcherContainer.callOnClick() - verify(falsingManager).isFalseTap(FalsingManager.LOW_PENALTY) - } - - @Test - fun testStartActivity() { - `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(false) - statusBarUserSwitcherContainer.callOnClick() - verify(userSwitcherDialogController).showDialog(any(), any()) - `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(true) - statusBarUserSwitcherContainer.callOnClick() - verify(activityStarter).startActivity(any(Intent::class.java), - eq(true) /* dismissShade */, - eq(null) /* animationController */, - eq(true) /* showOverLockscreenWhenLocked */, - eq(UserHandle.SYSTEM)) - } -} 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 8fb98c12d6ff..4b49420c99be 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 @@ -19,6 +19,7 @@ package com.android.systemui.user.domain.interactor import android.app.ActivityManager import android.app.admin.DevicePolicyManager +import android.content.ComponentName import android.content.Intent import android.content.pm.UserInfo import android.graphics.Bitmap @@ -33,7 +34,10 @@ import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter @@ -41,6 +45,7 @@ import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.UserSwitcherActivity import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.source.UserRecord @@ -48,9 +53,11 @@ import com.android.systemui.user.domain.model.ShowDialogRequestModel import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel 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.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers @@ -90,6 +97,7 @@ class UserInteractorTest : SysuiTestCase() { private lateinit var userRepository: FakeUserRepository private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var telephonyRepository: FakeTelephonyRepository + private lateinit var featureFlags: FakeFeatureFlags @Before fun setUp() { @@ -104,6 +112,7 @@ class UserInteractorTest : SysuiTestCase() { SUPERVISED_USER_CREATION_APP_PACKAGE, ) + featureFlags = FakeFeatureFlags() userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() telephonyRepository = FakeTelephonyRepository() @@ -147,7 +156,8 @@ class UserInteractorTest : SysuiTestCase() { uiEventLogger = uiEventLogger, resumeSessionReceiver = resumeSessionReceiver, resetOrExitSessionReceiver = resetOrExitSessionReceiver, - ) + ), + featureFlags = featureFlags, ) } @@ -672,6 +682,95 @@ class UserInteractorTest : SysuiTestCase() { ) } + @Test + fun `users - secondary user - no guest user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var res: List<UserModel>? = null + val job = underTest.users.onEach { res = it }.launchIn(this) + assertThat(res?.size == 2).isTrue() + assertThat(res?.find { it.isGuest }).isNull() + job.cancel() + } + + @Test + fun `users - secondary user - no guest action`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var res: List<UserActionModel>? = null + val job = underTest.actions.onEach { res = it }.launchIn(this) + assertThat(res?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull() + job.cancel() + } + + @Test + fun `users - secondary user - no guest user record`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var res: List<UserRecord>? = null + val job = underTest.userRecords.onEach { res = it }.launchIn(this) + assertThat(res?.find { it.isGuest }).isNull() + job.cancel() + } + + @Test + fun `show user switcher - full screen disabled - shows dialog switcher`() = + runBlocking(IMMEDIATE) { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false) + + var dialogRequest: ShowDialogRequestModel? = null + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) + + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + // Dialog is shown. + assertThat(dialogRequest).isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog) + + underTest.onDialogShown() + assertThat(dialogRequest).isNull() + + job.cancel() + } + + @Test + fun `show user switcher - full screen enabled - launches activity`() { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) + + // Dialog is shown. + val intentCaptor = argumentCaptor<Intent>() + verify(activityStarter) + .startActivity( + intentCaptor.capture(), + /* dismissShade= */ eq(true), + /* ActivityLaunchAnimator.Controller= */ nullable(), + /* showOverLockscreenWhenLocked= */ eq(true), + eq(UserHandle.SYSTEM), + ) + assertThat(intentCaptor.value.component) + .isEqualTo( + ComponentName( + context, + UserSwitcherActivity::class.java, + ) + ) + } + private fun assertUsers( models: List<UserModel>?, count: Int, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt new file mode 100644 index 000000000000..db348b8029a0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -0,0 +1,306 @@ +/* + * 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.user.ui.viewmodel + +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.systemui.GuestResetOrExitSessionReceiver +import com.android.systemui.GuestResumeSessionReceiver +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.RefreshUsersScheduler +import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.yield +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.doAnswer +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class StatusBarUserChipViewModelTest : SysuiTestCase() { + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var activityManager: ActivityManager + @Mock private lateinit var manager: UserManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver + @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver + + private lateinit var underTest: StatusBarUserChipViewModel + + private val userRepository = FakeUserRepository() + private val keyguardRepository = FakeKeyguardRepository() + private val featureFlags = FakeFeatureFlags() + private lateinit var guestUserInteractor: GuestUserInteractor + private lateinit var refreshUsersScheduler: RefreshUsersScheduler + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + doAnswer { invocation -> + val userId = invocation.arguments[0] as Int + when (userId) { + USER_ID_0 -> return@doAnswer USER_IMAGE_0 + USER_ID_1 -> return@doAnswer USER_IMAGE_1 + USER_ID_2 -> return@doAnswer USER_IMAGE_2 + else -> return@doAnswer mock<Bitmap>() + } + } + .`when`(manager) + .getUserIcon(anyInt()) + + userRepository.isStatusBarUserChipEnabled = true + + refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = testScope.backgroundScope, + mainDispatcher = testDispatcher, + repository = userRepository, + ) + guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = testScope.backgroundScope, + mainDispatcher = testDispatcher, + backgroundDispatcher = testDispatcher, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, + resumeSessionReceiver = resumeSessionReceiver, + resetOrExitSessionReceiver = resetOrExitSessionReceiver, + ) + + underTest = viewModel() + } + + @Test + fun `config is false - chip is disabled`() { + // the enabled bit is set at SystemUI startup, so recreate the view model here + userRepository.isStatusBarUserChipEnabled = false + underTest = viewModel() + + assertThat(underTest.chipEnabled).isFalse() + } + + @Test + fun `config is true - chip is enabled`() { + // the enabled bit is set at SystemUI startup, so recreate the view model here + userRepository.isStatusBarUserChipEnabled = true + underTest = viewModel() + + assertThat(underTest.chipEnabled).isTrue() + } + + @Test + fun `should show chip criteria - single user`() = + testScope.runTest { + userRepository.setUserInfos(listOf(USER_0)) + userRepository.setSelectedUserInfo(USER_0) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + val values = mutableListOf<Boolean>() + + val job = launch { underTest.isChipVisible.toList(values) } + advanceUntilIdle() + + assertThat(values).containsExactly(false) + + job.cancel() + } + + @Test + fun `should show chip criteria - multiple users`() = + testScope.runTest { + setMultipleUsers() + + var latest: Boolean? = null + val job = underTest.isChipVisible.onEach { latest = it }.launchIn(this) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun `user chip name - shows selected user info`() = + testScope.runTest { + setMultipleUsers() + + var latest: Text? = null + val job = underTest.userName.onEach { latest = it }.launchIn(this) + + userRepository.setSelectedUserInfo(USER_0) + assertThat(latest).isEqualTo(USER_NAME_0) + + userRepository.setSelectedUserInfo(USER_1) + assertThat(latest).isEqualTo(USER_NAME_1) + + userRepository.setSelectedUserInfo(USER_2) + assertThat(latest).isEqualTo(USER_NAME_2) + + job.cancel() + } + + @Test + fun `user chip avatar - shows selected user info`() = + testScope.runTest { + setMultipleUsers() + + // A little hacky. System server passes us bitmaps and we wrap them in the interactor. + // Unwrap them to make sure we're always tracking the current user's bitmap + var latest: Bitmap? = null + val job = + underTest.userAvatar + .onEach { + if (it !is BitmapDrawable) { + latest = null + } + + latest = (it as BitmapDrawable).bitmap + } + .launchIn(this) + + userRepository.setSelectedUserInfo(USER_0) + assertThat(latest).isEqualTo(USER_IMAGE_0) + + userRepository.setSelectedUserInfo(USER_1) + assertThat(latest).isEqualTo(USER_IMAGE_1) + + userRepository.setSelectedUserInfo(USER_2) + assertThat(latest).isEqualTo(USER_IMAGE_2) + + job.cancel() + } + + private fun viewModel(): StatusBarUserChipViewModel { + return StatusBarUserChipViewModel( + context = context, + interactor = + UserInteractor( + applicationContext = context, + repository = userRepository, + activityStarter = activityStarter, + keyguardInteractor = + KeyguardInteractor( + repository = keyguardRepository, + ), + featureFlags = featureFlags, + manager = manager, + applicationScope = testScope.backgroundScope, + telephonyInteractor = + TelephonyInteractor( + repository = FakeTelephonyRepository(), + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = testDispatcher, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = guestUserInteractor, + ) + ) + } + + private suspend fun setMultipleUsers() { + userRepository.setUserInfos(listOf(USER_0, USER_1, USER_2)) + userRepository.setSelectedUserInfo(USER_0) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + } + + companion object { + private const val USER_ID_0 = 0 + private val USER_IMAGE_0 = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val USER_NAME_0 = Text.Loaded("zero") + + private const val USER_ID_1 = 1 + private val USER_IMAGE_1 = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val USER_NAME_1 = Text.Loaded("one") + + private const val USER_ID_2 = 2 + private val USER_IMAGE_2 = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val USER_NAME_2 = Text.Loaded("two") + + private val USER_0 = + UserInfo( + USER_ID_0, + USER_NAME_0.text!!, + /* iconPath */ "", + /* flags */ 0, + /* userType */ UserManager.USER_TYPE_FULL_SYSTEM + ) + + private val USER_1 = + UserInfo( + USER_ID_1, + USER_NAME_1.text!!, + /* iconPath */ "", + /* flags */ 0, + /* userType */ UserManager.USER_TYPE_FULL_SYSTEM + ) + + private val USER_2 = + UserInfo( + USER_ID_2, + USER_NAME_2.text!!, + /* iconPath */ "", + /* flags */ 0, + /* userType */ UserManager.USER_TYPE_FULL_SYSTEM + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index db136800a3cc..eac7fc21e505 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter @@ -147,6 +148,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { KeyguardInteractor( repository = keyguardRepository, ), + featureFlags = FakeFeatureFlags(), manager = manager, applicationScope = injectedScope, telephonyInteractor = diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 325da4ead666..63448e236867 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -28,8 +28,6 @@ import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.FeatureFlags import com.android.systemui.globalactions.GlobalActionsDialogLite import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -43,7 +41,6 @@ import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel -import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.security.data.repository.SecurityRepository import com.android.systemui.security.data.repository.SecurityRepositoryImpl import com.android.systemui.settings.FakeUserTracker @@ -54,6 +51,7 @@ import com.android.systemui.statusbar.policy.FakeUserInfoController import com.android.systemui.statusbar.policy.SecurityController import com.android.systemui.statusbar.policy.UserInfoController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings @@ -97,13 +95,12 @@ class FooterActionsTestUtils( /** Create a [FooterActionsInteractor] to be used in tests. */ fun footerActionsInteractor( activityStarter: ActivityStarter = mock(), - featureFlags: FeatureFlags = FakeFeatureFlags(), metricsLogger: MetricsLogger = FakeMetricsLogger(), uiEventLogger: UiEventLogger = UiEventLoggerFake(), deviceProvisionedController: DeviceProvisionedController = mock(), qsSecurityFooterUtils: QSSecurityFooterUtils = mock(), fgsManagerController: FgsManagerController = mock(), - userSwitchDialogController: UserSwitchDialogController = mock(), + userInteractor: UserInteractor = mock(), securityRepository: SecurityRepository = securityRepository(), foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(), userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(), @@ -112,13 +109,12 @@ class FooterActionsTestUtils( ): FooterActionsInteractor { return FooterActionsInteractorImpl( activityStarter, - featureFlags, metricsLogger, uiEventLogger, deviceProvisionedController, qsSecurityFooterUtils, fgsManagerController, - userSwitchDialogController, + userInteractor, securityRepository, foregroundServicesRepository, userSwitcherRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index b7c8cbf40bea..ea5a302ce05a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -49,6 +49,8 @@ class FakeUserRepository : UserRepository { override val isGuestUserCreationScheduled = AtomicBoolean() + override var isStatusBarUserChipEnabled: Boolean = false + override var secondaryUserId: Int = UserHandle.USER_NULL override var isRefreshUsersPaused: Boolean = false diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java index 33ece0084906..21e16a1e7be4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java @@ -16,7 +16,6 @@ package com.android.systemui.util; -import android.content.Context; import android.provider.DeviceConfig; import android.provider.DeviceConfig.OnPropertiesChangedListener; import android.provider.DeviceConfig.Properties; @@ -83,7 +82,7 @@ public class DeviceConfigProxyFake extends DeviceConfigProxy { } @Override - public void enforceReadPermission(Context context, String namespace) { + public void enforceReadPermission(String namespace) { // no-op } diff --git a/packages/overlays/DisplayCutoutEmulationHoleOverlay/res/values-en-rCA/strings.xml b/packages/overlays/DisplayCutoutEmulationHoleOverlay/res/values-en-rCA/strings.xml index 9db960fe68c1..e7ec332a6f7a 100644 --- a/packages/overlays/DisplayCutoutEmulationHoleOverlay/res/values-en-rCA/strings.xml +++ b/packages/overlays/DisplayCutoutEmulationHoleOverlay/res/values-en-rCA/strings.xml @@ -17,5 +17,5 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="display_cutout_emulation_overlay" msgid="7305489596221077240">"Punch hole cutout"</string> + <string name="display_cutout_emulation_overlay" msgid="7305489596221077240">"Punch Hole cutout"</string> </resources> diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java index 3ea1bcbbbcd9..7d8bb51ff586 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java @@ -114,7 +114,7 @@ public class AppWidgetXmlUtil { info.minWidth = parser.getAttributeInt(null, ATTR_MIN_WIDTH, 0); info.minHeight = parser.getAttributeInt(null, ATTR_MIN_HEIGHT, 0); info.minResizeWidth = parser.getAttributeInt(null, ATTR_MIN_RESIZE_WIDTH, 0); - info.minResizeWidth = parser.getAttributeInt(null, ATTR_MIN_RESIZE_HEIGHT, 0); + info.minResizeHeight = parser.getAttributeInt(null, ATTR_MIN_RESIZE_HEIGHT, 0); info.maxResizeWidth = parser.getAttributeInt(null, ATTR_MAX_RESIZE_WIDTH, 0); info.maxResizeHeight = parser.getAttributeInt(null, ATTR_MAX_RESIZE_HEIGHT, 0); info.targetCellWidth = parser.getAttributeInt(null, ATTR_TARGET_CELL_WIDTH, 0); diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index be2107529f8b..fbde9e0ea5d1 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -227,6 +227,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return mParams.getName(); } + /** Returns the policy specified for this policy type */ + public @VirtualDeviceParams.DevicePolicy int getDevicePolicy( + @VirtualDeviceParams.PolicyType int policyType) { + return mParams.getDevicePolicy(policyType); + } + /** Returns the unique device ID of this device. */ @Override // Binder call public int getDeviceId() { 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 c400a74da4ce..a8797a05ed24 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -233,6 +233,13 @@ public class VirtualDeviceManagerService extends SystemService { mLocalService.onAppsOnVirtualDeviceChanged(); } + @VisibleForTesting + void addVirtualDevice(VirtualDeviceImpl virtualDevice) { + synchronized (mVirtualDeviceManagerLock) { + mVirtualDevices.put(virtualDevice.getAssociationId(), virtualDevice); + } + } + class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub implements VirtualDeviceImpl.PendingTrampolineCallback { @@ -358,6 +365,12 @@ public class VirtualDeviceManagerService extends SystemService { return virtualDevices; } + @Override // BinderCall + @VirtualDeviceParams.DevicePolicy + public int getDevicePolicy(int deviceId, @VirtualDeviceParams.PolicyType int policyType) { + return mLocalService.getDevicePolicy(deviceId, policyType); + } + @Nullable private AssociationInfo getAssociationInfo(String packageName, int associationId) { final int callingUserId = getCallingUserHandle().getIdentifier(); @@ -439,6 +452,20 @@ public class VirtualDeviceManagerService extends SystemService { } @Override + @VirtualDeviceParams.DevicePolicy + public int getDevicePolicy(int deviceId, @VirtualDeviceParams.PolicyType int policyType) { + synchronized (mVirtualDeviceManagerLock) { + for (int i = 0; i < mVirtualDevices.size(); i++) { + final VirtualDeviceImpl device = mVirtualDevices.valueAt(i); + if (device.getDeviceId() == deviceId) { + return device.getDevicePolicy(policyType); + } + } + } + return VirtualDeviceParams.DEVICE_POLICY_DEFAULT; + } + + @Override public void onVirtualDisplayCreated(int displayId) { final VirtualDisplayListener[] listeners; synchronized (mVirtualDeviceManagerLock) { diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 2662e03a5627..544dd4e6dcff 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -26,16 +26,21 @@ import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.IBackgroundInstallControlService; import android.content.pm.InstallSourceInfo; import android.content.pm.ModuleInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; +import android.content.pm.SharedLibraryInfo; import android.content.pm.Signature; import android.content.pm.SigningDetails; import android.content.pm.SigningInfo; +import android.content.pm.parsing.result.ParseInput; import android.content.pm.parsing.result.ParseResult; import android.content.pm.parsing.result.ParseTypeImpl; import android.os.Build; +import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; @@ -43,6 +48,8 @@ import android.os.ServiceManager; import android.os.ShellCallback; import android.os.ShellCommand; import android.os.SystemProperties; +import android.os.UserHandle; +import android.text.TextUtils; import android.util.PackageUtils; import android.util.Slog; import android.util.apk.ApkSignatureVerifier; @@ -54,14 +61,16 @@ import com.android.internal.util.FrameworkStatsLog; import libcore.util.HexEncoding; +import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.security.PublicKey; import java.security.cert.CertificateException; import java.util.ArrayList; -import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executors; import java.util.stream.Collectors; @@ -82,10 +91,36 @@ public class BinaryTransparencyService extends SystemService { @VisibleForTesting static final String BINARY_HASH_ERROR = "SHA256HashError"; + static final int MEASURE_APEX_AND_MODULES = 1; + static final int MEASURE_PRELOADS = 2; + static final int MEASURE_NEW_MBAS = 3; + + static final long RECORD_MEASUREMENTS_COOLDOWN_MS = 24 * 60 * 60 * 1000; + + @VisibleForTesting + static final String BUNDLE_PACKAGE_INFO = "package-info"; + @VisibleForTesting + static final String BUNDLE_CONTENT_DIGEST_ALGORITHM = "content-digest-algo"; + @VisibleForTesting + static final String BUNDLE_CONTENT_DIGEST = "content-digest"; + + // used for indicating any type of error during MBA measurement + static final int MBA_STATUS_ERROR = 0; + // used for indicating factory condition preloads + static final int MBA_STATUS_PRELOADED = 1; + // used for indicating preloaded apps that are updated + static final int MBA_STATUS_UPDATED_PRELOAD = 2; + // used for indicating newly installed MBAs + static final int MBA_STATUS_NEW_INSTALL = 3; + // used for indicating newly installed MBAs that are updated (but unused currently) + static final int MBA_STATUS_UPDATED_NEW_INSTALL = 4; + + private static final boolean DEBUG = true; // set this to false upon submission + private final Context mContext; private String mVbmetaDigest; - private HashMap<String, String> mBinaryHashes; - private HashMap<String, Long> mBinaryLastUpdateTimes; + // the system time (in ms) the last measurement was taken + private long mMeasurementsLastRecordedMs; final class BinaryTransparencyServiceImpl extends IBinaryTransparencyService.Stub { @@ -95,25 +130,298 @@ public class BinaryTransparencyService extends SystemService { } @Override - public Map getApexInfo() { - HashMap results = new HashMap(); - if (!updateBinaryMeasurements()) { - Slog.e(TAG, "Error refreshing APEX measurements."); - return results; + public List getApexInfo() { + List<Bundle> results = new ArrayList<>(); + + for (PackageInfo packageInfo : getCurrentInstalledApexs()) { + Bundle apexMeasurement = measurePackage(packageInfo); + results.add(apexMeasurement); } + + return results; + } + + /** + * A helper function to compute the SHA256 digest of APK package signer. + * @param signingInfo The signingInfo of a package, usually {@link PackageInfo#signingInfo}. + * @return an array of {@code String} representing hex encoded string of the + * SHA256 digest of APK signer(s). The number of signers will be reflected by the + * size of the array. + * However, {@code null} is returned if there is any error. + */ + private String[] computePackageSignerSha256Digests(@Nullable SigningInfo signingInfo) { + if (signingInfo == null) { + Slog.e(TAG, "signingInfo is null"); + return null; + } + + Signature[] packageSigners = signingInfo.getApkContentsSigners(); + List<String> resultList = new ArrayList<>(); + for (Signature packageSigner : packageSigners) { + byte[] digest = PackageUtils.computeSha256DigestBytes(packageSigner.toByteArray()); + String digestHexString = HexEncoding.encodeToString(digest, false); + resultList.add(digestHexString); + } + return resultList.toArray(new String[1]); + } + + /** + * Perform basic measurement (i.e. content digest) on a given package. + * @param packageInfo The package to be measured. + * @return a {@link android.os.Bundle} that packs the measurement result with the following + * keys: {@link #BUNDLE_PACKAGE_INFO}, + * {@link #BUNDLE_CONTENT_DIGEST_ALGORITHM} + * {@link #BUNDLE_CONTENT_DIGEST} + */ + private @NonNull Bundle measurePackage(PackageInfo packageInfo) { + Bundle result = new Bundle(); + + // compute content digest + if (DEBUG) { + Slog.d(TAG, "Computing content digest for " + packageInfo.packageName + " at " + + packageInfo.applicationInfo.sourceDir); + } + Map<Integer, byte[]> contentDigests = computeApkContentDigest( + packageInfo.applicationInfo.sourceDir); + result.putParcelable(BUNDLE_PACKAGE_INFO, packageInfo); + if (contentDigests == null) { + Slog.d(TAG, "Failed to compute content digest for " + + packageInfo.applicationInfo.sourceDir); + result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0); + result.putByteArray(BUNDLE_CONTENT_DIGEST, null); + return result; + } + + // in this iteration, we'll be supporting only 2 types of digests: + // CHUNKED_SHA256 and CHUNKED_SHA512. + // And only one of them will be available per package. + if (contentDigests.containsKey(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)) { + Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256; + result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId); + result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId)); + } else if (contentDigests.containsKey( + ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)) { + Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512; + result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId); + result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId)); + } else { + // TODO(b/259423111): considering putting the raw values for the algorithm & digest + // into the bundle to track potential other digest algorithms that may be in use + result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0); + result.putByteArray(BUNDLE_CONTENT_DIGEST, null); + } + + return result; + } + + + /** + * Measures and records digests for *all* covered binaries/packages. + * + * This method will be called in a Job scheduled to take measurements periodically. + * + * Packages that are covered so far are: + * - all APEXs (introduced in Android T) + * - all mainline modules (introduced in Android T) + * - all preloaded apps and their update(s) (new in Android U) + * - dynamically installed mobile bundled apps (MBAs) (new in Android U) + * + * @return a {@code List<Bundle>}. Each Bundle item contains values as + * defined by the return value of {@link #measurePackage(PackageInfo)}. + */ + public List getMeasurementsForAllPackages() { + List<Bundle> results = new ArrayList<>(); PackageManager pm = mContext.getPackageManager(); - if (pm == null) { - Slog.e(TAG, "Error obtaining an instance of PackageManager."); - return results; + Set<String> packagesMeasured = new HashSet<>(); + + // check if we should record the resulting measurements + long currentTimeMs = System.currentTimeMillis(); + boolean record = false; + if ((currentTimeMs - mMeasurementsLastRecordedMs) >= RECORD_MEASUREMENTS_COOLDOWN_MS) { + Slog.d(TAG, "Measurement was last taken at " + mMeasurementsLastRecordedMs + + " and is now updated to: " + currentTimeMs); + mMeasurementsLastRecordedMs = currentTimeMs; + record = true; + } + + // measure all APEXs first + if (DEBUG) { + Slog.d(TAG, "Measuring APEXs..."); + } + for (PackageInfo packageInfo : getCurrentInstalledApexs()) { + packagesMeasured.add(packageInfo.packageName); + + Bundle apexMeasurement = measurePackage(packageInfo); + results.add(apexMeasurement); + + if (record) { + // compute digests of signing info + String[] signerDigestHexStrings = computePackageSignerSha256Digests( + packageInfo.signingInfo); + + // log to Westworld + FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED, + packageInfo.packageName, + packageInfo.getLongVersionCode(), + HexEncoding.encodeToString(apexMeasurement.getByteArray( + BUNDLE_CONTENT_DIGEST), false), + apexMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM), + signerDigestHexStrings); + } + } + if (DEBUG) { + Slog.d(TAG, "Measured " + packagesMeasured.size() + + " packages after considering APEXs."); + } + + // proceed with all preloaded apps + for (PackageInfo packageInfo : pm.getInstalledPackages( + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_FACTORY_ONLY + | PackageManager.GET_SIGNING_CERTIFICATES))) { + if (packagesMeasured.contains(packageInfo.packageName)) { + continue; + } + packagesMeasured.add(packageInfo.packageName); + + int mba_status = MBA_STATUS_PRELOADED; + if (packageInfo.signingInfo == null) { + Slog.d(TAG, "Preload " + packageInfo.packageName + " at " + + packageInfo.applicationInfo.sourceDir + " has likely been updated."); + mba_status = MBA_STATUS_UPDATED_PRELOAD; + + PackageInfo origPackageInfo = packageInfo; + try { + packageInfo = pm.getPackageInfo(packageInfo.packageName, + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL + | PackageManager.GET_SIGNING_CERTIFICATES)); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Failed to obtain an updated PackageInfo of " + + origPackageInfo.packageName, e); + packageInfo = origPackageInfo; + mba_status = MBA_STATUS_ERROR; + } + } + + + Bundle packageMeasurement = measurePackage(packageInfo); + results.add(packageMeasurement); + + if (record) { + // compute digests of signing info + String[] signerDigestHexStrings = computePackageSignerSha256Digests( + packageInfo.signingInfo); + + // now we should have all the bits for the atom + /* TODO: Uncomment and test after merging new atom definition. + FrameworkStatsLog.write(FrameworkStatsLog.MOBILE_BUNDLED_APP_INFO_GATHERED, + packageInfo.packageName, + packageInfo.getLongVersionCode(), + HexEncoding.encodeToString(packageMeasurement.getByteArray( + BUNDLE_CONTENT_DIGEST), false), + packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM), + signerDigestHexStrings, // signer_cert_digest + mba_status, // mba_status + null, // initiator + null, // initiator_signer_digest + null, // installer + null // originator + ); + */ + } } + if (DEBUG) { + Slog.d(TAG, "Measured " + packagesMeasured.size() + + " packages after considering preloads"); + } + + // lastly measure all newly installed MBAs + for (PackageInfo packageInfo : getNewlyInstalledMbas()) { + if (packagesMeasured.contains(packageInfo.packageName)) { + continue; + } + packagesMeasured.add(packageInfo.packageName); + + Bundle packageMeasurement = measurePackage(packageInfo); + results.add(packageMeasurement); + + if (record) { + // compute digests of signing info + String[] signerDigestHexStrings = computePackageSignerSha256Digests( + packageInfo.signingInfo); + + // then extract package's InstallSourceInfo + if (DEBUG) { + Slog.d(TAG, "Extracting InstallSourceInfo for " + packageInfo.packageName); + } + InstallSourceInfo installSourceInfo = getInstallSourceInfo( + packageInfo.packageName); + String initiator = null; + SigningInfo initiatorSignerInfo = null; + String[] initiatorSignerInfoDigest = null; + String installer = null; + String originator = null; + + if (installSourceInfo != null) { + initiator = installSourceInfo.getInitiatingPackageName(); + initiatorSignerInfo = installSourceInfo.getInitiatingPackageSigningInfo(); + if (initiatorSignerInfo != null) { + initiatorSignerInfoDigest = computePackageSignerSha256Digests( + initiatorSignerInfo); + } + installer = installSourceInfo.getInstallingPackageName(); + originator = installSourceInfo.getOriginatingPackageName(); + } - for (PackageInfo packageInfo : getInstalledApexs()) { - results.put(packageInfo, mBinaryHashes.get(packageInfo.packageName)); + // we should now have all the info needed for the atom + /* TODO: Uncomment and test after merging new atom definition. + FrameworkStatsLog.write(FrameworkStatsLog.MOBILE_BUNDLED_APP_INFO_GATHERED, + packageInfo.packageName, + packageInfo.getLongVersionCode(), + HexEncoding.encodeToString(packageMeasurement.getByteArray( + BUNDLE_CONTENT_DIGEST), false), + packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM), + signerDigestHexStrings, + MBA_STATUS_NEW_INSTALL, // mba_status + initiator, + initiatorSignerInfoDigest, + installer, + originator + ); + */ + } + } + if (DEBUG) { + long timeSpentMeasuring = System.currentTimeMillis() - currentTimeMs; + Slog.d(TAG, "Measured " + packagesMeasured.size() + + " packages altogether in " + timeSpentMeasuring + "ms"); } return results; } + /** + * A wrapper around + * {@link ApkSignatureVerifier#verifySignaturesInternal(ParseInput, String, int, boolean)}. + * @param pathToApk The APK's installation path + * @return a {@code Map<Integer, byte[]>} with algorithm type as the key and content + * digest as the value. + * a {@code null} is returned upon encountering any error. + */ + private Map<Integer, byte[]> computeApkContentDigest(String pathToApk) { + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + ParseResult<ApkSignatureVerifier.SigningDetailsWithDigests> parseResult = + ApkSignatureVerifier.verifySignaturesInternal(input, + pathToApk, + SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2, false); + if (parseResult.isError()) { + Slog.e(TAG, "Failed to compute content digest for " + + pathToApk + " due to: " + + parseResult.getErrorMessage()); + return null; + } + return parseResult.getResult().contentDigests; + } + @Override public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, @@ -165,43 +473,36 @@ public class BinaryTransparencyService extends SystemService { private void printPackageMeasurements(PackageInfo packageInfo, final PrintWriter pw) { - pw.print(mBinaryHashes.get(packageInfo.packageName) + ","); - // TODO: To be moved to somewhere more suitable - final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); - ParseResult<ApkSignatureVerifier.SigningDetailsWithDigests> parseResult = - ApkSignatureVerifier.verifySignaturesInternal(input, - packageInfo.applicationInfo.sourceDir, - SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3, false); - if (parseResult.isError()) { + Map<Integer, byte[]> contentDigests = computeApkContentDigest( + packageInfo.applicationInfo.sourceDir); + if (contentDigests == null) { pw.println("ERROR: Failed to compute package content digest for " - + packageInfo.applicationInfo.sourceDir + "due to: " - + parseResult.getErrorMessage()); - } else { - ApkSignatureVerifier.SigningDetailsWithDigests signingDetails = - parseResult.getResult(); - Map<Integer, byte[]> contentDigests = signingDetails.contentDigests; - for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) { - Integer algorithmId = entry.getKey(); - byte[] cDigest = entry.getValue(); - //pw.print("Content digest algorithm: "); - switch (algorithmId) { - case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256: - pw.print("CHUNKED_SHA256:"); - break; - case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512: - pw.print("CHUNKED_SHA512:"); - break; - case ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256: - pw.print("VERITY_CHUNKED_SHA256:"); - break; - case ApkSigningBlockUtils.CONTENT_DIGEST_SHA256: - pw.print("SHA256:"); - break; - default: - pw.print("UNKNOWN:"); - } - pw.print(HexEncoding.encodeToString(cDigest, false)); + + packageInfo.applicationInfo.sourceDir); + return; + } + + for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) { + Integer algorithmId = entry.getKey(); + byte[] contentDigest = entry.getValue(); + + // TODO(b/259348134): consider refactoring the following to a helper method + switch (algorithmId) { + case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256: + pw.print("CHUNKED_SHA256:"); + break; + case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512: + pw.print("CHUNKED_SHA512:"); + break; + case ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256: + pw.print("VERITY_CHUNKED_SHA256:"); + break; + case ApkSigningBlockUtils.CONTENT_DIGEST_SHA256: + pw.print("SHA256:"); + break; + default: + pw.print("UNKNOWN_ALGO_ID(" + algorithmId + "):"); } + pw.print(HexEncoding.encodeToString(contentDigest, false)); } } @@ -211,14 +512,49 @@ public class BinaryTransparencyService extends SystemService { pw.println("Current install location: " + packageInfo.applicationInfo.sourceDir); if (packageInfo.applicationInfo.sourceDir.startsWith("/data/apex/")) { - String origPackageFilepath = getOriginalPreinstalledLocation( + String origPackageFilepath = getOriginalApexPreinstalledLocation( packageInfo.packageName, packageInfo.applicationInfo.sourceDir); - pw.println("|--> Package pre-installed location: " + origPackageFilepath); - String digest = mBinaryHashes.get(origPackageFilepath); - if (digest == null) { - pw.println("ERROR finding SHA256-digest from cache..."); + pw.println("|--> Pre-installed package install location: " + + origPackageFilepath); + + // TODO(b/259347186): revive this with the proper cmd options. + /* + String digest = PackageUtils.computeSha256DigestForLargeFile( + origPackageFilepath, PackageUtils.createLargeFileBuffer()); + */ + + Map<Integer, byte[]> contentDigests = computeApkContentDigest( + origPackageFilepath); + if (contentDigests == null) { + pw.println("ERROR: Failed to compute package content digest for " + + origPackageFilepath); } else { - pw.println("|--> Pre-installed package SHA256-digest: " + digest); + // TODO(b/259348134): consider refactoring this to a helper method + for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) { + Integer algorithmId = entry.getKey(); + byte[] contentDigest = entry.getValue(); + pw.print("|--> Pre-installed package content digest algorithm: "); + switch (algorithmId) { + case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256: + pw.print("CHUNKED_SHA256"); + break; + case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512: + pw.print("CHUNKED_SHA512"); + break; + case ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256: + pw.print("VERITY_CHUNKED_SHA256"); + break; + case ApkSigningBlockUtils.CONTENT_DIGEST_SHA256: + pw.print("SHA256"); + break; + default: + pw.print("UNKNOWN"); + } + pw.print("\n"); + pw.print("|--> Pre-installed package content digest: "); + pw.print(HexEncoding.encodeToString(contentDigest, false)); + pw.print("\n"); + } } } pw.println("First install time (ms): " + packageInfo.firstInstallTime); @@ -281,21 +617,90 @@ public class BinaryTransparencyService extends SystemService { + (moduleInfo.isHidden() ? "hidden" : "visible")); } + private void printAppDetails(PackageInfo packageInfo, + boolean printLibraries, + final PrintWriter pw) { + pw.println("--- App Details ---"); + pw.println("Name: " + packageInfo.applicationInfo.name); + pw.println("Label: " + mContext.getPackageManager().getApplicationLabel( + packageInfo.applicationInfo)); + pw.println("Description: " + packageInfo.applicationInfo.loadDescription( + mContext.getPackageManager())); + pw.println("Has code: " + packageInfo.applicationInfo.hasCode()); + pw.println("Is enabled: " + packageInfo.applicationInfo.enabled); + pw.println("Is suspended: " + ((packageInfo.applicationInfo.flags + & ApplicationInfo.FLAG_SUSPENDED) != 0)); + + pw.println("Compile SDK version: " + packageInfo.compileSdkVersion); + pw.println("Target SDK version: " + + packageInfo.applicationInfo.targetSdkVersion); + + pw.println("Is privileged: " + + packageInfo.applicationInfo.isPrivilegedApp()); + pw.println("Is a stub: " + packageInfo.isStub); + pw.println("Is a core app: " + packageInfo.coreApp); + pw.println("SEInfo: " + packageInfo.applicationInfo.seInfo); + pw.println("Component factory: " + + packageInfo.applicationInfo.appComponentFactory); + pw.println("Process name: " + packageInfo.applicationInfo.processName); + pw.println("Task affinity : " + packageInfo.applicationInfo.taskAffinity); + pw.println("UID: " + packageInfo.applicationInfo.uid); + pw.println("Shared UID: " + packageInfo.sharedUserId); + + if (printLibraries) { + pw.println("== App's Shared Libraries =="); + List<SharedLibraryInfo> sharedLibraryInfos = + packageInfo.applicationInfo.getSharedLibraryInfos(); + if (sharedLibraryInfos == null || sharedLibraryInfos.isEmpty()) { + pw.println("<none>"); + } + + for (int i = 0; i < sharedLibraryInfos.size(); i++) { + SharedLibraryInfo sharedLibraryInfo = sharedLibraryInfos.get(i); + pw.println(" ++ Library #" + (i + 1) + " ++"); + pw.println(" Lib name: " + sharedLibraryInfo.getName()); + long libVersion = sharedLibraryInfo.getLongVersion(); + pw.print(" Lib version: "); + if (libVersion == SharedLibraryInfo.VERSION_UNDEFINED) { + pw.print("undefined"); + } else { + pw.print(libVersion); + } + pw.print("\n"); + + pw.println(" Lib package name (if available): " + + sharedLibraryInfo.getPackageName()); + pw.println(" Lib path: " + sharedLibraryInfo.getPath()); + pw.print(" Lib type: "); + switch (sharedLibraryInfo.getType()) { + case SharedLibraryInfo.TYPE_BUILTIN: + pw.print("built-in"); + break; + case SharedLibraryInfo.TYPE_DYNAMIC: + pw.print("dynamic"); + break; + case SharedLibraryInfo.TYPE_STATIC: + pw.print("static"); + break; + case SharedLibraryInfo.TYPE_SDK_PACKAGE: + pw.print("SDK"); + break; + case SharedLibraryInfo.VERSION_UNDEFINED: + default: + pw.print("undefined"); + break; + } + pw.print("\n"); + pw.println(" Is a native lib: " + sharedLibraryInfo.isNative()); + } + } + + } + private int printAllApexs() { final PrintWriter pw = getOutPrintWriter(); boolean verbose = false; String opt; - - // refresh cache to make sure info is most up-to-date - if (!updateBinaryMeasurements()) { - pw.println("ERROR: Failed to refresh info for APEXs."); - return -1; - } - if (mBinaryHashes == null || (mBinaryHashes.size() == 0)) { - pw.println("ERROR: Unable to obtain apex_info at this time."); - return -1; - } - while ((opt = getNextOption()) != null) { switch (opt) { case "-v": @@ -315,13 +720,15 @@ public class BinaryTransparencyService extends SystemService { if (!verbose) { pw.println("APEX Info [Format: package_name,package_version," - + "package_sha256_digest," + // TODO(b/259347186): revive via special cmd line option + //+ "package_sha256_digest," + "content_digest_algorithm:content_digest]:"); } - for (PackageInfo packageInfo : getInstalledApexs()) { + for (PackageInfo packageInfo : getCurrentInstalledApexs()) { if (verbose) { pw.println("APEX Info [Format: package_name,package_version," - + "package_sha256_digest," + // TODO(b/259347186): revive via special cmd line option + //+ "package_sha256_digest," + "content_digest_algorithm:content_digest]:"); } String packageName = packageInfo.packageName; @@ -352,17 +759,6 @@ public class BinaryTransparencyService extends SystemService { final PrintWriter pw = getOutPrintWriter(); boolean verbose = false; String opt; - - // refresh cache to make sure info is most up-to-date - if (!updateBinaryMeasurements()) { - pw.println("ERROR: Failed to refresh info for Modules."); - return -1; - } - if (mBinaryHashes == null || (mBinaryHashes.size() == 0)) { - pw.println("ERROR: Unable to obtain module_info at this time."); - return -1; - } - while ((opt = getNextOption()) != null) { switch (opt) { case "-v": @@ -382,14 +778,16 @@ public class BinaryTransparencyService extends SystemService { if (!verbose) { pw.println("Module Info [Format: package_name,package_version," - + "package_sha256_digest," + // TODO(b/259347186): revive via special cmd line option + //+ "package_sha256_digest," + "content_digest_algorithm:content_digest]:"); } for (ModuleInfo module : pm.getInstalledModules(PackageManager.MATCH_ALL)) { String packageName = module.getPackageName(); if (verbose) { pw.println("Module Info [Format: package_name,package_version," - + "package_sha256_digest," + // TODO(b/259347186): revive via special cmd line option + //+ "package_sha256_digest," + "content_digest_algorithm:content_digest]:"); } try { @@ -421,6 +819,72 @@ public class BinaryTransparencyService extends SystemService { return 0; } + private int printAllMbas() { + final PrintWriter pw = getOutPrintWriter(); + boolean verbose = false; + boolean printLibraries = false; + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-v": + verbose = true; + break; + case "-l": + printLibraries = true; + break; + default: + pw.println("ERROR: Unknown option: " + opt); + return 1; + } + } + + if (!verbose) { + pw.println("MBA Info [Format: package_name,package_version," + // TODO(b/259347186): revive via special cmd line option + //+ "package_sha256_digest," + + "content_digest_algorithm:content_digest]:"); + } + for (PackageInfo packageInfo : getNewlyInstalledMbas()) { + if (verbose) { + pw.println("MBA Info [Format: package_name,package_version," + // TODO(b/259347186): revive via special cmd line option + //+ "package_sha256_digest," + + "content_digest_algorithm:content_digest]:"); + } + pw.print(packageInfo.packageName + ","); + pw.print(packageInfo.getLongVersionCode() + ","); + printPackageMeasurements(packageInfo, pw); + pw.print("\n"); + + if (verbose) { + printAppDetails(packageInfo, printLibraries, pw); + printPackageInstallationInfo(packageInfo, pw); + printPackageSignerDetails(packageInfo.signingInfo, pw); + pw.println(""); + } + } + return 0; + } + + // TODO(b/259347186): add option handling full file-based SHA256 digest + private int printAllPreloads() { + final PrintWriter pw = getOutPrintWriter(); + + PackageManager pm = mContext.getPackageManager(); + if (pm == null) { + Slog.e(TAG, "Failed to obtain PackageManager."); + return -1; + } + List<PackageInfo> factoryApps = pm.getInstalledPackages( + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_FACTORY_ONLY)); + + pw.println("Preload Info [Format: package_name]"); + for (PackageInfo packageInfo : factoryApps) { + pw.println(packageInfo.packageName); + } + return 0; + } + @Override public int onCommand(String cmd) { if (cmd == null) { @@ -443,6 +907,10 @@ public class BinaryTransparencyService extends SystemService { return printAllApexs(); case "module_info": return printAllModules(); + case "mba_info": + return printAllMbas(); + case "preload_info": + return printAllPreloads(); default: pw.println(String.format("ERROR: Unknown info type '%s'", infoType)); @@ -466,11 +934,18 @@ public class BinaryTransparencyService extends SystemService { pw.println(""); pw.println(" get apex_info [-v]"); pw.println(" Print information about installed APEXs on device."); - pw.println(" -v: lists more verbose information about each APEX"); + pw.println(" -v: lists more verbose information about each APEX."); pw.println(""); pw.println(" get module_info [-v]"); pw.println(" Print information about installed modules on device."); - pw.println(" -v: lists more verbose information about each module"); + pw.println(" -v: lists more verbose information about each module."); + pw.println(""); + pw.println(" get mba_info [-v] [-l]"); + pw.println(" Print information about installed mobile bundle apps " + + "(MBAs on device)."); + pw.println(" -v: lists more verbose information about each app."); + pw.println(" -l: lists shared library info. This will only be " + + "listed with -v"); pw.println(""); } @@ -488,8 +963,7 @@ public class BinaryTransparencyService extends SystemService { mContext = context; mServiceImpl = new BinaryTransparencyServiceImpl(); mVbmetaDigest = VBMETA_DIGEST_UNINITIALIZED; - mBinaryHashes = new HashMap<>(); - mBinaryLastUpdateTimes = new HashMap<>(); + mMeasurementsLastRecordedMs = 0; } /** @@ -520,44 +994,43 @@ public class BinaryTransparencyService extends SystemService { Slog.i(TAG, "Boot completed. Getting VBMeta Digest."); getVBMetaDigestInformation(); - // due to potentially long computation that holds up boot time, computations for - // SHA256 digests of APEX and Module packages are scheduled here, - // but only executed when device is idle. - Slog.i(TAG, "Scheduling APEX and Module measurements to be updated."); + // to avoid the risk of holding up boot time, computations to measure APEX, Module, and + // MBA digests are scheduled here, but only executed when the device is idle and plugged + // in. + Slog.i(TAG, "Scheduling measurements to be taken."); UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext, BinaryTransparencyService.this); } } /** - * JobService to update binary measurements and update internal cache. + * JobService to measure all covered binaries and record result to Westworld. */ public static class UpdateMeasurementsJobService extends JobService { - private static final int COMPUTE_APEX_MODULE_SHA256_JOB_ID = - BinaryTransparencyService.UpdateMeasurementsJobService.class.hashCode(); + private static final int DO_BINARY_MEASUREMENTS_JOB_ID = + UpdateMeasurementsJobService.class.hashCode(); @Override public boolean onStartJob(JobParameters params) { Slog.d(TAG, "Job to update binary measurements started."); - if (params.getJobId() != COMPUTE_APEX_MODULE_SHA256_JOB_ID) { + if (params.getJobId() != DO_BINARY_MEASUREMENTS_JOB_ID) { return false; } - // we'll still update the measurements via threads to be mindful of low-end devices + // we'll perform binary measurements via threads to be mindful of low-end devices // where this operation might take longer than expected, and so that we don't block // system_server's main thread. Executors.defaultThreadFactory().newThread(() -> { - // since we can't call updateBinaryMeasurements() directly, calling - // getApexInfo() achieves the same effect, and we simply discard the return - // value - + // we discard the return value of getMeasurementsForAllPackages() as the + // results of the measurements will be recorded, and that is what we're aiming + // for with this job. IBinder b = ServiceManager.getService(Context.BINARY_TRANSPARENCY_SERVICE); IBinaryTransparencyService iBtsService = IBinaryTransparencyService.Stub.asInterface(b); try { - iBtsService.getApexInfo(); + iBtsService.getMeasurementsForAllPackages(); } catch (RemoteException e) { - Slog.e(TAG, "Updating binary measurements was interrupted.", e); + Slog.e(TAG, "Taking binary measurements was interrupted.", e); return; } jobFinished(params, false); @@ -573,25 +1046,26 @@ public class BinaryTransparencyService extends SystemService { @SuppressLint("DefaultLocale") static void scheduleBinaryMeasurements(Context context, BinaryTransparencyService service) { - Slog.i(TAG, "Scheduling APEX & Module SHA256 digest computation job"); + Slog.i(TAG, "Scheduling binary content-digest computation job"); final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); if (jobScheduler == null) { Slog.e(TAG, "Failed to obtain an instance of JobScheduler."); return; } - final JobInfo jobInfo = new JobInfo.Builder(COMPUTE_APEX_MODULE_SHA256_JOB_ID, + final JobInfo jobInfo = new JobInfo.Builder(DO_BINARY_MEASUREMENTS_JOB_ID, new ComponentName(context, UpdateMeasurementsJobService.class)) .setRequiresDeviceIdle(true) .setRequiresCharging(true) + .setPeriodic(RECORD_MEASUREMENTS_COOLDOWN_MS) .build(); if (jobScheduler.schedule(jobInfo) != JobScheduler.RESULT_SUCCESS) { - Slog.e(TAG, "Failed to schedule job to update binary measurements."); + Slog.e(TAG, "Failed to schedule job to measure binaries."); return; } - Slog.d(TAG, String.format( - "Job %d to update binary measurements scheduled successfully.", - COMPUTE_APEX_MODULE_SHA256_JOB_ID)); + Slog.d(TAG, TextUtils.formatSimple( + "Job %d to measure binaries was scheduled successfully.", + DO_BINARY_MEASUREMENTS_JOB_ID)); } } @@ -602,7 +1076,7 @@ public class BinaryTransparencyService extends SystemService { } @NonNull - private List<PackageInfo> getInstalledApexs() { + private List<PackageInfo> getCurrentInstalledApexs() { List<PackageInfo> results = new ArrayList<>(); PackageManager pm = mContext.getPackageManager(); if (pm == null) { @@ -636,164 +1110,52 @@ public class BinaryTransparencyService extends SystemService { } } - - /** - * Updates the internal data structure with the most current APEX measurements. - * @return true if update is successful; false otherwise. - */ - private boolean updateBinaryMeasurements() { - if (mBinaryHashes.size() == 0) { - Slog.d(TAG, "No apex in cache yet."); - doFreshBinaryMeasurements(); - return true; - } - - PackageManager pm = mContext.getPackageManager(); - if (pm == null) { - Slog.e(TAG, "Failed to obtain a valid PackageManager instance."); - return false; - } - - // We're assuming updates to existing modules and APEXs can happen, but not brand new - // ones appearing out of the blue. Thus, we're going to only go through our cache to check - // for changes, rather than freshly invoking `getInstalledPackages()` and - // `getInstalledModules()` - byte[] largeFileBuffer = PackageUtils.createLargeFileBuffer(); - for (Map.Entry<String, Long> entry : mBinaryLastUpdateTimes.entrySet()) { - String packageName = entry.getKey(); - try { - PackageInfo packageInfo = pm.getPackageInfo(packageName, - PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX)); - long cachedUpdateTime = entry.getValue(); - - if (packageInfo.lastUpdateTime > cachedUpdateTime) { - Slog.d(TAG, packageName + " has been updated!"); - entry.setValue(packageInfo.lastUpdateTime); - - // compute the digest for the updated package - String sha256digest = PackageUtils.computeSha256DigestForLargeFile( - packageInfo.applicationInfo.sourceDir, largeFileBuffer); - if (sha256digest == null) { - Slog.e(TAG, "Failed to compute SHA256sum for file at " - + packageInfo.applicationInfo.sourceDir); - mBinaryHashes.put(packageName, BINARY_HASH_ERROR); - } else { - mBinaryHashes.put(packageName, sha256digest); - } - - if (packageInfo.isApex) { - FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED, - packageInfo.packageName, - packageInfo.getLongVersionCode(), - mBinaryHashes.get(packageInfo.packageName), - 4, // indicating that the digest is SHA256 - null); // TODO: This is to comform to the extended schema. - } - } - } catch (PackageManager.NameNotFoundException e) { - Slog.e(TAG, "Could not find package with name " + packageName); - continue; - } - } - - return true; - } - - private String getOriginalPreinstalledLocation(String packageName, + // TODO(b/259349011): Need to be more robust against package name mismatch in the filename + private String getOriginalApexPreinstalledLocation(String packageName, String currentInstalledLocation) { if (currentInstalledLocation.contains("/decompressed/")) { - return "/system/apex/" + packageName + ".capex"; + String resultPath = "system/apex" + packageName + ".capex"; + File f = new File(resultPath); + if (f.exists()) { + return resultPath; + } + return "/system/apex/" + packageName + ".next.capex"; } return "/system/apex" + packageName + "apex"; } - private void doFreshBinaryMeasurements() { - PackageManager pm = mContext.getPackageManager(); - Slog.d(TAG, "Obtained package manager"); - - // In general, we care about all APEXs, *and* all Modules, which may include some APKs. - - // First, we deal with all installed APEXs. - byte[] largeFileBuffer = PackageUtils.createLargeFileBuffer(); - for (PackageInfo packageInfo : getInstalledApexs()) { - ApplicationInfo appInfo = packageInfo.applicationInfo; - - // compute SHA256 for these APEXs - String sha256digest = PackageUtils.computeSha256DigestForLargeFile(appInfo.sourceDir, - largeFileBuffer); - if (sha256digest == null) { - Slog.e(TAG, String.format("Failed to compute SHA256 digest for %s", - packageInfo.packageName)); - mBinaryHashes.put(packageInfo.packageName, BINARY_HASH_ERROR); - } else { - mBinaryHashes.put(packageInfo.packageName, sha256digest); - } - FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED, packageInfo.packageName, - packageInfo.getLongVersionCode(), mBinaryHashes.get(packageInfo.packageName)); - Slog.d(TAG, String.format("Last update time for %s: %d", packageInfo.packageName, - packageInfo.lastUpdateTime)); - mBinaryLastUpdateTimes.put(packageInfo.packageName, packageInfo.lastUpdateTime); - } - - for (PackageInfo packageInfo : getInstalledApexs()) { - ApplicationInfo appInfo = packageInfo.applicationInfo; - if (!appInfo.sourceDir.startsWith("/data/")) { - continue; - } - String origInstallPath = getOriginalPreinstalledLocation(packageInfo.packageName, - appInfo.sourceDir); - - // compute SHA256 for the orig /system APEXs - String sha256digest = PackageUtils.computeSha256DigestForLargeFile(origInstallPath, - largeFileBuffer); - if (sha256digest == null) { - Slog.e(TAG, String.format("Failed to compute SHA256 digest for file at %s", - origInstallPath)); - mBinaryHashes.put(origInstallPath, BINARY_HASH_ERROR); - } else { - mBinaryHashes.put(origInstallPath, sha256digest); - } - // there'd be no entry into mBinaryLastUpdateTimes + /** + * Wrapper method to call into IBICS to get a list of all newly installed MBAs. + * + * We expect IBICS to maintain an accurate list of installed MBAs, and we merely make use of + * the results within this service. This means we do not further check whether the + * apps in the returned slice is still installed or not, esp. considering that preloaded apps + * could be updated, or post-setup installed apps *might* be deleted in real time. + * + * Note that we do *not* cache the results from IBICS because of the more dynamic nature of + * MBAs v.s. other binaries that we measure. + * + * @return a list of preloaded apps + dynamically installed apps that fit the definition of MBA. + */ + @NonNull + private List<PackageInfo> getNewlyInstalledMbas() { + List<PackageInfo> result = new ArrayList<>(); + IBackgroundInstallControlService iBics = IBackgroundInstallControlService.Stub.asInterface( + ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)); + if (iBics == null) { + Slog.e(TAG, + "Failed to obtain an IBinder instance of IBackgroundInstallControlService"); + return result; } - - // Next, get all installed modules from PackageManager - skip over those APEXs we've - // processed above - for (ModuleInfo module : pm.getInstalledModules(PackageManager.MATCH_ALL)) { - String packageName = module.getPackageName(); - if (packageName == null) { - Slog.e(TAG, "ERROR: Encountered null package name for module " - + module.getApexModuleName()); - continue; - } - if (mBinaryHashes.containsKey(module.getPackageName())) { - continue; - } - - // get PackageInfo for this module - try { - PackageInfo packageInfo = pm.getPackageInfo(packageName, - PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX)); - ApplicationInfo appInfo = packageInfo.applicationInfo; - - // compute SHA256 digest for these modules - String sha256digest = PackageUtils.computeSha256DigestForLargeFile( - appInfo.sourceDir, largeFileBuffer); - if (sha256digest == null) { - Slog.e(TAG, String.format("Failed to compute SHA256 digest for %s", - packageName)); - mBinaryHashes.put(packageName, BINARY_HASH_ERROR); - } else { - mBinaryHashes.put(packageName, sha256digest); - } - Slog.d(TAG, String.format("Last update time for %s: %d", packageName, - packageInfo.lastUpdateTime)); - mBinaryLastUpdateTimes.put(packageName, packageInfo.lastUpdateTime); - } catch (PackageManager.NameNotFoundException e) { - Slog.e(TAG, "ERROR: Could not obtain PackageInfo for package name: " - + packageName); - continue; - } + ParceledListSlice<PackageInfo> slice; + try { + slice = iBics.getBackgroundInstalledPackages( + PackageManager.MATCH_ALL | PackageManager.GET_SIGNING_CERTIFICATES, + UserHandle.USER_SYSTEM); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get a list of MBAs.", e); + return result; } + return slice.getList(); } - } diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index e529010e6b7d..7d2e2766fd0b 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -466,7 +466,8 @@ public class GestureLauncherService extends SystemService { public static boolean isEmergencyGestureSettingEnabled(Context context, int userId) { return isEmergencyGestureEnabled(context.getResources()) && Settings.Secure.getIntForUser(context.getContentResolver(), - Settings.Secure.EMERGENCY_GESTURE_ENABLED, 1, userId) != 0; + Settings.Secure.EMERGENCY_GESTURE_ENABLED, + isDefaultEmergencyGestureEnabled(context.getResources()) ? 1 : 0, userId) != 0; } /** @@ -513,6 +514,11 @@ public class GestureLauncherService extends SystemService { return resources.getBoolean(com.android.internal.R.bool.config_emergencyGestureEnabled); } + private static boolean isDefaultEmergencyGestureEnabled(Resources resources) { + return resources.getBoolean( + com.android.internal.R.bool.config_defaultEmergencyGestureEnabled); + } + /** * Whether GestureLauncherService should be enabled according to system properties. */ diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java index c3cd1359cd5f..a05b84baf667 100644 --- a/services/core/java/com/android/server/SystemServiceManager.java +++ b/services/core/java/com/android/server/SystemServiceManager.java @@ -351,12 +351,24 @@ public final class SystemServiceManager implements Dumpable { * Starts the given user. */ public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId) { - EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId); - final TargetUser targetUser = newTargetUser(userId); synchronized (mTargetUsers) { + // On Automotive / Headless System User Mode, the system user will be started twice: + // - Once by some external or local service that switches the system user to + // the background. + // - Once by the ActivityManagerService, when the system is marked ready. + // These two events are not synchronized and the order of execution is + // non-deterministic. To avoid starting the system user twice, verify whether + // the system user has already been started by checking the mTargetUsers. + // TODO(b/242195409): this workaround shouldn't be necessary once we move + // the headless-user start logic to UserManager-land. + if (userId == UserHandle.USER_SYSTEM && mTargetUsers.contains(userId)) { + Slog.e(TAG, "Skipping starting system user twice"); + return; + } mTargetUsers.put(userId, targetUser); } + EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId); onUser(t, USER_STARTING, /* prevUser= */ null, targetUser); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 86bb699f07d2..5c18635f570c 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -137,6 +137,7 @@ import android.content.Context; import android.content.IIntentSender; import android.content.Intent; import android.content.IntentSender; +import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; @@ -309,6 +310,12 @@ public final class ActiveServices { final ArrayList<ServiceRecord> mPendingFgsNotifications = new ArrayList<>(); /** + * Map of ForegroundServiceDelegation to the delegation ServiceRecord. The delegation + * ServiceRecord has flag isFgsDelegate set to true. + */ + final ArrayMap<ForegroundServiceDelegation, ServiceRecord> mFgsDelegations = new ArrayMap<>(); + + /** * Whether there is a rate limit that suppresses immediate re-deferral of new FGS * notifications from each app. On by default, disabled only by shell command for * test-suite purposes. To disable the behavior more generally, use the usual @@ -561,7 +568,7 @@ public final class ActiveServices { try { final ServiceRecord.StartItem si = r.pendingStarts.get(0); startServiceInnerLocked(this, si.intent, r, false, true, si.callingId, - r.startRequested); + si.mCallingProcessName, r.startRequested); } catch (TransactionTooLargeException e) { // Ignore, nobody upstack cares. } @@ -884,8 +891,10 @@ public final class ActiveServices { // alias component name to the client, not the "target" component name, which is // what realResult contains. final ComponentName realResult = - startServiceInnerLocked(r, service, callingUid, callingPid, fgRequired, callerFg, - allowBackgroundActivityStarts, backgroundActivityStartsToken); + startServiceInnerLocked(r, service, callingUid, callingPid, + getCallingProcessNameLocked(callingUid, callingPid, callingPackage), + fgRequired, callerFg, allowBackgroundActivityStarts, + backgroundActivityStartsToken); if (res.aliasComponent != null && !realResult.getPackageName().startsWith("!") && !realResult.getPackageName().startsWith("?")) { @@ -895,10 +904,18 @@ public final class ActiveServices { } } + private String getCallingProcessNameLocked(int callingUid, int callingPid, + String callingPackage) { + synchronized (mAm.mPidsSelfLocked) { + final ProcessRecord callingApp = mAm.mPidsSelfLocked.get(callingPid); + return callingApp != null ? callingApp.processName : callingPackage; + } + } + private ComponentName startServiceInnerLocked(ServiceRecord r, Intent service, - int callingUid, int callingPid, boolean fgRequired, boolean callerFg, - boolean allowBackgroundActivityStarts, @Nullable IBinder backgroundActivityStartsToken) - throws TransactionTooLargeException { + int callingUid, int callingPid, String callingProcessName, boolean fgRequired, + boolean callerFg, boolean allowBackgroundActivityStarts, + @Nullable IBinder backgroundActivityStartsToken) throws TransactionTooLargeException { NeededUriGrants neededGrants = mAm.mUgmInternal.checkGrantUriPermissionFromIntent( service, callingUid, r.packageName, r.userId); if (unscheduleServiceRestartLocked(r, callingUid, false)) { @@ -910,7 +927,7 @@ public final class ActiveServices { r.delayedStop = false; r.fgRequired = fgRequired; r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(), - service, neededGrants, callingUid)); + service, neededGrants, callingUid, callingProcessName)); if (fgRequired) { // We are now effectively running a foreground service. @@ -995,7 +1012,7 @@ public final class ActiveServices { r.allowBgActivityStartsOnServiceStart(backgroundActivityStartsToken); } ComponentName cmp = startServiceInnerLocked(smap, service, r, callerFg, addToStarting, - callingUid, wasStartRequested); + callingUid, callingProcessName, wasStartRequested); return cmp; } @@ -1113,6 +1130,8 @@ public final class ActiveServices { curPendingBringups = new ArrayList<>(); mPendingBringups.put(s, curPendingBringups); } + final String callingProcessName = getCallingProcessNameLocked( + callingUid, callingPid, callingPackage); curPendingBringups.add(new Runnable() { @Override public void run() { @@ -1145,8 +1164,8 @@ public final class ActiveServices { } else { // Starting a service try { startServiceInnerLocked(s, serviceIntent, callingUid, callingPid, - fgRequired, callerFg, allowBackgroundActivityStarts, - backgroundActivityStartsToken); + callingProcessName, fgRequired, callerFg, + allowBackgroundActivityStarts, backgroundActivityStartsToken); } catch (TransactionTooLargeException e) { /* ignore - local call */ } @@ -1191,8 +1210,8 @@ public final class ActiveServices { } ComponentName startServiceInnerLocked(ServiceMap smap, Intent service, ServiceRecord r, - boolean callerFg, boolean addToStarting, int callingUid, boolean wasStartRequested) - throws TransactionTooLargeException { + boolean callerFg, boolean addToStarting, int callingUid, String callingProcessName, + boolean wasStartRequested) throws TransactionTooLargeException { synchronized (mAm.mProcessStats.mLock) { final ServiceState stracker = r.getTracker(); if (stracker != null) { @@ -1226,7 +1245,8 @@ public final class ActiveServices { ? SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD : (wasStartRequested || !r.getConnections().isEmpty() ? SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_HOT - : SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM)); + : SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM), + getShortProcessNameForStats(callingUid, callingProcessName)); if (r.startRequested && addToStarting) { boolean first = smap.mStartingBackground.size() == 0; @@ -1249,6 +1269,22 @@ public final class ActiveServices { return r.name; } + private @Nullable String getShortProcessNameForStats(int uid, String processName) { + final String[] packages = mAm.mContext.getPackageManager().getPackagesForUid(uid); + if (packages != null && packages.length == 1) { + // Not the shared UID case, let's see if the package name equals to the process name. + if (TextUtils.equals(packages[0], processName)) { + // same name, just return null here. + return null; + } else if (processName != null && processName.startsWith(packages[0])) { + // return the suffix of the process name + return processName.substring(packages[0].length()); + } + } + // return the full process name. + return processName; + } + private void stopServiceLocked(ServiceRecord service, boolean enqueueOomAdj) { try { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "stopServiceLocked()"); @@ -3043,7 +3079,7 @@ public final class ActiveServices { ServiceLookupResult res = retrieveServiceLocked(service, instanceName, isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage, callingPid, callingUid, userId, true, callerFg, - isBindExternal, allowInstant); + isBindExternal, allowInstant, null /* fgsDelegateOptions */); if (res == null) { return 0; } @@ -3192,7 +3228,8 @@ public final class ActiveServices { ? SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD : (wasStartRequested || hadConnections ? SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_HOT - : SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM)); + : SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM), + getShortProcessNameForStats(callingUid, callerApp.processName)); if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Bind " + s + " with " + b + ": received=" + b.intent.received @@ -3501,7 +3538,7 @@ public final class ActiveServices { boolean allowInstant) { return retrieveServiceLocked(service, instanceName, false, 0, null, resolvedType, callingPackage, callingPid, callingUid, userId, createIfNeeded, callingFromFg, - isBindExternal, allowInstant); + isBindExternal, allowInstant, null /* fgsDelegateOptions */); } private ServiceLookupResult retrieveServiceLocked(Intent service, @@ -3509,7 +3546,7 @@ public final class ActiveServices { String sdkSandboxClientAppPackage, String resolvedType, String callingPackage, int callingPid, int callingUid, int userId, boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal, - boolean allowInstant) { + boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions) { if (isSdkSandboxService && instanceName == null) { throw new IllegalArgumentException("No instanceName provided for sdk sandbox process"); } @@ -3572,6 +3609,53 @@ public final class ActiveServices { } } } + + if (r == null && fgsDelegateOptions != null) { + // Create a ServiceRecord for FGS delegate. + final ServiceInfo sInfo = new ServiceInfo(); + ApplicationInfo aInfo = null; + try { + aInfo = AppGlobals.getPackageManager().getApplicationInfo( + fgsDelegateOptions.mClientPackageName, + ActivityManagerService.STOCK_PM_FLAGS, + userId); + } catch (RemoteException ex) { + // pm is in same process, this will never happen. + } + if (aInfo == null) { + throw new SecurityException("startForegroundServiceDelegate failed, " + + "could not resolve client package " + callingPackage); + } + if (aInfo.uid != fgsDelegateOptions.mClientUid) { + throw new SecurityException("startForegroundServiceDelegate failed, " + + "uid:" + aInfo.uid + + " does not match clientUid:" + fgsDelegateOptions.mClientUid); + } + sInfo.applicationInfo = aInfo; + sInfo.packageName = aInfo.packageName; + sInfo.mForegroundServiceType = fgsDelegateOptions.mForegroundServiceTypes; + sInfo.processName = aInfo.processName; + final ComponentName cn = service.getComponent(); + sInfo.name = cn.getClassName(); + if (createIfNeeded) { + final Intent.FilterComparison filter = + new Intent.FilterComparison(service.cloneFilter()); + final ServiceRestarter res = new ServiceRestarter(); + r = new ServiceRecord(mAm, cn /* name */, cn /* instanceName */, + sInfo.applicationInfo.packageName, sInfo.applicationInfo.uid, filter, sInfo, + callingFromFg, res, null /* sdkSandboxProcessName */, + INVALID_UID /* sdkSandboxClientAppUid */, + null /* sdkSandboxClientAppPackage */); + res.setService(r); + smap.mServicesByInstanceName.put(cn, r); + smap.mServicesByIntent.put(filter, r); + if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Retrieve created new service: " + r); + r.mRecentCallingPackage = callingPackage; + r.mRecentCallingUid = callingUid; + } + return new ServiceLookupResult(r, resolution.getAlias()); + } + if (r == null) { try { int flags = ActivityManagerService.STOCK_PM_FLAGS @@ -4661,7 +4745,7 @@ public final class ActiveServices { // be called. if (r.startRequested && r.callStart && r.pendingStarts.size() == 0) { r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(), - null, null, 0)); + null, null, 0, null)); } sendServiceArgsLocked(r, execInFg, true); @@ -4983,16 +5067,31 @@ public final class ActiveServices { // Bump the process to the top of LRU list mAm.updateLruProcessLocked(r.app, false, null); updateServiceForegroundLocked(r.app.mServices, false); - try { - oomAdjusted |= bumpServiceExecutingLocked(r, false, "destroy", - oomAdjusted ? 0 : OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); - mDestroyingServices.add(r); - r.destroying = true; - r.app.getThread().scheduleStopService(r); - } catch (Exception e) { - Slog.w(TAG, "Exception when destroying service " - + r.shortInstanceName, e); - serviceProcessGoneLocked(r, enqueueOomAdj); + if (r.mIsFgsDelegate) { + if (r.mFgsDelegation.mConnection != null) { + mAm.mHandler.post(() -> { + r.mFgsDelegation.mConnection.onServiceDisconnected( + r.mFgsDelegation.mOptions.getComponentName()); + }); + } + for (int i = mFgsDelegations.size() - 1; i >= 0; i--) { + if (mFgsDelegations.valueAt(i) == r) { + mFgsDelegations.removeAt(i); + break; + } + } + } else { + try { + oomAdjusted |= bumpServiceExecutingLocked(r, false, "destroy", + oomAdjusted ? 0 : OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE); + mDestroyingServices.add(r); + r.destroying = true; + r.app.getThread().scheduleStopService(r); + } catch (Exception e) { + Slog.w(TAG, "Exception when destroying service " + + r.shortInstanceName, e); + serviceProcessGoneLocked(r, enqueueOomAdj); + } } } else { if (DEBUG_SERVICE) Slog.v( @@ -5620,7 +5719,7 @@ public final class ActiveServices { stopServiceLocked(sr, true); } else { sr.pendingStarts.add(new ServiceRecord.StartItem(sr, true, - sr.getLastStartId(), baseIntent, null, 0)); + sr.getLastStartId(), baseIntent, null, 0, null)); if (sr.app != null && sr.app.getThread() != null) { // We always run in the foreground, since this is called as // part of the "remove task" UI operation. @@ -7234,7 +7333,12 @@ public final class ActiveServices { ActivityManagerUtils.hashComponentNameForAtom(r.shortInstanceName), r.mFgsHasNotificationPermission, r.foregroundServiceType, - fgsTypeCheckCode); + fgsTypeCheckCode, + r.mIsFgsDelegate, + r.mFgsDelegation != null ? r.mFgsDelegation.mOptions.mClientUid : INVALID_UID, + r.mFgsDelegation != null ? r.mFgsDelegation.mOptions.mDelegationService + : ForegroundServiceDelegationOptions.DELEGATION_SERVICE_DEFAULT + ); int event = 0; if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) { @@ -7298,4 +7402,163 @@ public final class ActiveServices { return "UNKNOWN"; } } + + /** + * Start a foreground service delegate. The delegate is not an actual service component, it is + * merely a delegate that promotes the client process into foreground service process state. + * + * @param options an ForegroundServiceDelegationOptions object. + * @param connection callback if the delegate is started successfully. + * @return true if delegate is started, false otherwise. + * @throw SecurityException if PackageManaager can not resolve + * {@link ForegroundServiceDelegationOptions#mClientPackageName} or the resolved + * package's UID is not same as {@link ForegroundServiceDelegationOptions#mClientUid} + */ + boolean startForegroundServiceDelegateLocked( + @NonNull ForegroundServiceDelegationOptions options, + @Nullable ServiceConnection connection) { + Slog.v(TAG, "startForegroundServiceDelegateLocked " + options.getDescription()); + final ComponentName cn = options.getComponentName(); + for (int i = mFgsDelegations.size() - 1; i >= 0; i--) { + ForegroundServiceDelegation delegation = mFgsDelegations.keyAt(i); + if (delegation.mOptions.isSameDelegate(options)) { + Slog.e(TAG, "startForegroundServiceDelegate " + options.getDescription() + + " already exists, multiple connections are not allowed"); + return false; + } + } + final int callingPid = options.mClientPid; + final int callingUid = options.mClientUid; + final int userId = UserHandle.getUserId(callingUid); + final String callingPackage = options.mClientPackageName; + + if (!canStartForegroundServiceLocked(callingPid, callingUid, callingPackage)) { + Slog.d(TAG, "startForegroundServiceDelegateLocked aborted," + + " app is in the background"); + return false; + } + + IApplicationThread caller = options.mClientAppThread; + ProcessRecord callerApp; + if (caller != null) { + callerApp = mAm.getRecordForAppLOSP(caller); + } else { + synchronized (mAm.mPidsSelfLocked) { + callerApp = mAm.mPidsSelfLocked.get(callingPid); + caller = callerApp.getThread(); + } + } + if (callerApp == null) { + throw new SecurityException( + "Unable to find app for caller " + caller + + " (pid=" + callingPid + + ") when startForegroundServiceDelegateLocked " + cn); + } + + Intent intent = new Intent(); + intent.setComponent(cn); + ServiceLookupResult res = retrieveServiceLocked(intent, null /*instanceName */, + false /* isSdkSandboxService */, INVALID_UID /* sdkSandboxClientAppUid */, + null /* sdkSandboxClientAppPackage */, null /* resolvedType */, callingPackage, + callingPid, callingUid, userId, true /* createIfNeeded */, + false /* callingFromFg */, false /* isBindExternal */, false /* allowInstant */ , + options); + if (res == null || res.record == null) { + Slog.d(TAG, + "startForegroundServiceDelegateLocked retrieveServiceLocked returns null"); + return false; + } + + final ServiceRecord r = res.record; + r.setProcess(callerApp, caller, callingPid, null); + r.mIsFgsDelegate = true; + final ForegroundServiceDelegation delegation = + new ForegroundServiceDelegation(options, connection); + r.mFgsDelegation = delegation; + mFgsDelegations.put(delegation, r); + r.isForeground = true; + r.mFgsEnterTime = SystemClock.uptimeMillis(); + r.foregroundServiceType = options.mForegroundServiceTypes; + setFgsRestrictionLocked(callingPackage, callingPid, callingUid, intent, r, userId, + false, false); + final ProcessServiceRecord psr = callerApp.mServices; + final boolean newService = psr.startService(r); + // updateOomAdj. + updateServiceForegroundLocked(psr, /* oomAdj= */ true); + + synchronized (mAm.mProcessStats.mLock) { + final ServiceState stracker = r.getTracker(); + if (stracker != null) { + stracker.setForeground(true, + mAm.mProcessStats.getMemFactorLocked(), + SystemClock.uptimeMillis()); + } + } + + mAm.mBatteryStatsService.noteServiceStartRunning(callingUid, callingPackage, + cn.getClassName()); + mAm.mAppOpsService.startOperation(AppOpsManager.getToken(mAm.mAppOpsService), + AppOpsManager.OP_START_FOREGROUND, r.appInfo.uid, r.packageName, null, + true, false, null, false, + AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); + registerAppOpCallbackLocked(r); + logFGSStateChangeLocked(r, + FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER, + 0, FGS_STOP_REASON_UNKNOWN, FGS_TYPE_POLICY_CHECK_UNKNOWN); + // Notify the caller. + if (connection != null) { + mAm.mHandler.post(() -> { + connection.onServiceConnected(cn, delegation.mBinder); + }); + } + signalForegroundServiceObserversLocked(r); + return true; + } + + /** + * Stop the foreground service delegate. This removes the process out of foreground service + * process state. + * + * @param options an ForegroundServiceDelegationOptions object. + */ + void stopForegroundServiceDelegateLocked(@NonNull ForegroundServiceDelegationOptions options) { + ServiceRecord r = null; + for (int i = mFgsDelegations.size() - 1; i >= 0; i--) { + if (mFgsDelegations.keyAt(i).mOptions.isSameDelegate(options)) { + Slog.d(TAG, "stopForegroundServiceDelegateLocked " + options.getDescription()); + r = mFgsDelegations.valueAt(i); + break; + } + } + if (r != null) { + bringDownServiceLocked(r, false); + } else { + Slog.e(TAG, "stopForegroundServiceDelegateLocked delegate does not exist " + + options.getDescription()); + } + } + + /** + * Stop the foreground service delegate by its ServiceConnection. + * This removes the process out of foreground service process state. + * + * @param connection an ServiceConnection object. + */ + void stopForegroundServiceDelegateLocked(@NonNull ServiceConnection connection) { + ServiceRecord r = null; + for (int i = mFgsDelegations.size() - 1; i >= 0; i--) { + final ForegroundServiceDelegation d = mFgsDelegations.keyAt(i); + if (d.mConnection == connection) { + Slog.d(TAG, "stopForegroundServiceDelegateLocked " + + d.mOptions.getDescription()); + r = mFgsDelegations.valueAt(i); + break; + } + } + if (r != null) { + bringDownServiceLocked(r, false); + } else { + Slog.e(TAG, "stopForegroundServiceDelegateLocked delegate does not exist"); + } + } } diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 003f7f0d88fb..8b5b795b8ff5 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -318,6 +318,12 @@ final class ActivityManagerConstants extends ContentObserver { "deferred_fgs_notification_interval"; /** + * Same as {@link #KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL} but for "short FGS". + */ + private static final String KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL_FOR_SHORT = + "deferred_fgs_notification_interval_for_short"; + + /** * Time in milliseconds; once an FGS notification for a given uid has been * deferred, no subsequent FGS notification from that uid will be deferred * until this amount of time has passed. Default is two minutes @@ -327,6 +333,12 @@ final class ActivityManagerConstants extends ContentObserver { "deferred_fgs_notification_exclusion_time"; /** + * Same as {@link #KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME} but for "short FGS". + */ + private static final String KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME_FOR_SHORT = + "deferred_fgs_notification_exclusion_time_for_short"; + + /** * Default value for mPushMessagingOverQuotaBehavior if not explicitly set in * Settings.Global. */ @@ -583,11 +595,22 @@ final class ActivityManagerConstants extends ContentObserver { // the foreground state. volatile long mFgsNotificationDeferralInterval = 10_000; + /** + * Same as {@link #mFgsNotificationDeferralInterval} but used for "short FGS". + */ + volatile long mFgsNotificationDeferralIntervalForShort = mFgsNotificationDeferralInterval; + // Rate limit: minimum time after an app's FGS notification is deferred // before another FGS notification from that app can be deferred. volatile long mFgsNotificationDeferralExclusionTime = 2 * 60 * 1000L; /** + * Same as {@link #mFgsNotificationDeferralExclusionTime} but used for "short FGS". + */ + volatile long mFgsNotificationDeferralExclusionTimeForShort = + mFgsNotificationDeferralExclusionTime; + + /** * When server pushing message is over the quote, select one of the temp allow list type as * defined in {@link PowerExemptionManager.TempAllowListType} */ @@ -923,6 +946,32 @@ final class ActivityManagerConstants extends ContentObserver { public static boolean PROACTIVE_KILLS_ENABLED = DEFAULT_PROACTIVE_KILLS_ENABLED; public static float LOW_SWAP_THRESHOLD_PERCENT = DEFAULT_LOW_SWAP_THRESHOLD_PERCENT; + /** Timeout for a "short service" FGS, in milliseconds. */ + private static final String KEY_SHORT_FGS_TIMEOUT_DURATION = + "short_fgs_timeout_duration"; + + /** @see #KEY_SHORT_FGS_TIMEOUT_DURATION */ + static final long DEFAULT_SHORT_FGS_TIMEOUT_DURATION = 60_000; + + /** @see #KEY_SHORT_FGS_TIMEOUT_DURATION */ + public static volatile long mShortFgsTimeoutDuration = DEFAULT_SHORT_FGS_TIMEOUT_DURATION; + + /** + * If a "short service" doesn't finish within this after the timeout ( + * {@link #KEY_SHORT_FGS_TIMEOUT_DURATION}), then we'll declare an ANR. + * i.e. if the timeout is 60 seconds, and this ANR extra duration is 5 seconds, then + * the app will be ANR'ed in 65 seconds after a short service starts and it's not stopped. + */ + private static final String KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION = + "short_fgs_anr_extra_wait_duration"; + + /** @see #KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION */ + static final long DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION = 5_000; + + /** @see #KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION */ + public static volatile long mShortFgsAnrExtraWaitDuration = + DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION; + private final OnPropertiesChangedListener mOnDeviceConfigChangedListener = new OnPropertiesChangedListener() { @Override @@ -962,6 +1011,12 @@ final class ActivityManagerConstants extends ContentObserver { case KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME: updateFgsNotificationDeferralExclusionTime(); break; + case KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL_FOR_SHORT: + updateFgsNotificationDeferralIntervalForShort(); + break; + case KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME_FOR_SHORT: + updateFgsNotificationDeferralExclusionTimeForShort(); + break; case KEY_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR: updatePushMessagingOverQuotaBehavior(); break; @@ -1058,6 +1113,12 @@ final class ActivityManagerConstants extends ContentObserver { case KEY_MAX_SERVICE_CONNECTIONS_PER_PROCESS: updateMaxServiceConnectionsPerProcess(); break; + case KEY_SHORT_FGS_TIMEOUT_DURATION: + updateShortFgsTimeoutDuration(); + break; + case KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION: + updateShortFgsAnrExtraWaitDuration(); + break; case KEY_PROACTIVE_KILLS_ENABLED: updateProactiveKillsEnabled(); break; @@ -1374,6 +1435,13 @@ final class ActivityManagerConstants extends ContentObserver { /*default value*/ 10_000L); } + private void updateFgsNotificationDeferralIntervalForShort() { + mFgsNotificationDeferralIntervalForShort = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL_FOR_SHORT, + /*default value*/ 10_000L); + } + private void updateFgsNotificationDeferralExclusionTime() { mFgsNotificationDeferralExclusionTime = DeviceConfig.getLong( DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, @@ -1381,6 +1449,13 @@ final class ActivityManagerConstants extends ContentObserver { /*default value*/ 2 * 60 * 1000L); } + private void updateFgsNotificationDeferralExclusionTimeForShort() { + mFgsNotificationDeferralExclusionTimeForShort = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME_FOR_SHORT, + /*default value*/ 2 * 60 * 1000L); + } + private void updatePushMessagingOverQuotaBehavior() { mPushMessagingOverQuotaBehavior = DeviceConfig.getInt( DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, @@ -1746,6 +1821,20 @@ final class ActivityManagerConstants extends ContentObserver { DEFAULT_MAX_SERVICE_CONNECTIONS_PER_PROCESS); } + private void updateShortFgsTimeoutDuration() { + mShortFgsTimeoutDuration = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_SHORT_FGS_TIMEOUT_DURATION, + DEFAULT_SHORT_FGS_TIMEOUT_DURATION); + } + + private void updateShortFgsAnrExtraWaitDuration() { + mShortFgsAnrExtraWaitDuration = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION, + DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION); + } + @NeverCompile // Avoid size overhead of debugging code. void dump(PrintWriter pw) { pw.println("ACTIVITY MANAGER SETTINGS (dumpsys activity settings) " @@ -1903,6 +1992,26 @@ final class ActivityManagerConstants extends ContentObserver { pw.print(" "); pw.print(KEY_LOW_SWAP_THRESHOLD_PERCENT); pw.print("="); pw.println(LOW_SWAP_THRESHOLD_PERCENT); + pw.print(" "); pw.print(KEY_DEFERRED_FGS_NOTIFICATIONS_ENABLED); + pw.print("="); pw.println(mFlagFgsNotificationDeferralEnabled); + pw.print(" "); pw.print(KEY_DEFERRED_FGS_NOTIFICATIONS_API_GATED); + pw.print("="); pw.println(mFlagFgsNotificationDeferralApiGated); + + pw.print(" "); pw.print(KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL); + pw.print("="); pw.println(mFgsNotificationDeferralInterval); + pw.print(" "); pw.print(KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL_FOR_SHORT); + pw.print("="); pw.println(mFgsNotificationDeferralIntervalForShort); + + pw.print(" "); pw.print(KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME); + pw.print("="); pw.println(mFgsNotificationDeferralExclusionTime); + pw.print(" "); pw.print(KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME_FOR_SHORT); + pw.print("="); pw.println(mFgsNotificationDeferralExclusionTimeForShort); + + pw.print(" "); pw.print(KEY_SHORT_FGS_TIMEOUT_DURATION); + pw.print("="); pw.println(mShortFgsTimeoutDuration); + pw.print(" "); pw.print(KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION); + pw.print("="); pw.println(mShortFgsAnrExtraWaitDuration); + pw.println(); if (mOverrideMaxCachedProcesses >= 0) { pw.print(" mOverrideMaxCachedProcesses="); pw.println(mOverrideMaxCachedProcesses); diff --git a/services/core/java/com/android/server/am/ActivityManagerLocal.java b/services/core/java/com/android/server/am/ActivityManagerLocal.java index 1d2c36b63bda..9f2cc7f9cb44 100644 --- a/services/core/java/com/android/server/am/ActivityManagerLocal.java +++ b/services/core/java/com/android/server/am/ActivityManagerLocal.java @@ -17,6 +17,7 @@ package com.android.server.am; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.content.Context; @@ -92,4 +93,28 @@ public interface ActivityManagerLocal { int clientAppUid, @NonNull String clientAppPackage, @NonNull String processName, @Context.BindServiceFlags int flags) throws RemoteException; + + /** + * Start a foreground service delegate. + * @param options foreground service delegate options. + * @param connection a service connection served as callback to caller. + * @return true if delegate is started successfully, false otherwise. + * @hide + */ + boolean startForegroundServiceDelegate(@NonNull ForegroundServiceDelegationOptions options, + @Nullable ServiceConnection connection); + + /** + * Stop a foreground service delegate. + * @param options the foreground service delegate options. + * @hide + */ + void stopForegroundServiceDelegate(@NonNull ForegroundServiceDelegationOptions options); + + /** + * Stop a foreground service delegate by service connection. + * @param connection service connection used to start delegate previously. + * @hide + */ + void stopForegroundServiceDelegate(@NonNull ServiceConnection connection); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index d44b727d724b..39f6ef2290f8 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -181,6 +181,7 @@ import android.app.ApplicationErrorReport; import android.app.ApplicationExitInfo; import android.app.ApplicationThreadConstants; import android.app.BroadcastOptions; +import android.app.ComponentOptions; import android.app.ContentProviderHolder; import android.app.IActivityController; import android.app.IActivityManager; @@ -8400,13 +8401,10 @@ public class ActivityManagerService extends IActivityManager.Stub // On Automotive / Headless System User Mode, at this point the system user has already been // started and unlocked, and some of the tasks we do here have already been done. So skip - // those in that case. + // those in that case. The duplicate system user start is guarded in SystemServiceManager. // TODO(b/242195409): this workaround shouldn't be necessary once we move the headless-user - // start logic to UserManager-land - final boolean bootingSystemUser = currentUserId == UserHandle.USER_SYSTEM; - if (bootingSystemUser) { - mUserController.onSystemUserStarting(); - } + // start logic to UserManager-land. + mUserController.onSystemUserStarting(); synchronized (this) { // Only start up encryption-aware persistent apps; once user is @@ -8436,7 +8434,15 @@ public class ActivityManagerService extends IActivityManager.Stub t.traceEnd(); } - if (bootingSystemUser) { + // Some systems - like automotive - will explicitly unlock system user then switch + // to a secondary user. Hence, we don't want to send duplicate broadcasts for + // the system user here. + // TODO(b/242195409): this workaround shouldn't be necessary once we move + // the headless-user start logic to UserManager-land. + final boolean isBootingSystemUser = (currentUserId == UserHandle.USER_SYSTEM) + && !UserManager.isHeadlessSystemUserMode(); + + if (isBootingSystemUser) { t.traceBegin("startHomeOnAllDisplays"); mAtmInternal.startHomeOnAllDisplays(currentUserId, "systemReady"); t.traceEnd(); @@ -8447,7 +8453,7 @@ public class ActivityManagerService extends IActivityManager.Stub t.traceEnd(); - if (bootingSystemUser) { + if (isBootingSystemUser) { t.traceBegin("sendUserStartBroadcast"); final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); @@ -8488,7 +8494,7 @@ public class ActivityManagerService extends IActivityManager.Stub mAtmInternal.resumeTopActivities(false /* scheduleIdle */); t.traceEnd(); - if (bootingSystemUser) { + if (isBootingSystemUser) { t.traceBegin("sendUserSwitchBroadcasts"); mUserController.sendUserSwitchBroadcasts(-1, currentUserId); t.traceEnd(); @@ -13504,9 +13510,19 @@ public class ActivityManagerService extends IActivityManager.Stub // Don't enforce the flag check if we're EITHER registering for only protected // broadcasts, or the receiver is null (a sticky broadcast). Sticky broadcasts should // not be used generally, so we will be marking them as exported by default - final boolean requireExplicitFlagForDynamicReceivers = CompatChanges.isChangeEnabled( + boolean requireExplicitFlagForDynamicReceivers = CompatChanges.isChangeEnabled( DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED, callingUid) && mConstants.mEnforceReceiverExportedFlagRequirement; + // STOPSHIP(b/259139792): Allow apps that are currently targeting U and in process of + // updating their receivers to be exempt from this requirement until their receivers + // are flagged. + if (requireExplicitFlagForDynamicReceivers) { + if ("com.google.android.apps.messaging".equals(callerPackage)) { + // Note, a versionCode check for this package is not performed because it could + // cause breakage with a subsequent update outside the system image. + requireExplicitFlagForDynamicReceivers = false; + } + } if (!onlyProtectedBroadcasts) { if (receiver == null && !explicitExportStateDefined) { // sticky broadcast, no flag specified (flag isn't required) @@ -13899,10 +13915,10 @@ public class ActivityManagerService extends IActivityManager.Stub throw new SecurityException( "Non-system callers may not flag broadcasts as alarm"); } - if (options.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) { + if (options.containsKey(ComponentOptions.KEY_INTERACTIVE)) { enforceCallingPermission( - android.Manifest.permission.BROADCAST_OPTION_INTERACTIVE, - "setInteractiveBroadcast"); + android.Manifest.permission.COMPONENT_OPTION_INTERACTIVE, + "setInteractive"); } } } @@ -18110,6 +18126,30 @@ public class ActivityManagerService extends IActivityManager.Stub mUidObserverController.register(observer, which, cutpoint, callingPackage, Binder.getCallingUid()); } + + @Override + public boolean startForegroundServiceDelegate( + @NonNull ForegroundServiceDelegationOptions options, + @Nullable ServiceConnection connection) { + synchronized (ActivityManagerService.this) { + return mServices.startForegroundServiceDelegateLocked(options, connection); + } + } + + @Override + public void stopForegroundServiceDelegate( + @NonNull ForegroundServiceDelegationOptions options) { + synchronized (ActivityManagerService.this) { + mServices.stopForegroundServiceDelegateLocked(options); + } + } + + @Override + public void stopForegroundServiceDelegate(@NonNull ServiceConnection connection) { + synchronized (ActivityManagerService.this) { + mServices.stopForegroundServiceDelegateLocked(connection); + } + } } long inputDispatchingTimedOut(int pid, final boolean aboveSystem, TimeoutRecord timeoutRecord) { @@ -18299,6 +18339,59 @@ public class ActivityManagerService extends IActivityManager.Stub } /** + * Start/stop foreground service delegate on a app's process. + * This interface is intended for the shell command to use. + */ + void setForegroundServiceDelegate(String packageName, int uid, boolean isStart, + @ForegroundServiceDelegationOptions.DelegationService int delegateService, + String clientInstanceName) { + final int callingUid = Binder.getCallingUid(); + if (callingUid != SYSTEM_UID && callingUid != ROOT_UID && callingUid != SHELL_UID) { + throw new SecurityException( + "No permission to start/stop foreground service delegate"); + } + final long callingId = Binder.clearCallingIdentity(); + try { + boolean foundPid = false; + synchronized (this) { + ArrayList<ForegroundServiceDelegationOptions> delegates = new ArrayList<>(); + synchronized (mPidsSelfLocked) { + for (int i = 0; i < mPidsSelfLocked.size(); i++) { + final ProcessRecord p = mPidsSelfLocked.valueAt(i); + final IApplicationThread thread = p.getThread(); + if (p.uid == uid && thread != null) { + foundPid = true; + int pid = mPidsSelfLocked.keyAt(i); + ForegroundServiceDelegationOptions options = + new ForegroundServiceDelegationOptions(pid, uid, packageName, + null /* clientAppThread */, + false /* isSticky */, + clientInstanceName, 0 /* foregroundServiceType */, + delegateService); + delegates.add(options); + } + } + } + for (int i = delegates.size() - 1; i >= 0; i--) { + final ForegroundServiceDelegationOptions options = delegates.get(i); + if (isStart) { + ((ActivityManagerLocal) mInternal).startForegroundServiceDelegate(options, + null /* connection */); + } else { + ((ActivityManagerLocal) mInternal).stopForegroundServiceDelegate(options); + } + } + } + if (!foundPid) { + Slog.e(TAG, "setForegroundServiceDelegate can not find process for packageName:" + + packageName + " uid:" + uid); + } + } finally { + Binder.restoreCallingIdentity(callingId); + } + } + + /** * Force the settings cache to be loaded */ void refreshSettingsCache() { diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index e4f947da868e..10f5a369824f 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -369,6 +369,8 @@ final class ActivityManagerShellCommand extends ShellCommand { return runResetDropboxRateLimiter(); case "list-secondary-displays-for-starting-users": return runListSecondaryDisplaysForStartingUsers(pw); + case "set-foreground-service-delegate": + return runSetForegroundServiceDelegate(pw); default: return handleDefaultCommands(cmd); } @@ -3592,6 +3594,45 @@ final class ActivityManagerShellCommand extends ShellCommand { return 0; } + int runSetForegroundServiceDelegate(PrintWriter pw) throws RemoteException { + int userId = UserHandle.USER_CURRENT; + + String opt; + while ((opt = getNextOption()) != null) { + if (opt.equals("--user")) { + userId = UserHandle.parseUserArg(getNextArgRequired()); + } else { + getErrPrintWriter().println("Error: Unknown option: " + opt); + return -1; + } + } + final String packageName = getNextArgRequired(); + final String action = getNextArgRequired(); + boolean isStart = true; + if ("start".equals(action)) { + isStart = true; + } else if ("stop".equals(action)) { + isStart = false; + } else { + pw.println("Error: action is either start or stop"); + return -1; + } + + int uid = INVALID_UID; + try { + final PackageManager pm = mInternal.mContext.getPackageManager(); + uid = pm.getPackageUidAsUser(packageName, + PackageManager.PackageInfoFlags.of(MATCH_ANY_USER), userId); + } catch (PackageManager.NameNotFoundException e) { + pw.println("Error: userId:" + userId + " package:" + packageName + " is not found"); + return -1; + } + mInternal.setForegroundServiceDelegate(packageName, uid, isStart, + ForegroundServiceDelegationOptions.DELEGATION_SERVICE_SPECIAL_USE, + "FgsDelegate"); + return 0; + } + int runResetDropboxRateLimiter() throws RemoteException { mInternal.resetDropboxRateLimiter(); return 0; @@ -3968,6 +4009,8 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println(" list-secondary-displays-for-starting-users"); pw.println(" Lists the id of displays that can be used to start users on " + "background."); + pw.println(" set-foreground-service-delegate [--user <USER_ID>] <PACKAGE> start|stop"); + pw.println(" Start/stop an app's foreground service delegate."); pw.println(); Intent.printIntentArgsHelp(pw, ""); } diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 739d2777fe17..7d9b4776bf37 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -596,20 +596,21 @@ class BroadcastProcessQueue { * barrier timestamp that are still waiting to be delivered. */ public boolean isBeyondBarrierLocked(@UptimeMillisLong long barrierTime) { - if (mActive != null) { - return mActive.enqueueTime > barrierTime; - } final SomeArgs next = mPending.peekFirst(); final SomeArgs nextUrgent = mPendingUrgent.peekFirst(); final SomeArgs nextOffload = mPendingOffload.peekFirst(); - // Empty queue is past any barrier - final boolean nextLater = (next == null) + + // Empty records are always past any barrier + final boolean activeBeyond = (mActive == null) + || mActive.enqueueTime > barrierTime; + final boolean nextBeyond = (next == null) || ((BroadcastRecord) next.arg1).enqueueTime > barrierTime; - final boolean nextUrgentLater = (nextUrgent == null) + final boolean nextUrgentBeyond = (nextUrgent == null) || ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime; - final boolean nextOffloadLater = (nextOffload == null) + final boolean nextOffloadBeyond = (nextOffload == null) || ((BroadcastRecord) nextOffload.arg1).enqueueTime > barrierTime; - return nextLater && nextUrgentLater && nextOffloadLater; + + return activeBeyond && nextBeyond && nextUrgentBeyond && nextOffloadBeyond; } public boolean isRunnable() { diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index 454b28402ced..fb7e0be69d78 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -51,7 +51,6 @@ import android.content.ContentResolver; import android.content.IIntentReceiver; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; @@ -1639,9 +1638,8 @@ public class BroadcastQueueImpl extends BroadcastQueue { } Slog.w(TAG, "Receiver during timeout of " + r + " : " + curReceiver); logBroadcastReceiverDiscardLocked(r); - String anrMessage = - "Broadcast of " + r.intent.toString() + ", waited " + timeoutDurationMs + "ms"; - TimeoutRecord timeoutRecord = TimeoutRecord.forBroadcastReceiver(anrMessage); + TimeoutRecord timeoutRecord = TimeoutRecord.forBroadcastReceiver(r.intent, + timeoutDurationMs); if (curReceiver != null && curReceiver instanceof BroadcastFilter) { BroadcastFilter bf = (BroadcastFilter) curReceiver; if (bf.receiverList.pid != 0 diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 008d95a51cd9..a7d843361762 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -907,8 +907,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (deliveryState == BroadcastRecord.DELIVERY_TIMEOUT) { r.anrCount++; if (app != null && !app.isDebugging()) { - mService.appNotResponding(queue.app, TimeoutRecord - .forBroadcastReceiver("Broadcast of " + r.toShortString())); + mService.appNotResponding(queue.app, TimeoutRecord.forBroadcastReceiver(r.intent)); } } else { mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_SOFT, queue); diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 6ea2dee5b578..84d744270e82 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -397,7 +397,7 @@ final class BroadcastRecord extends Binder { alarm = options != null && options.isAlarmBroadcast(); pushMessage = options != null && options.isPushMessagingBroadcast(); pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast(); - interactive = options != null && options.isInteractiveBroadcast(); + interactive = options != null && options.isInteractive(); this.filterExtrasForReceiver = filterExtrasForReceiver; } diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index e34cd12d0ec3..b98639e31d00 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -37,6 +37,7 @@ import android.provider.DeviceConfig.Properties; import android.provider.Settings; import android.text.TextUtils; import android.util.EventLog; +import android.util.IntArray; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -2110,15 +2111,28 @@ public final class CachedAppOptimizer { @GuardedBy({"mAm"}) @Override - public void onBlockingFileLock(int pid) { + public void onBlockingFileLock(IntArray pids) { if (DEBUG_FREEZER) { - Slog.d(TAG_AM, "Process (pid=" + pid + ") holds blocking file lock"); + Slog.d(TAG_AM, "Blocking file lock found: " + pids); } synchronized (mProcLock) { + int pid = pids.get(0); ProcessRecord app = mFrozenProcesses.get(pid); + ProcessRecord pr; if (app != null) { - Slog.i(TAG_AM, app.processName + " (" + pid + ") holds blocking file lock"); - unfreezeAppLSP(app, OomAdjuster.OOM_ADJ_REASON_NONE); + for (int i = 1; i < pids.size(); i++) { + int blocked = pids.get(i); + synchronized (mAm.mPidsSelfLocked) { + pr = mAm.mPidsSelfLocked.get(blocked); + } + if (pr != null && pr.mState.getCurAdj() < ProcessList.CACHED_APP_MIN_ADJ) { + Slog.d(TAG_AM, app.processName + " (" + pid + ") blocks " + + pr.processName + " (" + blocked + ")"); + // Found at least one blocked non-cached process + unfreezeAppLSP(app, OomAdjuster.OOM_ADJ_REASON_NONE); + break; + } + } } } } diff --git a/services/core/java/com/android/server/am/ForegroundServiceDelegation.java b/services/core/java/com/android/server/am/ForegroundServiceDelegation.java new file mode 100644 index 000000000000..a051d174e1a5 --- /dev/null +++ b/services/core/java/com/android/server/am/ForegroundServiceDelegation.java @@ -0,0 +1,40 @@ +/* + * 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.am; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.IBinder; + +/** + * A foreground service delegate which has client options and connection callback. + */ +public class ForegroundServiceDelegation { + public final IBinder mBinder = new Binder(); + @NonNull + public final ForegroundServiceDelegationOptions mOptions; + @Nullable + public final ServiceConnection mConnection; + + public ForegroundServiceDelegation(@NonNull ForegroundServiceDelegationOptions options, + @Nullable ServiceConnection connection) { + mOptions = options; + mConnection = connection; + } +} diff --git a/services/core/java/com/android/server/am/ForegroundServiceDelegationOptions.java b/services/core/java/com/android/server/am/ForegroundServiceDelegationOptions.java new file mode 100644 index 000000000000..5eb5a55d2c77 --- /dev/null +++ b/services/core/java/com/android/server/am/ForegroundServiceDelegationOptions.java @@ -0,0 +1,262 @@ +/* + * 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.am; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.IApplicationThread; +import android.content.ComponentName; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A service module such as MediaSessionService, VOIP, Camera, Microphone, Location can ask + * ActivityManagerService to start a foreground service delegate on behalf of the actual app, + * by which the client app's process state can be promoted to FOREGROUND_SERVICE process state which + * is higher than the app's actual process state if the app is in the background. This can help to + * keep the app in the memory and extra run-time. + * The app does not need to define an actual service component nor add it into manifest file. + */ +public class ForegroundServiceDelegationOptions { + + public static final int DELEGATION_SERVICE_DEFAULT = 0; + public static final int DELEGATION_SERVICE_DATA_SYNC = 1; + public static final int DELEGATION_SERVICE_MEDIA_PLAYBACK = 2; + public static final int DELEGATION_SERVICE_PHONE_CALL = 3; + public static final int DELEGATION_SERVICE_LOCATION = 4; + public static final int DELEGATION_SERVICE_CONNECTED_DEVICE = 5; + public static final int DELEGATION_SERVICE_MEDIA_PROJECTION = 6; + public static final int DELEGATION_SERVICE_CAMERA = 7; + public static final int DELEGATION_SERVICE_MICROPHONE = 8; + public static final int DELEGATION_SERVICE_HEALTH = 9; + public static final int DELEGATION_SERVICE_REMOTE_MESSAGING = 10; + public static final int DELEGATION_SERVICE_SYSTEM_EXEMPTED = 11; + public static final int DELEGATION_SERVICE_SPECIAL_USE = 12; + + @IntDef(flag = false, prefix = { "DELEGATION_SERVICE_" }, value = { + DELEGATION_SERVICE_DEFAULT, + DELEGATION_SERVICE_DATA_SYNC, + DELEGATION_SERVICE_MEDIA_PLAYBACK, + DELEGATION_SERVICE_PHONE_CALL, + DELEGATION_SERVICE_LOCATION, + DELEGATION_SERVICE_CONNECTED_DEVICE, + DELEGATION_SERVICE_MEDIA_PROJECTION, + DELEGATION_SERVICE_CAMERA, + DELEGATION_SERVICE_MICROPHONE, + DELEGATION_SERVICE_HEALTH, + DELEGATION_SERVICE_REMOTE_MESSAGING, + DELEGATION_SERVICE_SYSTEM_EXEMPTED, + DELEGATION_SERVICE_SPECIAL_USE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DelegationService {} + + // The actual app's PID + public final int mClientPid; + // The actual app's UID + public final int mClientUid; + // The actual app's package name + @NonNull + public final String mClientPackageName; + // The actual app's app thread + @Nullable + public final IApplicationThread mClientAppThread; + public final boolean mSticky; // Is it a sticky service + + // The delegation service's instance name which is to identify the delegate. + @NonNull + public String mClientInstanceName; + // The foreground service types it consists of. + public final int mForegroundServiceTypes; + /** + * The service's name such as MediaSessionService, VOIP, Camera, Microphone, Location. This is + * the internal module's name which actually starts the FGS delegate on behalf of the client + * app. + */ + public final @DelegationService int mDelegationService; + + public ForegroundServiceDelegationOptions(int clientPid, + int clientUid, + @NonNull String clientPackageName, + @NonNull IApplicationThread clientAppThread, + boolean isSticky, + @NonNull String clientInstanceName, + int foregroundServiceTypes, + @DelegationService int delegationService) { + mClientPid = clientPid; + mClientUid = clientUid; + mClientPackageName = clientPackageName; + mClientAppThread = clientAppThread; + mSticky = isSticky; + mClientInstanceName = clientInstanceName; + mForegroundServiceTypes = foregroundServiceTypes; + mDelegationService = delegationService; + } + + /** + * A service delegates a foreground service state to a clientUID using a instanceName. + * This delegation is uniquely identified by + * mDelegationService/mClientUid/mClientPid/mClientInstanceName + */ + public boolean isSameDelegate(ForegroundServiceDelegationOptions that) { + return this.mDelegationService == that.mDelegationService + && this.mClientUid == that.mClientUid + && this.mClientPid == that.mClientPid + && this.mClientInstanceName.equals(that.mClientInstanceName); + } + + /** + * Construct a component name for this delegate. + */ + public ComponentName getComponentName() { + return new ComponentName(mClientPackageName, serviceCodeToString(mDelegationService) + + ":" + mClientInstanceName); + } + + /** + * Get string description of this delegate options. + */ + public String getDescription() { + StringBuilder sb = new StringBuilder(128); + sb.append("ForegroundServiceDelegate{") + .append("package:") + .append(mClientPackageName) + .append(",") + .append("service:") + .append(serviceCodeToString(mDelegationService)) + .append(",") + .append("uid:") + .append(mClientUid) + .append(",") + .append("pid:") + .append(mClientPid) + .append(",") + .append("instance:") + .append(mClientInstanceName) + .append("}"); + return sb.toString(); + } + + /** + * Map the integer service code to string name. + * @param serviceCode + * @return + */ + public static String serviceCodeToString(@DelegationService int serviceCode) { + switch (serviceCode) { + case DELEGATION_SERVICE_DEFAULT: + return "DEFAULT"; + case DELEGATION_SERVICE_DATA_SYNC: + return "DATA_SYNC"; + case DELEGATION_SERVICE_MEDIA_PLAYBACK: + return "MEDIA_PLAYBACK"; + case DELEGATION_SERVICE_PHONE_CALL: + return "PHONE_CALL"; + case DELEGATION_SERVICE_LOCATION: + return "LOCATION"; + case DELEGATION_SERVICE_CONNECTED_DEVICE: + return "CONNECTED_DEVICE"; + case DELEGATION_SERVICE_MEDIA_PROJECTION: + return "MEDIA_PROJECTION"; + case DELEGATION_SERVICE_CAMERA: + return "CAMERA"; + case DELEGATION_SERVICE_MICROPHONE: + return "MICROPHONE"; + case DELEGATION_SERVICE_HEALTH: + return "HEALTH"; + case DELEGATION_SERVICE_REMOTE_MESSAGING: + return "REMOTE_MESSAGING"; + case DELEGATION_SERVICE_SYSTEM_EXEMPTED: + return "SYSTEM_EXEMPTED"; + case DELEGATION_SERVICE_SPECIAL_USE: + return "SPECIAL_USE"; + default: + return "(unknown:" + serviceCode + ")"; + } + } + + public static class Builder { + int mClientPid; // The actual app PID + int mClientUid; // The actual app UID + String mClientPackageName; // The actual app's package name + int mClientNotificationId; // The actual app's notification + IApplicationThread mClientAppThread; // The actual app's app thread + boolean mSticky; // Is it a sticky service + String mClientInstanceName; // The delegation service instance name + int mForegroundServiceTypes; // The foreground service types it consists of + @DelegationService int mDelegationService; // The internal service's name, i.e. VOIP + + public Builder setClientPid(int clientPid) { + mClientPid = clientPid; + return this; + } + + public Builder setClientUid(int clientUid) { + mClientUid = clientUid; + return this; + } + + public Builder setClientPackageName(@NonNull String clientPackageName) { + mClientPackageName = clientPackageName; + return this; + } + + public Builder setClientNotificationId(int clientNotificationId) { + mClientNotificationId = clientNotificationId; + return this; + } + + public Builder setClientAppThread(@NonNull IApplicationThread clientAppThread) { + mClientAppThread = clientAppThread; + return this; + } + + public Builder setClientInstanceName(@NonNull String clientInstanceName) { + mClientInstanceName = clientInstanceName; + return this; + } + + public Builder setSticky(boolean isSticky) { + mSticky = isSticky; + return this; + } + + public Builder setForegroundServiceTypes(int foregroundServiceTypes) { + mForegroundServiceTypes = foregroundServiceTypes; + return this; + } + + public Builder setDelegationService(@DelegationService int delegationService) { + mDelegationService = delegationService; + return this; + } + + public ForegroundServiceDelegationOptions build() { + return new ForegroundServiceDelegationOptions(mClientPid, + mClientUid, + mClientPackageName, + mClientAppThread, + mSticky, + mClientInstanceName, + mForegroundServiceTypes, + mDelegationService + ); + } + } +} diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 4b82ad863c8e..c27ed7a46844 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -206,6 +206,10 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN // Last time mAllowWhileInUsePermissionInFgs or mAllowStartForeground is set. long mLastSetFgsRestrictionTime; + // This is a service record of a FGS delegate (not a service record of a real service) + boolean mIsFgsDelegate; + @Nullable ForegroundServiceDelegation mFgsDelegation; + String stringName; // caching of toString private int lastStartId; // identifier of most recent start request. @@ -233,6 +237,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN final boolean taskRemoved; final int id; final int callingId; + final String mCallingProcessName; final Intent intent; final NeededUriGrants neededGrants; long deliveredTime; @@ -242,14 +247,16 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN String stringName; // caching of toString - StartItem(ServiceRecord _sr, boolean _taskRemoved, int _id, Intent _intent, - NeededUriGrants _neededGrants, int _callingId) { + StartItem(ServiceRecord _sr, boolean _taskRemoved, int _id, + Intent _intent, NeededUriGrants _neededGrants, int _callingId, + String callingProcessName) { sr = _sr; taskRemoved = _taskRemoved; id = _id; intent = _intent; neededGrants = _neededGrants; callingId = _callingId; + mCallingProcessName = callingProcessName; } UriPermissionOwner getUriPermissionsLocked() { @@ -502,6 +509,9 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN pw.print(" foregroundId="); pw.print(foregroundId); pw.print(" foregroundNoti="); pw.println(foregroundNoti); } + if (mIsFgsDelegate) { + pw.print(prefix); pw.print("isFgsDelegate="); pw.println(mIsFgsDelegate); + } pw.print(prefix); pw.print("createTime="); TimeUtils.formatDuration(createRealTime, nowReal, pw); pw.print(" startingBgTimeout="); @@ -634,7 +644,9 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN serviceInfo.applicationInfo.uid, serviceInfo.applicationInfo.longVersionCode, serviceInfo.processName, serviceInfo.name); - tracker.applyNewOwner(this); + if (tracker != null) { + tracker.applyNewOwner(this); + } } return tracker; } diff --git a/services/core/java/com/android/server/biometrics/TEST_MAPPING b/services/core/java/com/android/server/biometrics/TEST_MAPPING index 36acc3c7344d..8b80674070a4 100644 --- a/services/core/java/com/android/server/biometrics/TEST_MAPPING +++ b/services/core/java/com/android/server/biometrics/TEST_MAPPING @@ -2,6 +2,9 @@ "presubmit": [ { "name": "CtsBiometricsTestCases" + }, + { + "name": "CtsBiometricsHostTestCases" } ] }
\ No newline at end of file diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java index da4361843681..d584c99cea72 100644 --- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java +++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java @@ -120,7 +120,7 @@ final class ALSProbe implements Probe { // if a final consumer is set it will call destroy/disable on the next value if requested if (!mDestroyed && mNextConsumer == null) { - disableLightSensorLoggingLocked(); + disableLightSensorLoggingLocked(false /* destroying */); } } @@ -130,7 +130,7 @@ final class ALSProbe implements Probe { // if a final consumer is set it will call destroy/disable on the next value if requested if (!mDestroyed && mNextConsumer == null) { - disableLightSensorLoggingLocked(); + disableLightSensorLoggingLocked(true /* destroying */); mDestroyed = true; } } @@ -177,11 +177,10 @@ final class ALSProbe implements Probe { final float current = mLastAmbientLux; if (current > -1f) { nextConsumer.consume(current); - } else if (mDestroyed) { - nextConsumer.consume(-1f); } else if (mNextConsumer != null) { mNextConsumer.add(nextConsumer); } else { + mDestroyed = false; mNextConsumer = nextConsumer; enableLightSensorLoggingLocked(); } @@ -199,12 +198,14 @@ final class ALSProbe implements Probe { resetTimerLocked(true /* start */); } - private void disableLightSensorLoggingLocked() { + private void disableLightSensorLoggingLocked(boolean destroying) { resetTimerLocked(false /* start */); if (mEnabled) { mEnabled = false; - mLastAmbientLux = -1; + if (!destroying) { + mLastAmbientLux = -1; + } mSensorManager.unregisterListener(mLightSensorListener); Slog.v(TAG, "Disable ALS: " + mLightSensorListener.hashCode()); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java index 7a13c91a2cf1..d11f0991a5d4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java @@ -22,6 +22,7 @@ import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; import android.hardware.biometrics.face.AuthenticationFrame; import android.hardware.biometrics.face.BaseFrame; +import android.hardware.biometrics.face.EnrollmentFrame; import android.hardware.face.Face; import android.hardware.face.FaceAuthenticationFrame; import android.hardware.face.FaceEnrollFrame; @@ -33,6 +34,7 @@ import android.util.Slog; import com.android.server.biometrics.HardwareAuthTokenUtils; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.EnrollClient; import com.android.server.biometrics.sensors.face.FaceUtils; import java.util.HashSet; @@ -200,22 +202,24 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { } @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) - // TODO(b/178414967): replace with notifyAuthenticationFrame and notifyEnrollmentFrame. @Override public void notifyAcquired(int userId, int acquireInfo) { - super.notifyAcquired_enforcePermission(); BaseFrame data = new BaseFrame(); data.acquiredInfo = (byte) acquireInfo; - AuthenticationFrame authenticationFrame = new AuthenticationFrame(); - authenticationFrame.data = data; - - // TODO(b/178414967): Currently onAuthenticationFrame and onEnrollmentFrame are the same. - // This will need to call the correct callback once the onAcquired callback is removed. - mSensor.getSessionForUser(userId).getHalSessionCallback().onAuthenticationFrame( - authenticationFrame); + if (mSensor.getScheduler().getCurrentClient() instanceof EnrollClient) { + final EnrollmentFrame frame = new EnrollmentFrame(); + frame.data = data; + mSensor.getSessionForUser(userId).getHalSessionCallback() + .onEnrollmentFrame(frame); + } else { + final AuthenticationFrame frame = new AuthenticationFrame(); + frame.data = data; + mSensor.getSessionForUser(userId).getHalSessionCallback() + .onAuthenticationFrame(frame); + } } @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) 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 81b56a310738..d2e572f2f979 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -18,6 +18,7 @@ package com.android.server.companion.virtual; import android.annotation.NonNull; import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.VirtualDeviceParams; import java.util.Set; @@ -109,4 +110,14 @@ public abstract class VirtualDeviceManagerInternal { * Returns true if the {@code displayId} is owned by any virtual device */ public abstract boolean isDisplayOwnedByAnyVirtualDevice(int displayId); + + /** + * Returns the device policy for the given virtual device and policy type. + * + * <p>In case the virtual device identifier is not valid, or there's no explicitly specified + * policy for that device and policy type, then + * {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT} is returned. + */ + public abstract @VirtualDeviceParams.DevicePolicy int getDevicePolicy( + int deviceId, @VirtualDeviceParams.PolicyType int policyType); } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 0741d46b6f29..bc9bc031ca35 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -645,7 +645,8 @@ public class Vpn { .addTransportType(NetworkCapabilities.TRANSPORT_VPN) .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) - .setTransportInfo(new VpnTransportInfo(VpnManager.TYPE_VPN_NONE, null)) + .setTransportInfo(new VpnTransportInfo( + VpnManager.TYPE_VPN_NONE, null /* sessionId */, false /* bypassable */)) .build(); loadAlwaysOnPackage(); @@ -709,7 +710,8 @@ public class Vpn { private void resetNetworkCapabilities() { mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities) .setUids(null) - .setTransportInfo(new VpnTransportInfo(VpnManager.TYPE_VPN_NONE, null)) + .setTransportInfo(new VpnTransportInfo( + VpnManager.TYPE_VPN_NONE, null /* sessionId */, false /* bypassable */)) .build(); } @@ -1567,7 +1569,8 @@ public class Vpn { capsBuilder.setUids(createUserAndRestrictedProfilesRanges(mUserId, mConfig.allowedApplications, mConfig.disallowedApplications)); - capsBuilder.setTransportInfo(new VpnTransportInfo(getActiveVpnType(), mConfig.session)); + capsBuilder.setTransportInfo( + new VpnTransportInfo(getActiveVpnType(), mConfig.session, mConfig.allowBypass)); // Only apps targeting Q and above can explicitly declare themselves as metered. // These VPNs are assumed metered unless they state otherwise. diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 838bb53f0b86..d6f0fd070f94 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1703,6 +1703,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mTempBrightnessEvent.setRbcStrength(mCdsi != null ? mCdsi.getReduceBrightColorsStrength() : -1); mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor); + mTempBrightnessEvent.setWasShortTermModelActive(hadUserBrightnessPoint); // Temporary is what we use during slider interactions. We avoid logging those so that // we don't spam logcat when the slider is being used. boolean tempToTempTransition = @@ -1713,12 +1714,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call || brightnessAdjustmentFlags != 0) { float lastBrightness = mLastBrightnessEvent.getBrightness(); mTempBrightnessEvent.setInitialBrightness(lastBrightness); - mTempBrightnessEvent.setFastAmbientLux( - mAutomaticBrightnessController == null - ? -1f : mAutomaticBrightnessController.getFastAmbientLux()); - mTempBrightnessEvent.setSlowAmbientLux( - mAutomaticBrightnessController == null - ? -1f : mAutomaticBrightnessController.getSlowAmbientLux()); mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness); mLastBrightnessEvent.copyFrom(mTempBrightnessEvent); BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent); @@ -2846,9 +2841,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED, convertToNits(event.getInitialBrightness()), convertToNits(event.getBrightness()), - event.getSlowAmbientLux(), + event.getLux(), event.getPhysicalDisplayId(), - event.isShortTermModelActive(), + event.wasShortTermModelActive(), appliedLowPowerMode, appliedRbcStrength, appliedHbmMaxNits, diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index bf0b388fdb56..300b5895ca98 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -1568,6 +1568,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mTempBrightnessEvent.setRbcStrength(mCdsi != null ? mCdsi.getReduceBrightColorsStrength() : -1); mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor); + mTempBrightnessEvent.setWasShortTermModelActive(hadUserBrightnessPoint); // Temporary is what we use during slider interactions. We avoid logging those so that // we don't spam logcat when the slider is being used. boolean tempToTempTransition = @@ -1578,12 +1579,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal || brightnessAdjustmentFlags != 0) { float lastBrightness = mLastBrightnessEvent.getBrightness(); mTempBrightnessEvent.setInitialBrightness(lastBrightness); - mTempBrightnessEvent.setFastAmbientLux( - mAutomaticBrightnessController == null - ? -1f : mAutomaticBrightnessController.getFastAmbientLux()); - mTempBrightnessEvent.setSlowAmbientLux( - mAutomaticBrightnessController == null - ? -1f : mAutomaticBrightnessController.getSlowAmbientLux()); mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness); mLastBrightnessEvent.copyFrom(mTempBrightnessEvent); BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent); @@ -2517,9 +2512,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED, convertToNits(event.getInitialBrightness()), convertToNits(event.getBrightness()), - event.getSlowAmbientLux(), + event.getLux(), event.getPhysicalDisplayId(), - event.isShortTermModelActive(), + event.wasShortTermModelActive(), appliedLowPowerMode, appliedRbcStrength, appliedHbmMaxNits, @@ -2694,7 +2689,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal int displayId, SensorManager sensorManager) { return new DisplayPowerProximityStateController(wakelockController, displayDeviceConfig, looper, nudgeUpdatePowerState, - displayId, sensorManager); + displayId, sensorManager, /* injector= */ null); } } diff --git a/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java index 5b64dd5ff319..a3433d9570a4 100644 --- a/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java +++ b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java @@ -30,6 +30,7 @@ import android.util.TimeUtils; import android.view.Display; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.utils.SensorUtils; import java.io.PrintWriter; @@ -40,16 +41,22 @@ import java.io.PrintWriter; * state changes. */ public final class DisplayPowerProximityStateController { - private static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 1; + @VisibleForTesting + static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 1; + @VisibleForTesting + static final int PROXIMITY_UNKNOWN = -1; + @VisibleForTesting + static final int PROXIMITY_POSITIVE = 1; + @VisibleForTesting + static final int PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY = 0; + private static final int MSG_IGNORE_PROXIMITY = 2; - private static final int PROXIMITY_UNKNOWN = -1; private static final int PROXIMITY_NEGATIVE = 0; - private static final int PROXIMITY_POSITIVE = 1; private static final boolean DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT = false; // Proximity sensor debounce delay in milliseconds for positive transitions. - private static final int PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY = 0; + // Proximity sensor debounce delay in milliseconds for negative transitions. private static final int PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY = 250; // Trigger proximity if distance is less than 5 cm. @@ -66,12 +73,13 @@ public final class DisplayPowerProximityStateController { private final DisplayPowerProximityStateHandler mHandler; // A runnable to execute the utility to update the power state. private final Runnable mNudgeUpdatePowerState; + private Clock mClock; // A listener which listen's to the events emitted by the proximity sensor. private final SensorEventListener mProximitySensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { if (mProximitySensorEnabled) { - final long time = SystemClock.uptimeMillis(); + final long time = mClock.uptimeMillis(); final float distance = event.values[0]; boolean positive = distance >= 0.0f && distance < mProximityThreshold; handleProximitySensorEvent(time, positive); @@ -147,7 +155,12 @@ public final class DisplayPowerProximityStateController { public DisplayPowerProximityStateController( WakelockController wakeLockController, DisplayDeviceConfig displayDeviceConfig, Looper looper, - Runnable nudgeUpdatePowerState, int displayId, SensorManager sensorManager) { + Runnable nudgeUpdatePowerState, int displayId, SensorManager sensorManager, + Injector injector) { + if (injector == null) { + injector = new Injector(); + } + mClock = injector.createClock(); mWakelockController = wakeLockController; mHandler = new DisplayPowerProximityStateHandler(looper); mNudgeUpdatePowerState = nudgeUpdatePowerState; @@ -239,7 +252,6 @@ public final class DisplayPowerProximityStateController { setProximitySensorEnabled(false); mWaitingForNegativeProximity = false; } - if (mScreenOffBecauseOfProximity && (mProximity != PROXIMITY_POSITIVE || mIgnoreProximityUntilChanged)) { // The screen *was* off due to prox being near, but now it's "far" so lets turn @@ -313,7 +325,7 @@ public final class DisplayPowerProximityStateController { + mSkipRampBecauseOfProximityChangeToNegative); } - private void ignoreProximitySensorUntilChangedInternal() { + void ignoreProximitySensorUntilChangedInternal() { if (!mIgnoreProximityUntilChanged && mProximity == PROXIMITY_POSITIVE) { // Only ignore if it is still reporting positive (near) @@ -414,7 +426,7 @@ public final class DisplayPowerProximityStateController { if (mProximitySensorEnabled && mPendingProximity != PROXIMITY_UNKNOWN && mPendingProximityDebounceTime >= 0) { - final long now = SystemClock.uptimeMillis(); + final long now = mClock.uptimeMillis(); if (mPendingProximityDebounceTime <= now) { if (mProximity != mPendingProximity) { // if the status of the sensor changed, stop ignoring. @@ -473,4 +485,66 @@ public final class DisplayPowerProximityStateController { } } + @VisibleForTesting + boolean getPendingWaitForNegativeProximityLocked() { + synchronized (mLock) { + return mPendingWaitForNegativeProximityLocked; + } + } + + @VisibleForTesting + boolean getWaitingForNegativeProximity() { + return mWaitingForNegativeProximity; + } + + @VisibleForTesting + boolean shouldIgnoreProximityUntilChanged() { + return mIgnoreProximityUntilChanged; + } + + boolean isProximitySensorEnabled() { + return mProximitySensorEnabled; + } + + @VisibleForTesting + Handler getHandler() { + return mHandler; + } + + @VisibleForTesting + int getPendingProximity() { + return mPendingProximity; + } + + @VisibleForTesting + int getProximity() { + return mProximity; + } + + + @VisibleForTesting + long getPendingProximityDebounceTime() { + return mPendingProximityDebounceTime; + } + + @VisibleForTesting + SensorEventListener getProximitySensorListener() { + return mProximitySensorListener; + } + + /** Functional interface for providing time. */ + @VisibleForTesting + interface Clock { + /** + * Returns current time in milliseconds since boot, not counting time spent in deep sleep. + */ + long uptimeMillis(); + } + + @VisibleForTesting + static class Injector { + Clock createClock() { + return () -> SystemClock.uptimeMillis(); + } + } } diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java index e3fa6220edf4..f19852b3eff5 100644 --- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java +++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java @@ -39,8 +39,6 @@ public final class BrightnessEvent { private String mPhysicalDisplayId; private long mTime; private float mLux; - private float mFastAmbientLux; - private float mSlowAmbientLux; private float mPreThresholdLux; private float mInitialBrightness; private float mBrightness; @@ -51,6 +49,7 @@ public final class BrightnessEvent { private int mRbcStrength; private float mThermalMax; private float mPowerFactor; + private boolean mWasShortTermModelActive; private int mFlags; private int mAdjustmentFlags; private boolean mAutomaticBrightnessEnabled; @@ -76,8 +75,6 @@ public final class BrightnessEvent { mTime = that.getTime(); // Lux values mLux = that.getLux(); - mFastAmbientLux = that.getFastAmbientLux(); - mSlowAmbientLux = that.getSlowAmbientLux(); mPreThresholdLux = that.getPreThresholdLux(); // Brightness values mInitialBrightness = that.getInitialBrightness(); @@ -90,6 +87,7 @@ public final class BrightnessEvent { mRbcStrength = that.getRbcStrength(); mThermalMax = that.getThermalMax(); mPowerFactor = that.getPowerFactor(); + mWasShortTermModelActive = that.wasShortTermModelActive(); mFlags = that.getFlags(); mAdjustmentFlags = that.getAdjustmentFlags(); // Auto-brightness setting @@ -105,8 +103,6 @@ public final class BrightnessEvent { mPhysicalDisplayId = ""; // Lux values mLux = 0; - mFastAmbientLux = 0; - mSlowAmbientLux = 0; mPreThresholdLux = 0; // Brightness values mInitialBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; @@ -119,6 +115,7 @@ public final class BrightnessEvent { mRbcStrength = 0; mThermalMax = PowerManager.BRIGHTNESS_MAX; mPowerFactor = 1f; + mWasShortTermModelActive = false; mFlags = 0; mAdjustmentFlags = 0; // Auto-brightness setting @@ -140,10 +137,6 @@ public final class BrightnessEvent { && mDisplayId == that.mDisplayId && mPhysicalDisplayId.equals(that.mPhysicalDisplayId) && Float.floatToRawIntBits(mLux) == Float.floatToRawIntBits(that.mLux) - && Float.floatToRawIntBits(mFastAmbientLux) - == Float.floatToRawIntBits(that.mFastAmbientLux) - && Float.floatToRawIntBits(mSlowAmbientLux) - == Float.floatToRawIntBits(that.mSlowAmbientLux) && Float.floatToRawIntBits(mPreThresholdLux) == Float.floatToRawIntBits(that.mPreThresholdLux) && Float.floatToRawIntBits(mInitialBrightness) @@ -161,6 +154,7 @@ public final class BrightnessEvent { == Float.floatToRawIntBits(that.mThermalMax) && Float.floatToRawIntBits(mPowerFactor) == Float.floatToRawIntBits(that.mPowerFactor) + && mWasShortTermModelActive == that.mWasShortTermModelActive && mFlags == that.mFlags && mAdjustmentFlags == that.mAdjustmentFlags && mAutomaticBrightnessEnabled == that.mAutomaticBrightnessEnabled; @@ -182,14 +176,13 @@ public final class BrightnessEvent { + ", rcmdBrt=" + mRecommendedBrightness + ", preBrt=" + mPreThresholdBrightness + ", lux=" + mLux - + ", fastLux=" + mFastAmbientLux - + ", slowLux=" + mSlowAmbientLux + ", preLux=" + mPreThresholdLux + ", hbmMax=" + mHbmMax + ", hbmMode=" + BrightnessInfo.hbmToString(mHbmMode) + ", rbcStrength=" + mRbcStrength + ", thrmMax=" + mThermalMax + ", powerFactor=" + mPowerFactor + + ", wasShortTermModelActive=" + mWasShortTermModelActive + ", flags=" + flagsToString() + ", reason=" + mReason.toString(mAdjustmentFlags) + ", autoBrightness=" + mAutomaticBrightnessEnabled; @@ -240,22 +233,6 @@ public final class BrightnessEvent { this.mLux = lux; } - public float getFastAmbientLux() { - return mFastAmbientLux; - } - - public void setFastAmbientLux(float mFastAmbientLux) { - this.mFastAmbientLux = mFastAmbientLux; - } - - public float getSlowAmbientLux() { - return mSlowAmbientLux; - } - - public void setSlowAmbientLux(float mSlowAmbientLux) { - this.mSlowAmbientLux = mSlowAmbientLux; - } - public float getPreThresholdLux() { return mPreThresholdLux; } @@ -344,6 +321,20 @@ public final class BrightnessEvent { return (mFlags & FLAG_LOW_POWER_MODE) != 0; } + /** + * Set whether the short term model was active before the brightness event. + */ + public boolean setWasShortTermModelActive(boolean wasShortTermModelActive) { + return this.mWasShortTermModelActive = wasShortTermModelActive; + } + + /** + * Returns whether the short term model was active before the brightness event. + */ + public boolean wasShortTermModelActive() { + return this.mWasShortTermModelActive; + } + public int getFlags() { return mFlags; } @@ -352,10 +343,6 @@ public final class BrightnessEvent { this.mFlags = flags; } - public boolean isShortTermModelActive() { - return (mFlags & FLAG_USER_SET) != 0; - } - public int getAdjustmentFlags() { return mAdjustmentFlags; } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 9bce471fd0cb..8a22ab968165 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -837,7 +837,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { void enableAudioReturnChannel(boolean enabled) { assertRunOnServiceThread(); HdmiDeviceInfo avr = getAvrDeviceInfo(); - if (avr != null) { + if (avr != null && avr.getPortId() != Constants.INVALID_PORT_ID) { mService.enableAudioReturnChannel(avr.getPortId(), enabled); } } @@ -1336,19 +1336,31 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { } @ServiceThreadOnly + private void forceDisableArcOnAllPins() { + List<HdmiPortInfo> ports = mService.getPortInfo(); + for (HdmiPortInfo port : ports) { + if (isArcFeatureEnabled(port.getId())) { + mService.enableAudioReturnChannel(port.getId(), false); + } + } + } + + @ServiceThreadOnly private void disableArcIfExist() { assertRunOnServiceThread(); HdmiDeviceInfo avr = getAvrDeviceInfo(); if (avr == null) { return; } - disableArc(); // Seq #44. removeAllRunningArcAction(); if (!hasAction(RequestArcTerminationAction.class) && isArcEstablished()) { addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress())); } + + // Disable ARC Pin earlier, prevent the case where AVR doesn't send <Terminate ARC> in time + forceDisableArcOnAllPins(); } @ServiceThreadOnly diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index d02faad1956e..25e71e8ceca1 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -2081,9 +2081,11 @@ public class LockSettingsService extends ILockSettings.Stub { public VerifyCredentialResponse checkCredential(LockscreenCredential credential, int userId, ICheckCredentialProgressCallback progressCallback) { checkPasswordReadPermission(); + final long identity = Binder.clearCallingIdentity(); try { return doVerifyCredential(credential, userId, progressCallback, 0 /* flags */); } finally { + Binder.restoreCallingIdentity(identity); scheduleGc(); } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 90135ad7684b..d6b9bd5d3114 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3797,13 +3797,13 @@ public class NotificationManagerService extends SystemService { } private void createNotificationChannelsImpl(String pkg, int uid, - ParceledListSlice channelsList) { - createNotificationChannelsImpl(pkg, uid, channelsList, + ParceledListSlice channelsList, boolean fromTargetApp) { + createNotificationChannelsImpl(pkg, uid, channelsList, fromTargetApp, ActivityTaskManager.INVALID_TASK_ID); } private void createNotificationChannelsImpl(String pkg, int uid, - ParceledListSlice channelsList, int startingTaskId) { + ParceledListSlice channelsList, boolean fromTargetApp, int startingTaskId) { List<NotificationChannel> channels = channelsList.getList(); final int channelsSize = channels.size(); ParceledListSlice<NotificationChannel> oldChannels = @@ -3815,7 +3815,7 @@ public class NotificationManagerService extends SystemService { final NotificationChannel channel = channels.get(i); Objects.requireNonNull(channel, "channel in list is null"); needsPolicyFileChange = mPreferencesHelper.createNotificationChannel(pkg, uid, - channel, true /* fromTargetApp */, + channel, fromTargetApp, mConditionProviders.isPackageOrComponentAllowed( pkg, UserHandle.getUserId(uid))); if (needsPolicyFileChange) { @@ -3851,6 +3851,7 @@ public class NotificationManagerService extends SystemService { @Override public void createNotificationChannels(String pkg, ParceledListSlice channelsList) { checkCallerIsSystemOrSameApp(pkg); + boolean fromTargetApp = !isCallerSystemOrPhone(); // if not system, it's from the app int taskId = ActivityTaskManager.INVALID_TASK_ID; try { int uid = mPackageManager.getPackageUid(pkg, 0, @@ -3859,14 +3860,15 @@ public class NotificationManagerService extends SystemService { } catch (RemoteException e) { // Do nothing } - createNotificationChannelsImpl(pkg, Binder.getCallingUid(), channelsList, taskId); + createNotificationChannelsImpl(pkg, Binder.getCallingUid(), channelsList, fromTargetApp, + taskId); } @Override public void createNotificationChannelsForPackage(String pkg, int uid, ParceledListSlice channelsList) { enforceSystemOrSystemUI("only system can call this"); - createNotificationChannelsImpl(pkg, uid, channelsList); + createNotificationChannelsImpl(pkg, uid, channelsList, false /* fromTargetApp */); } @Override @@ -3881,7 +3883,8 @@ public class NotificationManagerService extends SystemService { CONVERSATION_CHANNEL_ID_FORMAT, parentId, conversationId)); conversationChannel.setConversationId(parentId, conversationId); createNotificationChannelsImpl( - pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel))); + pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel)), + false /* fromTargetApp */); mRankingHandler.requestSort(); handleSavePolicyFile(); } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 1bbcc839ccd1..444fef634de3 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -918,7 +918,7 @@ public class PreferencesHelper implements RankingConfig { throw new IllegalArgumentException("Reserved id"); } NotificationChannel existing = r.channels.get(channel.getId()); - if (existing != null && fromTargetApp) { + if (existing != null) { // Actually modifying an existing channel - keep most of the existing settings if (existing.isDeleted()) { // The existing channel was deleted - undelete it. @@ -1004,9 +1004,7 @@ public class PreferencesHelper implements RankingConfig { } if (fromTargetApp) { channel.setLockscreenVisibility(r.visibility); - channel.setAllowBubbles(existing != null - ? existing.getAllowBubbles() - : NotificationChannel.DEFAULT_ALLOW_BUBBLE); + channel.setAllowBubbles(NotificationChannel.DEFAULT_ALLOW_BUBBLE); } clearLockedFieldsLocked(channel); diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index c97711b3aa80..5b837f1daa6f 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -81,11 +81,8 @@ import com.android.server.utils.Watcher; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.Set; /** * Implementation of the methods that update the internal structures of AppsFilter. Because of the @@ -113,7 +110,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, */ @GuardedBy("mQueryableViaUsesPermissionLock") @NonNull - private HashMap<String, Set<Integer>> mPermissionToUids; + private final ArrayMap<String, ArraySet<Integer>> mPermissionToUids; /** * A cache that maps parsed {@link android.R.styleable#AndroidManifestUsesPermission @@ -123,7 +120,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, */ @GuardedBy("mQueryableViaUsesPermissionLock") @NonNull - private HashMap<String, Set<Integer>> mUsesPermissionToUids; + private final ArrayMap<String, ArraySet<Integer>> mUsesPermissionToUids; /** * Ensures an observer is in the list, exactly once. The observer cannot be null. The @@ -225,8 +222,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, mProtectedBroadcasts = new WatchedArraySet<>(); mProtectedBroadcastsSnapshot = new SnapshotCache.Auto<>( mProtectedBroadcasts, mProtectedBroadcasts, "AppsFilter.mProtectedBroadcasts"); - mPermissionToUids = new HashMap<>(); - mUsesPermissionToUids = new HashMap<>(); + mPermissionToUids = new ArrayMap<>(); + mUsesPermissionToUids = new ArrayMap<>(); mSnapshot = new SnapshotCache<AppsFilterSnapshot>(this, this) { @Override @@ -609,7 +606,10 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, // Lookup in the mPermissionToUids cache if installed packages have // defined this permission. if (mPermissionToUids.containsKey(usesPermissionName)) { - for (int targetAppId : mPermissionToUids.get(usesPermissionName)) { + final ArraySet<Integer> permissionDefiners = + mPermissionToUids.get(usesPermissionName); + for (int j = 0; j < permissionDefiners.size(); j++) { + final int targetAppId = permissionDefiners.valueAt(j); if (targetAppId != newPkgSetting.getAppId()) { mQueryableViaUsesPermission.add(newPkgSetting.getAppId(), targetAppId); @@ -619,7 +619,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, // Record in mUsesPermissionToUids that a permission was requested // by a new package if (!mUsesPermissionToUids.containsKey(usesPermissionName)) { - mUsesPermissionToUids.put(usesPermissionName, new HashSet<>()); + mUsesPermissionToUids.put(usesPermissionName, new ArraySet<>()); } mUsesPermissionToUids.get(usesPermissionName).add(newPkgSetting.getAppId()); } @@ -633,7 +633,10 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, // Lookup in the mUsesPermissionToUids cache if installed packages have // requested this permission. if (mUsesPermissionToUids.containsKey(permissionName)) { - for (int queryingAppId : mUsesPermissionToUids.get(permissionName)) { + final ArraySet<Integer> permissionUsers = mUsesPermissionToUids.get( + permissionName); + for (int j = 0; j < permissionUsers.size(); j++) { + final int queryingAppId = permissionUsers.valueAt(j); if (queryingAppId != newPkgSetting.getAppId()) { mQueryableViaUsesPermission.add(queryingAppId, newPkgSetting.getAppId()); @@ -642,7 +645,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } // Record in mPermissionToUids that a permission was defined by a new package if (!mPermissionToUids.containsKey(permissionName)) { - mPermissionToUids.put(permissionName, new HashSet<>()); + mPermissionToUids.put(permissionName, new ArraySet<>()); } mPermissionToUids.get(permissionName).add(newPkgSetting.getAppId()); } diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java index 88a3f8e4c8c1..095a7f6066b5 100644 --- a/services/core/java/com/android/server/pm/DeletePackageHelper.java +++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java @@ -261,6 +261,7 @@ final class DeletePackageHelper { final boolean killApp = (deleteFlags & PackageManager.DELETE_DONT_KILL_APP) == 0; info.sendPackageRemovedBroadcasts(killApp, removedBySystem); info.sendSystemPackageUpdatedBroadcasts(); + PackageMetrics.onUninstallSucceeded(info, deleteFlags, mUserManagerInternal); } // Force a gc to clear up things. diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 2a0b44d43bd7..70bd24ce20a2 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2619,11 +2619,29 @@ final class InstallPackageHelper { } } + Bundle extras = new Bundle(); + extras.putInt(Intent.EXTRA_UID, request.getUid()); + if (update) { + extras.putBoolean(Intent.EXTRA_REPLACING, true); + } + extras.putInt(PackageInstaller.EXTRA_DATA_LOADER_TYPE, dataLoaderType); + + // If a package is a static shared library, then only the installer of the package + // should get the broadcast. + if (installerPackageName != null + && request.getPkg().getStaticSharedLibraryName() != null) { + mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName, + extras, 0 /*flags*/, + installerPackageName, null /*finishedReceiver*/, + request.getNewUsers(), null /* instantUserIds*/, + null /* broadcastAllowList */, null); + } + // Send installed broadcasts if the package is not a static shared lib. if (request.getPkg().getStaticSharedLibraryName() == null) { mPm.mProcessLoggingHandler.invalidateBaseApkHash(request.getPkg().getBaseApkPath()); - // Send added for users that see the package for the first time + // Send PACKAGE_ADDED broadcast for users that see the package for the first time // sendPackageAddedForNewUsers also deals with system apps int appId = UserHandle.getAppId(request.getUid()); boolean isSystem = request.getPkg().isSystem(); @@ -2631,13 +2649,9 @@ final class InstallPackageHelper { isSystem || virtualPreload, virtualPreload /*startReceiver*/, appId, firstUserIds, firstInstantUserIds, dataLoaderType); - // Send added for users that don't see the package for the first time - Bundle extras = new Bundle(); - extras.putInt(Intent.EXTRA_UID, request.getUid()); - if (update) { - extras.putBoolean(Intent.EXTRA_REPLACING, true); - } - extras.putInt(PackageInstaller.EXTRA_DATA_LOADER_TYPE, dataLoaderType); + // Send PACKAGE_ADDED broadcast for users that don't see + // the package for the first time + // Send to all running apps. final SparseArray<int[]> newBroadcastAllowList; synchronized (mPm.mLock) { @@ -2650,8 +2664,8 @@ final class InstallPackageHelper { extras, 0 /*flags*/, null /*targetPackage*/, null /*finishedReceiver*/, updateUserIds, instantUserIds, newBroadcastAllowList, null); + // Send to the installer, even if it's not running. if (installerPackageName != null) { - // Send to the installer, even if it's not running. mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName, extras, 0 /*flags*/, installerPackageName, null /*finishedReceiver*/, diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index 01a8bd0a4225..4e5a6f9fa2b5 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -16,6 +16,7 @@ package com.android.server.pm; +import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_UNSPECIFIED; import static android.content.pm.PackageManager.INSTALL_REASON_UNKNOWN; import static android.content.pm.PackageManager.INSTALL_SCENARIO_DEFAULT; import static android.content.pm.PackageManager.INSTALL_SUCCEEDED; @@ -114,6 +115,8 @@ final class InstallRequest { @Nullable private final PackageMetrics mPackageMetrics; + private final int mSessionId; + private final int mRequireUserAction; // New install InstallRequest(InstallingSession params) { @@ -128,6 +131,8 @@ final class InstallRequest { params.mDataLoaderType, params.mPackageSource); mPackageMetrics = new PackageMetrics(this); mIsInstallInherit = params.mIsInherit; + mSessionId = params.mSessionId; + mRequireUserAction = params.mRequireUserAction; } // Install existing package as user @@ -141,6 +146,8 @@ final class InstallRequest { mPostInstallRunnable = runnable; mPackageMetrics = new PackageMetrics(this); mIsInstallForUsers = true; + mSessionId = -1; + mRequireUserAction = USER_ACTION_UNSPECIFIED; } // addForInit @@ -158,6 +165,8 @@ final class InstallRequest { mScanFlags = scanFlags; mScanResult = scanResult; mPackageMetrics = null; // No logging from this code path + mSessionId = -1; + mRequireUserAction = USER_ACTION_UNSPECIFIED; } @Nullable @@ -565,6 +574,14 @@ final class InstallRequest { } } + public int getSessionId() { + return mSessionId; + } + + public int getRequireUserAction() { + return mRequireUserAction; + } + public void setScanFlags(int scanFlags) { mScanFlags = scanFlags; } diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java index e4a0a3ab8dfa..d8494dbaa827 100644 --- a/services/core/java/com/android/server/pm/InstallingSession.java +++ b/services/core/java/com/android/server/pm/InstallingSession.java @@ -18,6 +18,7 @@ package com.android.server.pm; import static android.app.AppOpsManager.MODE_DEFAULT; import static android.content.pm.PackageInstaller.SessionParams.MODE_INHERIT_EXISTING; +import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_UNSPECIFIED; import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; import static android.content.pm.PackageManager.INSTALL_STAGED; import static android.content.pm.PackageManager.INSTALL_SUCCEEDED; @@ -95,7 +96,10 @@ class InstallingSession { final InstallPackageHelper mInstallPackageHelper; final RemovePackageHelper mRemovePackageHelper; final boolean mIsInherit; + final int mSessionId; + final int mRequireUserAction; + // For move install InstallingSession(OriginInfo originInfo, MoveInfo moveInfo, IPackageInstallObserver2 observer, int installFlags, InstallSource installSource, String volumeUuid, UserHandle user, String packageAbiOverride, int packageSource, @@ -124,9 +128,11 @@ class InstallingSession { mPackageSource = packageSource; mPackageLite = packageLite; mIsInherit = false; + mSessionId = -1; + mRequireUserAction = USER_ACTION_UNSPECIFIED; } - InstallingSession(File stagedDir, IPackageInstallObserver2 observer, + InstallingSession(int sessionId, File stagedDir, IPackageInstallObserver2 observer, PackageInstaller.SessionParams sessionParams, InstallSource installSource, UserHandle user, SigningDetails signingDetails, int installerUid, PackageLite packageLite, PackageManagerService pm) { @@ -155,6 +161,8 @@ class InstallingSession { mPackageSource = sessionParams.packageSource; mPackageLite = packageLite; mIsInherit = sessionParams.mode == MODE_INHERIT_EXISTING; + mSessionId = sessionId; + mRequireUserAction = sessionParams.requireUserAction; } @Override diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 5d13a45e03e3..a2b462a8d76c 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -2566,8 +2566,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } synchronized (mLock) { - return new InstallingSession(stageDir, localObserver, params, mInstallSource, user, - mSigningDetails, mInstallerUid, mPackageLite, mPm); + return new InstallingSession(sessionId, stageDir, localObserver, params, mInstallSource, + user, mSigningDetails, mInstallerUid, mPackageLite, mPm); } } @@ -3711,7 +3711,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private boolean dispatchPendingAbandonCallback() { final Runnable callback; synchronized (mLock) { - Preconditions.checkState(mStageDirInUse); + if (!mStageDirInUse) { + return false; + } mStageDirInUse = false; callback = mPendingAbandonCallback; mPendingAbandonCallback = null; diff --git a/services/core/java/com/android/server/pm/PackageMetrics.java b/services/core/java/com/android/server/pm/PackageMetrics.java index 0391163856b2..0574f737a54b 100644 --- a/services/core/java/com/android/server/pm/PackageMetrics.java +++ b/services/core/java/com/android/server/pm/PackageMetrics.java @@ -19,12 +19,13 @@ package com.android.server.pm; import static android.os.Process.INVALID_UID; import android.annotation.IntDef; +import android.content.pm.PackageManager; import android.content.pm.parsing.ApkLiteParseUtils; -import android.os.UserManager; import android.util.Pair; import android.util.SparseArray; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.LocalServices; import com.android.server.pm.pkg.PackageStateInternal; import java.io.File; @@ -73,6 +74,8 @@ final class PackageMetrics { } private void reportInstallationStats(Computer snapshot, boolean success) { + UserManagerInternal userManagerInternal = + LocalServices.getService(UserManagerInternal.class); // TODO(b/249294752): do not log if adb final long installDurationMillis = System.currentTimeMillis() - mInstallStartTimestampMillis; @@ -89,13 +92,13 @@ final class PackageMetrics { final long apksSize = getApksSize(ps.getPath()); FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_INSTALLATION_SESSION_REPORTED, - 0 /* session_id */, + mInstallRequest.getSessionId() /* session_id */, success ? null : packageName /* not report package_name on success */, mInstallRequest.getUid() /* uid */, newUsers /* user_ids */, - getUserTypes(newUsers) /* user_types */, + userManagerInternal.getUserTypesForStatsd(newUsers) /* user_types */, originalUsers /* original_user_ids */, - getUserTypes(originalUsers) /* original_user_types */, + userManagerInternal.getUserTypesForStatsd(originalUsers) /* original_user_types */, mInstallRequest.getReturnCode() /* public_return_code */, 0 /* internal_error_code */, apksSize /* apks_size_bytes */, @@ -107,7 +110,7 @@ final class PackageMetrics { installerUid /* installer_package_uid */, -1 /* original_installer_package_uid */, mInstallRequest.getDataLoaderType() /* data_loader_type */, - 0 /* user_action_required_type */, + mInstallRequest.getRequireUserAction() /* user_action_required_type */, mInstallRequest.isInstantInstall() /* is_instant */, mInstallRequest.isInstallReplace() /* is_replace */, mInstallRequest.isInstallSystem() /* is_system */, @@ -163,18 +166,6 @@ final class PackageMetrics { return new Pair<>(stepsArray, durationsArray); } - private static int[] getUserTypes(int[] userIds) { - if (userIds == null) { - return null; - } - final int[] userTypes = new int[userIds.length]; - for (int i = 0; i < userTypes.length; i++) { - String userType = UserManagerService.getInstance().getUserInfo(userIds[i]).userType; - userTypes[i] = UserManager.getUserTypeForStatsd(userType); - } - return userTypes; - } - private static class InstallStep { private final long mStartTimestampMillis; private long mDurationMillis = -1; @@ -191,4 +182,20 @@ final class PackageMetrics { return mDurationMillis; } } + + public static void onUninstallSucceeded(PackageRemovedInfo info, int deleteFlags, + UserManagerInternal userManagerInternal) { + if (info.mIsUpdate) { + // Not logging uninstalls caused by app updates + return; + } + final int[] removedUsers = info.mRemovedUsers; + final int[] removedUserTypes = userManagerInternal.getUserTypesForStatsd(removedUsers); + final int[] originalUsers = info.mOrigUsers; + final int[] originalUserTypes = userManagerInternal.getUserTypesForStatsd(originalUsers); + FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_UNINSTALLATION_REPORTED, + info.mUid, removedUsers, removedUserTypes, originalUsers, originalUserTypes, + deleteFlags, PackageManager.DELETE_SUCCEEDED, info.mIsRemovedPackageSystemUpdate, + !info.mRemovedForAllUsers); + } } diff --git a/services/core/java/com/android/server/pm/PackageRemovedInfo.java b/services/core/java/com/android/server/pm/PackageRemovedInfo.java index 3c863d080d79..4cac1151136d 100644 --- a/services/core/java/com/android/server/pm/PackageRemovedInfo.java +++ b/services/core/java/com/android/server/pm/PackageRemovedInfo.java @@ -111,12 +111,6 @@ final class PackageRemovedInfo { } private void sendPackageRemovedBroadcastInternal(boolean killApp, boolean removedBySystem) { - // Don't send static shared library removal broadcasts as these - // libs are visible only the apps that depend on them an one - // cannot remove the library if it has a dependency. - if (mIsStaticSharedLib) { - return; - } Bundle extras = new Bundle(); final int removedUid = mRemovedAppId >= 0 ? mRemovedAppId : mUid; extras.putInt(Intent.EXTRA_UID, removedUid); @@ -128,15 +122,22 @@ final class PackageRemovedInfo { extras.putBoolean(Intent.EXTRA_REPLACING, true); } extras.putBoolean(Intent.EXTRA_REMOVED_FOR_ALL_USERS, mRemovedForAllUsers); + + // Send PACKAGE_REMOVED broadcast to the respective installer. + if (mRemovedPackage != null && mInstallerPackageName != null) { + mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED, + mRemovedPackage, extras, 0 /*flags*/, + mInstallerPackageName, null, mBroadcastUsers, mInstantUserIds, null, null); + } + if (mIsStaticSharedLib) { + // When uninstalling static shared libraries, only the package's installer needs to be + // sent a PACKAGE_REMOVED broadcast. There are no other intended recipients. + return; + } if (mRemovedPackage != null) { mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED, mRemovedPackage, extras, 0, null /*targetPackage*/, null, mBroadcastUsers, mInstantUserIds, mBroadcastAllowList, null); - if (mInstallerPackageName != null) { - mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED, - mRemovedPackage, extras, 0 /*flags*/, - mInstallerPackageName, null, mBroadcastUsers, mInstantUserIds, null, null); - } mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED_INTERNAL, mRemovedPackage, extras, 0 /*flags*/, PLATFORM_PACKAGE_NAME, null /*finishedReceiver*/, mBroadcastUsers, mInstantUserIds, diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index 9dafcceefdd0..1420cbf3ae45 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -437,4 +437,8 @@ public abstract class UserManagerInternal { /** TODO(b/244333150): temporary method until UserVisibilityMediator handles that logic */ public abstract void onUserVisibilityChanged(@UserIdInt int userId, boolean visible); + + /** Return the integer types of the given user IDs. Only used for reporting metrics to statsd. + */ + public abstract int[] getUserTypesForStatsd(@UserIdInt int[] userIds); } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 4a4a231f4fba..9f84ab030cef 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -6893,6 +6893,24 @@ public class UserManagerService extends IUserManager.Stub { } }); } + + @Override + public int[] getUserTypesForStatsd(@UserIdInt int[] userIds) { + if (userIds == null) { + return null; + } + final int[] userTypes = new int[userIds.length]; + for (int i = 0; i < userTypes.length; i++) { + final UserInfo userInfo = getUserInfo(userIds[i]); + if (userInfo == null) { + // Not possible because the input user ids should all be valid + userTypes[i] = UserManager.getUserTypeForStatsd(""); + } else { + userTypes[i] = UserManager.getUserTypeForStatsd(userInfo.userType); + } + } + return userTypes; + } } // class LocalService 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 916df89d6521..0c5e451a93cc 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -11507,6 +11507,9 @@ public class BatteryStatsImpl extends BatteryStats { mHistory.reset(); + // Store the empty state to disk to ensure consistency + writeSyncLocked(); + // Flush external data, gathering snapshots, but don't process it since it is pre-reset data mIgnoreNextExternalStats = true; mExternalSync.scheduleSync("reset", ExternalStatsSync.UPDATE_ON_RESET); diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 7281a4742b84..50eab256c411 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -272,7 +272,6 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D mContext = context; LocalServices.addService(StatusBarManagerInternal.class, mInternalService); - LocalServices.addService(GlobalActionsProvider.class, mGlobalActionsProvider); // We always have a default display. final UiState state = new UiState(); @@ -289,6 +288,17 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D mSessionMonitor = new SessionMonitor(mContext); } + /** + * Publish the {@link GlobalActionsProvider}. + */ + // TODO(b/259420401): investigate if we can extract GlobalActionsProvider to its own system + // service. + public void publishGlobalActionsProvider() { + if (LocalServices.getService(GlobalActionsProvider.class) == null) { + LocalServices.addService(GlobalActionsProvider.class, mGlobalActionsProvider); + } + } + private IOverlayManager getOverlayManager() { // No need to synchronize; worst-case scenario it will be fetched twice. if (mOverlayManager == null) { diff --git a/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java b/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java index 8218fa5c4643..80d959977b0f 100644 --- a/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java +++ b/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java @@ -19,20 +19,15 @@ package com.android.server.timezonedetector; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; -import android.os.ShellCommand; -import android.os.SystemClock; -import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.StringTokenizer; /** - * A time zone suggestion from the location_time_zone_manager service to the time_zone_detector - * service. + * A time zone suggestion from the location_time_zone_manager service (AKA the location-based time + * zone detection algorithm). * * <p>Geolocation-based suggestions have the following properties: * @@ -63,24 +58,16 @@ import java.util.StringTokenizer; * location_time_zone_manager may become uncertain if components further downstream cannot * determine the device's location with sufficient accuracy, or if the location is known but no * time zone can be determined because no time zone mapping information is available.</li> - * <li>{@code debugInfo} contains debugging metadata associated with the suggestion. This is - * used to record why the suggestion exists and how it was obtained. This information exists - * only to aid in debugging and therefore is used by {@link #toString()}, but it is not for use - * in detection logic and is not considered in {@link #hashCode()} or {@link #equals(Object)}. * </li> * </ul> - * - * @hide */ public final class GeolocationTimeZoneSuggestion { @ElapsedRealtimeLong private final long mEffectiveFromElapsedMillis; @Nullable private final List<String> mZoneIds; - @Nullable private ArrayList<String> mDebugInfo; private GeolocationTimeZoneSuggestion( - @ElapsedRealtimeLong long effectiveFromElapsedMillis, - @Nullable List<String> zoneIds) { + @ElapsedRealtimeLong long effectiveFromElapsedMillis, @Nullable List<String> zoneIds) { mEffectiveFromElapsedMillis = effectiveFromElapsedMillis; if (zoneIds == null) { // Unopinionated @@ -104,8 +91,7 @@ public final class GeolocationTimeZoneSuggestion { */ @NonNull public static GeolocationTimeZoneSuggestion createCertainSuggestion( - @ElapsedRealtimeLong long effectiveFromElapsedMillis, - @NonNull List<String> zoneIds) { + @ElapsedRealtimeLong long effectiveFromElapsedMillis, @NonNull List<String> zoneIds) { return new GeolocationTimeZoneSuggestion(effectiveFromElapsedMillis, zoneIds); } @@ -126,25 +112,6 @@ public final class GeolocationTimeZoneSuggestion { return mZoneIds; } - /** Returns debug information. See {@link GeolocationTimeZoneSuggestion} for details. */ - @NonNull - public List<String> getDebugInfo() { - return mDebugInfo == null - ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo); - } - - /** - * Associates information with the instance that can be useful for debugging / logging. The - * information is present in {@link #toString()} but is not considered for - * {@link #equals(Object)} and {@link #hashCode()}. - */ - public void addDebugInfo(String... debugInfos) { - if (mDebugInfo == null) { - mDebugInfo = new ArrayList<>(); - } - mDebugInfo.addAll(Arrays.asList(debugInfos)); - } - @Override public boolean equals(Object o) { if (this == o) { @@ -169,59 +136,6 @@ public final class GeolocationTimeZoneSuggestion { return "GeolocationTimeZoneSuggestion{" + "mEffectiveFromElapsedMillis=" + mEffectiveFromElapsedMillis + ", mZoneIds=" + mZoneIds - + ", mDebugInfo=" + mDebugInfo + '}'; } - - /** @hide */ - public static GeolocationTimeZoneSuggestion parseCommandLineArg(@NonNull ShellCommand cmd) { - String zoneIdsString = null; - String opt; - while ((opt = cmd.getNextArg()) != null) { - switch (opt) { - case "--zone_ids": { - zoneIdsString = cmd.getNextArgRequired(); - break; - } - default: { - throw new IllegalArgumentException("Unknown option: " + opt); - } - } - } - - if (zoneIdsString == null) { - throw new IllegalArgumentException("Missing --zone_ids"); - } - - long elapsedRealtimeMillis = SystemClock.elapsedRealtime(); - List<String> zoneIds = parseZoneIdsArg(zoneIdsString); - GeolocationTimeZoneSuggestion suggestion = - new GeolocationTimeZoneSuggestion(elapsedRealtimeMillis, zoneIds); - suggestion.addDebugInfo("Command line injection"); - return suggestion; - } - - private static List<String> parseZoneIdsArg(String zoneIdsString) { - if ("UNCERTAIN".equals(zoneIdsString)) { - return null; - } else if ("EMPTY".equals(zoneIdsString)) { - return Collections.emptyList(); - } else { - ArrayList<String> zoneIds = new ArrayList<>(); - StringTokenizer tokenizer = new StringTokenizer(zoneIdsString, ","); - while (tokenizer.hasMoreTokens()) { - zoneIds.add(tokenizer.nextToken()); - } - return zoneIds; - } - } - - /** @hide */ - public static void printCommandLineOpts(@NonNull PrintWriter pw) { - pw.println("Geolocation suggestion options:"); - pw.println(" --zone_ids {UNCERTAIN|EMPTY|<Olson ID>+}"); - pw.println(); - pw.println("See " + GeolocationTimeZoneSuggestion.class.getName() - + " for more information"); - } } diff --git a/services/core/java/com/android/server/timezonedetector/LocationAlgorithmEvent.java b/services/core/java/com/android/server/timezonedetector/LocationAlgorithmEvent.java new file mode 100644 index 000000000000..1ffd9a11b300 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/LocationAlgorithmEvent.java @@ -0,0 +1,194 @@ +/* + * 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.timezonedetector; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.time.LocationTimeZoneAlgorithmStatus; +import android.os.ShellCommand; +import android.os.SystemClock; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.StringTokenizer; + +/** + * An event from the location_time_zone_manager service (AKA the location-based time zone detection + * algorithm). An event can represent a new time zone recommendation, an algorithm status change, or + * both. + * + * <p>Events have the following properties: + * + * <ul> + * <li>{@code algorithmStatus}: The current status of the location-based time zone detection + * algorithm.</li> + * <li>{@code suggestion}: The latest time zone suggestion, if there is one.</li> + * <li>{@code debugInfo} contains debugging metadata associated with the suggestion. This is + * used to record why the event exists and how information contained within it was obtained. + * This information exists only to aid in debugging and therefore is used by + * {@link #toString()}, but it is not for use in detection logic and is not considered in + * {@link #hashCode()} or {@link #equals(Object)}. + * </li> + * </ul> + */ +public final class LocationAlgorithmEvent { + + @NonNull private final LocationTimeZoneAlgorithmStatus mAlgorithmStatus; + @Nullable private final GeolocationTimeZoneSuggestion mSuggestion; + @Nullable private ArrayList<String> mDebugInfo; + + /** Creates a new instance. */ + public LocationAlgorithmEvent( + @NonNull LocationTimeZoneAlgorithmStatus algorithmStatus, + @Nullable GeolocationTimeZoneSuggestion suggestion) { + mAlgorithmStatus = Objects.requireNonNull(algorithmStatus); + mSuggestion = suggestion; + } + + /** + * Returns the status of the location time zone detector algorithm. + */ + @NonNull + public LocationTimeZoneAlgorithmStatus getAlgorithmStatus() { + return mAlgorithmStatus; + } + + /** + * Returns the latest location algorithm suggestion. See {@link LocationAlgorithmEvent} for + * details. + */ + @Nullable + public GeolocationTimeZoneSuggestion getSuggestion() { + return mSuggestion; + } + + /** Returns debug information. See {@link LocationAlgorithmEvent} for details. */ + @NonNull + public List<String> getDebugInfo() { + return mDebugInfo == null + ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo); + } + + /** + * Associates information with the instance that can be useful for debugging / logging. The + * information is present in {@link #toString()} but is not considered for + * {@link #equals(Object)} and {@link #hashCode()}. + */ + public void addDebugInfo(String... debugInfos) { + if (mDebugInfo == null) { + mDebugInfo = new ArrayList<>(); + } + mDebugInfo.addAll(Arrays.asList(debugInfos)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LocationAlgorithmEvent that = (LocationAlgorithmEvent) o; + return mAlgorithmStatus.equals(that.mAlgorithmStatus) + && Objects.equals(mSuggestion, that.mSuggestion); + } + + @Override + public int hashCode() { + return Objects.hash(mAlgorithmStatus, mSuggestion); + } + + @Override + public String toString() { + return "LocationAlgorithmEvent{" + + "mAlgorithmStatus=" + mAlgorithmStatus + + ", mSuggestion=" + mSuggestion + + ", mDebugInfo=" + mDebugInfo + + '}'; + } + + static LocationAlgorithmEvent parseCommandLineArg(@NonNull ShellCommand cmd) { + String suggestionString = null; + LocationTimeZoneAlgorithmStatus algorithmStatus = null; + String opt; + while ((opt = cmd.getNextArg()) != null) { + switch (opt) { + case "--status": { + algorithmStatus = LocationTimeZoneAlgorithmStatus.parseCommandlineArg( + cmd.getNextArgRequired()); + break; + } + case "--suggestion": { + suggestionString = cmd.getNextArgRequired(); + break; + } + default: { + throw new IllegalArgumentException("Unknown option: " + opt); + } + } + } + + if (algorithmStatus == null) { + throw new IllegalArgumentException("Missing --status"); + } + + GeolocationTimeZoneSuggestion suggestion = null; + if (suggestionString != null) { + List<String> zoneIds = parseZoneIds(suggestionString); + long elapsedRealtimeMillis = SystemClock.elapsedRealtime(); + if (zoneIds == null) { + suggestion = GeolocationTimeZoneSuggestion.createUncertainSuggestion( + elapsedRealtimeMillis); + } else { + suggestion = GeolocationTimeZoneSuggestion.createCertainSuggestion( + elapsedRealtimeMillis, zoneIds); + } + } + + LocationAlgorithmEvent event = new LocationAlgorithmEvent(algorithmStatus, suggestion); + event.addDebugInfo("Command line injection"); + return event; + } + + private static List<String> parseZoneIds(String zoneIdsString) { + if ("UNCERTAIN".equals(zoneIdsString)) { + return null; + } else if ("EMPTY".equals(zoneIdsString)) { + return Collections.emptyList(); + } else { + ArrayList<String> zoneIds = new ArrayList<>(); + StringTokenizer tokenizer = new StringTokenizer(zoneIdsString, ","); + while (tokenizer.hasMoreTokens()) { + zoneIds.add(tokenizer.nextToken()); + } + return zoneIds; + } + } + + static void printCommandLineOpts(@NonNull PrintWriter pw) { + pw.println("Location algorithm event options:"); + pw.println(" --status {LocationTimeZoneAlgorithmStatus toString() format}"); + pw.println(" [--suggestion {UNCERTAIN|EMPTY|<Olson ID>+}]"); + pw.println(); + pw.println("See " + LocationAlgorithmEvent.class.getName() + " for more information"); + } +} diff --git a/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java b/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java index 6c36989320e8..aad53596fc19 100644 --- a/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java +++ b/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java @@ -89,7 +89,7 @@ public final class MetricsTimeZoneDetectorState { @NonNull String deviceTimeZoneId, @Nullable ManualTimeZoneSuggestion latestManualSuggestion, @Nullable TelephonyTimeZoneSuggestion latestTelephonySuggestion, - @Nullable GeolocationTimeZoneSuggestion latestGeolocationSuggestion) { + @Nullable LocationAlgorithmEvent latestLocationAlgorithmEvent) { boolean includeZoneIds = configurationInternal.isEnhancedMetricsCollectionEnabled(); String metricDeviceTimeZoneId = includeZoneIds ? deviceTimeZoneId : null; @@ -101,9 +101,13 @@ public final class MetricsTimeZoneDetectorState { MetricsTimeZoneSuggestion latestCanonicalTelephonySuggestion = createMetricsTimeZoneSuggestion( tzIdOrdinalGenerator, latestTelephonySuggestion, includeZoneIds); - MetricsTimeZoneSuggestion latestCanonicalGeolocationSuggestion = - createMetricsTimeZoneSuggestion( - tzIdOrdinalGenerator, latestGeolocationSuggestion, includeZoneIds); + + MetricsTimeZoneSuggestion latestCanonicalGeolocationSuggestion = null; + if (latestLocationAlgorithmEvent != null) { + GeolocationTimeZoneSuggestion suggestion = latestLocationAlgorithmEvent.getSuggestion(); + latestCanonicalGeolocationSuggestion = createMetricsTimeZoneSuggestion( + tzIdOrdinalGenerator, suggestion, includeZoneIds); + } return new MetricsTimeZoneDetectorState( configurationInternal, deviceTimeZoneIdOrdinal, metricDeviceTimeZoneId, diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java index 80cf1d6b9031..74a518bf8382 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java @@ -59,11 +59,11 @@ public interface TimeZoneDetectorInternal { boolean setManualTimeZoneForDpm(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion); /** - * Suggests the current time zone, determined using geolocation, to the detector. The - * detector may ignore the signal based on system settings, whether better information is - * available, and so on. This method may be implemented asynchronously. + * Handles the supplied {@link LocationAlgorithmEvent}. The detector may ignore the event based + * on system settings, whether better information is available, and so on. This method may be + * implemented asynchronously. */ - void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion); + void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent locationAlgorithmEvent); /** Generates a state snapshot for metrics. */ @NonNull diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java index dfb44df7b993..07d04737c3e2 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java @@ -76,13 +76,14 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter } @Override - public void suggestGeolocationTimeZone( - @NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) { - Objects.requireNonNull(timeZoneSuggestion); + public void handleLocationAlgorithmEvent( + @NonNull LocationAlgorithmEvent locationAlgorithmEvent) { + Objects.requireNonNull(locationAlgorithmEvent); // This call can take place on the mHandler thread because there is no return value. mHandler.post( - () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion)); + () -> mTimeZoneDetectorStrategy.handleLocationAlgorithmEvent( + locationAlgorithmEvent)); } @Override diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index f415cf03fdec..f8c1c9269ff3 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java @@ -300,12 +300,13 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub } /** Provided for command-line access. This is not exposed as a binder API. */ - void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) { + void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent locationAlgorithmEvent) { enforceSuggestGeolocationTimeZonePermission(); - Objects.requireNonNull(timeZoneSuggestion); + Objects.requireNonNull(locationAlgorithmEvent); mHandler.post( - () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion)); + () -> mTimeZoneDetectorStrategy.handleLocationAlgorithmEvent( + locationAlgorithmEvent)); } @Override diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java index 1b9f8e6cd66f..69274dba7825 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java @@ -19,6 +19,7 @@ import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_CONFIR import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_DUMP_METRICS; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_ENABLE_TELEPHONY_FALLBACK; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_GET_TIME_ZONE_STATE; +import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_IS_GEO_DETECTION_ENABLED; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_IS_GEO_DETECTION_SUPPORTED; @@ -27,7 +28,6 @@ import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SERVIC import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SET_AUTO_DETECTION_ENABLED; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SET_GEO_DETECTION_ENABLED; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SET_TIME_ZONE_STATE; -import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE; import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE; import static android.provider.DeviceConfig.NAMESPACE_SYSTEM_TIME; @@ -79,8 +79,8 @@ class TimeZoneDetectorShellCommand extends ShellCommand { return runIsGeoDetectionEnabled(); case SHELL_COMMAND_SET_GEO_DETECTION_ENABLED: return runSetGeoDetectionEnabled(); - case SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE: - return runSuggestGeolocationTimeZone(); + case SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT: + return runHandleLocationEvent(); case SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE: return runSuggestManualTimeZone(); case SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE: @@ -153,34 +153,34 @@ class TimeZoneDetectorShellCommand extends ShellCommand { return mInterface.updateConfiguration(userId, configuration) ? 0 : 1; } - private int runSuggestGeolocationTimeZone() { - return runSuggestTimeZone( - () -> GeolocationTimeZoneSuggestion.parseCommandLineArg(this), - mInterface::suggestGeolocationTimeZone); + private int runHandleLocationEvent() { + return runSingleArgMethod( + () -> LocationAlgorithmEvent.parseCommandLineArg(this), + mInterface::handleLocationAlgorithmEvent); } private int runSuggestManualTimeZone() { - return runSuggestTimeZone( + return runSingleArgMethod( () -> ManualTimeZoneSuggestion.parseCommandLineArg(this), mInterface::suggestManualTimeZone); } private int runSuggestTelephonyTimeZone() { - return runSuggestTimeZone( + return runSingleArgMethod( () -> TelephonyTimeZoneSuggestion.parseCommandLineArg(this), mInterface::suggestTelephonyTimeZone); } - private <T> int runSuggestTimeZone(Supplier<T> suggestionParser, Consumer<T> invoker) { + private <T> int runSingleArgMethod(Supplier<T> argParser, Consumer<T> invoker) { final PrintWriter pw = getOutPrintWriter(); try { - T suggestion = suggestionParser.get(); - if (suggestion == null) { - pw.println("Error: suggestion not specified"); + T arg = argParser.get(); + if (arg == null) { + pw.println("Error: arg not specified"); return 1; } - invoker.accept(suggestion); - pw.println("Suggestion " + suggestion + " injected."); + invoker.accept(arg); + pw.println("Arg " + arg + " injected."); return 0; } catch (RuntimeException e) { pw.println(e); @@ -263,18 +263,18 @@ class TimeZoneDetectorShellCommand extends ShellCommand { pw.printf(" Sets the geolocation time zone detection enabled setting.\n"); pw.printf(" %s\n", SHELL_COMMAND_ENABLE_TELEPHONY_FALLBACK); pw.printf(" Signals that telephony time zone detection fall back can be used if" - + " geolocation detection is supported and enabled. This is a temporary state until" - + " geolocation detection becomes \"certain\". To have an effect this requires that" - + " the telephony fallback feature is supported on the device, see below for" - + " for device_config flags.\n"); - pw.println(); - pw.printf(" %s <geolocation suggestion opts>\n", - SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE); - pw.printf(" Suggests a time zone as if via the \"location\" origin.\n"); + + " geolocation detection is supported and enabled.\n)"); + pw.printf(" This is a temporary state until geolocation detection becomes \"certain\"." + + "\n"); + pw.printf(" To have an effect this requires that the telephony fallback feature is" + + " supported on the device, see below for device_config flags.\n"); + pw.printf(" %s <location event opts>\n", SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT); + pw.printf(" Simulates an event from the location time zone detection algorithm.\n"); pw.printf(" %s <manual suggestion opts>\n", SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE); - pw.printf(" Suggests a time zone as if via the \"manual\" origin.\n"); + pw.printf(" Suggests a time zone as if supplied by a user manually.\n"); pw.printf(" %s <telephony suggestion opts>\n", SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE); - pw.printf(" Suggests a time zone as if via the \"telephony\" origin.\n"); + pw.printf(" Simulates a time zone suggestion from the telephony time zone detection" + + " algorithm.\n"); pw.printf(" %s\n", SHELL_COMMAND_GET_TIME_ZONE_STATE); pw.printf(" Returns the current time zone setting state.\n"); pw.printf(" %s <time zone state options>\n", SHELL_COMMAND_SET_TIME_ZONE_STATE); @@ -284,7 +284,7 @@ class TimeZoneDetectorShellCommand extends ShellCommand { pw.printf(" %s\n", SHELL_COMMAND_DUMP_METRICS); pw.printf(" Dumps the service metrics to stdout for inspection.\n"); pw.println(); - GeolocationTimeZoneSuggestion.printCommandLineOpts(pw); + LocationAlgorithmEvent.printCommandLineOpts(pw); pw.println(); ManualTimeZoneSuggestion.printCommandLineOpts(pw); pw.println(); diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java index 328cf72b262e..5768a6bfa0dc 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java @@ -157,10 +157,9 @@ public interface TimeZoneDetectorStrategy extends Dumpable { boolean confirmTimeZone(@NonNull String timeZoneId); /** - * Suggests zero, one or more time zones for the device, or withdraws a previous suggestion if - * {@link GeolocationTimeZoneSuggestion#getZoneIds()} is {@code null}. + * Handles an event from the location-based time zone detection algorithm. */ - void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion suggestion); + void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent event); /** * Suggests a time zone for the device using manually-entered (i.e. user sourced) information. @@ -183,7 +182,7 @@ public interface TimeZoneDetectorStrategy extends Dumpable { /** * Tells the strategy that it can fall back to telephony detection while geolocation detection - * remains uncertain. {@link #suggestGeolocationTimeZone(GeolocationTimeZoneSuggestion)} can + * remains uncertain. {@link #handleLocationAlgorithmEvent(LocationAlgorithmEvent)} can * disable it again. See {@link TimeZoneDetectorStrategy} for details. */ void enableTelephonyTimeZoneFallback(); diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java index ecf25e9c157c..eecf0f74bff6 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java @@ -29,9 +29,13 @@ import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.app.time.DetectorStatusTypes; +import android.app.time.LocationTimeZoneAlgorithmStatus; +import android.app.time.TelephonyTimeZoneAlgorithmStatus; import android.app.time.TimeZoneCapabilities; import android.app.time.TimeZoneCapabilitiesAndConfig; import android.app.time.TimeZoneConfiguration; +import android.app.time.TimeZoneDetectorStatus; import android.app.time.TimeZoneState; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; @@ -183,11 +187,10 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat new ArrayMapWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); /** - * The latest geolocation suggestion received. If the user disabled geolocation time zone - * detection then the latest suggestion is cleared. + * The latest location algorithm event received. */ @GuardedBy("this") - private final ReferenceWithHistory<GeolocationTimeZoneSuggestion> mLatestGeoLocationSuggestion = + private final ReferenceWithHistory<LocationAlgorithmEvent> mLatestLocationAlgorithmEvent = new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); /** @@ -208,6 +211,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat @NonNull private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>(); /** + * A snapshot of the current detector status. A local copy is cached because it is relatively + * heavyweight to obtain and is used more often than it is expected to change. + */ + @GuardedBy("this") + @NonNull + private TimeZoneDetectorStatus mDetectorStatus; + + /** * A snapshot of the current user's {@link ConfigurationInternal}. A local copy is cached * because it is relatively heavyweight to obtain and is used more often than it is expected to * change. Because many operations are asynchronous, this value may be out of date but should @@ -258,8 +269,10 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // Listen for config and user changes and get an initial snapshot of configuration. StateChangeListener stateChangeListener = this::handleConfigurationInternalMaybeChanged; mServiceConfigAccessor.addConfigurationInternalChangeListener(stateChangeListener); - mCurrentConfigurationInternal = - mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + + // Initialize mCurrentConfigurationInternal and mDetectorStatus with their starting + // values. + updateCurrentConfigurationInternalIfRequired("TimeZoneDetectorStrategyImpl:"); } } @@ -278,6 +291,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat configurationInternal = mServiceConfigAccessor.getConfigurationInternal(userId); } return new TimeZoneCapabilitiesAndConfig( + mDetectorStatus, configurationInternal.asCapabilities(bypassUserPolicyChecks), configurationInternal.asConfiguration()); } @@ -295,28 +309,52 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // but that could mean an immediate call to getCapabilitiesAndConfig() for the current user // wouldn't see the update. So, handle the cache update and notifications here. When the // async update listener triggers it will find everything already up to date and do nothing. - if (updateSuccessful && mCurrentConfigurationInternal.getUserId() == userId) { - ConfigurationInternal configurationInternal = - mServiceConfigAccessor.getConfigurationInternal(userId); - - // If the configuration actually changed, update the cached copy synchronously and do - // other necessary house-keeping / (async) listener notifications. - if (!configurationInternal.equals(mCurrentConfigurationInternal)) { - mCurrentConfigurationInternal = configurationInternal; - - String logMsg = "updateConfiguration:" - + " userId=" + userId - + ", configuration=" + configuration - + ", bypassUserPolicyChecks=" + bypassUserPolicyChecks - + ", mCurrentConfigurationInternal=" + mCurrentConfigurationInternal; - logTimeZoneDebugInfo(logMsg); - - handleConfigurationInternalChanged(logMsg); - } + if (updateSuccessful) { + String logMsg = "updateConfiguration:" + + " userId=" + userId + + ", configuration=" + configuration + + ", bypassUserPolicyChecks=" + bypassUserPolicyChecks; + updateCurrentConfigurationInternalIfRequired(logMsg); } return updateSuccessful; } + @GuardedBy("this") + private void updateCurrentConfigurationInternalIfRequired(@NonNull String logMsg) { + ConfigurationInternal newCurrentConfigurationInternal = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + // mCurrentConfigurationInternal is null the first time this method is called. + ConfigurationInternal oldCurrentConfigurationInternal = mCurrentConfigurationInternal; + + // If the configuration actually changed, update the cached copy synchronously and do + // other necessary house-keeping / (async) listener notifications. + if (!newCurrentConfigurationInternal.equals(oldCurrentConfigurationInternal)) { + mCurrentConfigurationInternal = newCurrentConfigurationInternal; + + logMsg += " [oldConfiguration=" + oldCurrentConfigurationInternal + + ", newConfiguration=" + newCurrentConfigurationInternal + + "]"; + logTimeZoneDebugInfo(logMsg); + + // ConfigurationInternal changes can affect the detector's status. + updateDetectorStatus(); + + // The configuration and maybe the status changed so notify listeners. + notifyStateChangeListenersAsynchronously(); + + // The configuration change may have changed available suggestions or the way + // suggestions are used, so re-run detection. + doAutoTimeZoneDetection(mCurrentConfigurationInternal, logMsg); + } + } + + @GuardedBy("this") + private void notifyStateChangeListenersAsynchronously() { + for (StateChangeListener listener : mStateChangeListeners) { + mStateChangeHandler.post(listener::onChange); + } + } + @Override public synchronized void addChangeListener(StateChangeListener listener) { mStateChangeListeners.add(listener); @@ -356,33 +394,39 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat } @Override - public synchronized void suggestGeolocationTimeZone( - @NonNull GeolocationTimeZoneSuggestion suggestion) { - + public synchronized void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent event) { ConfigurationInternal currentUserConfig = mCurrentConfigurationInternal; if (DBG) { - Slog.d(LOG_TAG, "Geolocation suggestion received." + Slog.d(LOG_TAG, "Location algorithm event received." + " currentUserConfig=" + currentUserConfig - + " newSuggestion=" + suggestion); + + " event=" + event); } - Objects.requireNonNull(suggestion); + Objects.requireNonNull(event); - // Geolocation suggestions may be stored but not used during time zone detection if the + // Location algorithm events may be stored but not used during time zone detection if the // configuration doesn't have geo time zone detection enabled. The caller is expected to - // withdraw a previous suggestion (i.e. submit an "uncertain" suggestion, when geo time zone - // detection is disabled. - - // The suggestion's "effective from" time is ignored: we currently assume suggestions - // are made in a sensible order and the most recent is always the best one to use. - mLatestGeoLocationSuggestion.set(suggestion); + // withdraw a previous suggestion, i.e. submit an event containing an "uncertain" + // suggestion, when geo time zone detection is disabled. + + // We currently assume events are made in a sensible order and the most recent is always the + // best one to use. + mLatestLocationAlgorithmEvent.set(event); + + // The latest location algorithm event can affect the cached detector status, so update it + // and notify state change listeners as needed. + boolean statusChanged = updateDetectorStatus(); + if (statusChanged) { + notifyStateChangeListenersAsynchronously(); + } // Update the mTelephonyTimeZoneFallbackEnabled state if needed: a certain suggestion // will usually disable telephony fallback mode if it is currently enabled. + // TODO(b/236624675)Some provider status codes can be used to enable telephony fallback. disableTelephonyFallbackIfNeeded(); - // Now perform auto time zone detection. The new suggestion may be used to modify the - // time zone setting. - String reason = "New geolocation time zone suggested. suggestion=" + suggestion; + // Now perform auto time zone detection. The new event may be used to modify the time zone + // setting. + String reason = "New location algorithm event received. event=" + event; doAutoTimeZoneDetection(currentUserConfig, reason); } @@ -465,9 +509,9 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat + mTelephonyTimeZoneFallbackEnabled; logTimeZoneDebugInfo(logMsg); - // mTelephonyTimeZoneFallbackEnabled and mLatestGeoLocationSuggestion interact. - // If there is currently a certain geolocation suggestion, then the telephony fallback - // value needs to be considered after changing it. + // mTelephonyTimeZoneFallbackEnabled and mLatestLocationAlgorithmEvent interact. + // If the latest event contains a "certain" geolocation suggestion, then the telephony + // fallback value needs to be considered after changing it. // With the way that the mTelephonyTimeZoneFallbackEnabled time is currently chosen // above, and the fact that geolocation suggestions should never have a time in the // future, the following call will be a no-op, and telephony fallback will remain @@ -507,7 +551,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat mEnvironment.getDeviceTimeZone(), getLatestManualSuggestion(), telephonySuggestion, - getLatestGeolocationSuggestion()); + getLatestLocationAlgorithmEvent()); } @Override @@ -606,13 +650,15 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat */ @GuardedBy("this") private boolean doGeolocationTimeZoneDetection(@NonNull String detectionReason) { - GeolocationTimeZoneSuggestion latestGeolocationSuggestion = - mLatestGeoLocationSuggestion.get(); - if (latestGeolocationSuggestion == null) { + // Terminate early if there's nothing to do. + LocationAlgorithmEvent latestLocationAlgorithmEvent = mLatestLocationAlgorithmEvent.get(); + if (latestLocationAlgorithmEvent == null + || latestLocationAlgorithmEvent.getSuggestion() == null) { return false; } - List<String> zoneIds = latestGeolocationSuggestion.getZoneIds(); + GeolocationTimeZoneSuggestion suggestion = latestLocationAlgorithmEvent.getSuggestion(); + List<String> zoneIds = suggestion.getZoneIds(); if (zoneIds == null) { // This means the originator of the suggestion is uncertain about the time zone. The // existing time zone setting must be left as it is but detection can go on looking for @@ -645,13 +691,18 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat } /** - * Sets the mTelephonyTimeZoneFallbackEnabled state to {@code false} if the latest geo - * suggestion is a "certain" suggestion that comes after the time when telephony fallback was - * enabled. + * Sets the mTelephonyTimeZoneFallbackEnabled state to {@code false} if the latest location + * algorithm event contains a "certain" suggestion that comes after the time when telephony + * fallback was enabled. */ @GuardedBy("this") private void disableTelephonyFallbackIfNeeded() { - GeolocationTimeZoneSuggestion suggestion = mLatestGeoLocationSuggestion.get(); + LocationAlgorithmEvent latestLocationAlgorithmEvent = mLatestLocationAlgorithmEvent.get(); + if (latestLocationAlgorithmEvent == null) { + return; + } + + GeolocationTimeZoneSuggestion suggestion = latestLocationAlgorithmEvent.getSuggestion(); boolean isLatestSuggestionCertain = suggestion != null && suggestion.getZoneIds() != null; if (isLatestSuggestionCertain && mTelephonyTimeZoneFallbackEnabled.getValue()) { // This transition ONLY changes mTelephonyTimeZoneFallbackEnabled from @@ -809,33 +860,27 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat * Handles a configuration change notification. */ private synchronized void handleConfigurationInternalMaybeChanged() { - ConfigurationInternal currentUserConfig = - mServiceConfigAccessor.getCurrentUserConfigurationInternal(); - - // The configuration may not actually have changed so check before doing anything. - if (!currentUserConfig.equals(mCurrentConfigurationInternal)) { - String logMsg = "handleConfigurationInternalMaybeChanged:" - + " oldConfiguration=" + mCurrentConfigurationInternal - + ", newConfiguration=" + currentUserConfig; - logTimeZoneDebugInfo(logMsg); - - mCurrentConfigurationInternal = currentUserConfig; - - handleConfigurationInternalChanged(logMsg); - } + String logMsg = "handleConfigurationInternalMaybeChanged:"; + updateCurrentConfigurationInternalIfRequired(logMsg); } - /** House-keeping that needs to be done when the mCurrentConfigurationInternal has changed. */ + /** + * Called whenever the information that contributes to {@link #mDetectorStatus} could have + * changed. Updates the cached status snapshot if required. + * + * @return true if the status had changed and has been updated + */ @GuardedBy("this") - private void handleConfigurationInternalChanged(@NonNull String logMsg) { - // Notify change listeners asynchronously. - for (StateChangeListener listener : mStateChangeListeners) { - mStateChangeHandler.post(listener::onChange); + private boolean updateDetectorStatus() { + TimeZoneDetectorStatus newDetectorStatus = createTimeZoneDetectorStatus( + mCurrentConfigurationInternal, mLatestLocationAlgorithmEvent.get()); + // mDetectorStatus is null the first time this method is called. + TimeZoneDetectorStatus oldDetectorStatus = mDetectorStatus; + boolean statusChanged = !newDetectorStatus.equals(oldDetectorStatus); + if (statusChanged) { + mDetectorStatus = newDetectorStatus; } - - // The configuration change may have changed available suggestions or the way - // suggestions are used, so re-run detection. - doAutoTimeZoneDetection(mCurrentConfigurationInternal, logMsg); + return statusChanged; } /** @@ -847,6 +892,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat ipw.increaseIndent(); // level 1 ipw.println("mCurrentConfigurationInternal=" + mCurrentConfigurationInternal); + ipw.println("mDetectorStatus=" + mDetectorStatus); final boolean bypassUserPolicyChecks = false; ipw.println("[Capabilities=" + mCurrentConfigurationInternal.asCapabilities(bypassUserPolicyChecks) + "]"); @@ -870,9 +916,9 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat mLatestManualSuggestion.dump(ipw); ipw.decreaseIndent(); // level 2 - ipw.println("Geolocation suggestion history:"); + ipw.println("Location algorithm event history:"); ipw.increaseIndent(); // level 2 - mLatestGeoLocationSuggestion.dump(ipw); + mLatestLocationAlgorithmEvent.dump(ipw); ipw.decreaseIndent(); // level 2 ipw.println("Telephony suggestion history:"); @@ -886,6 +932,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat * A method used to inspect strategy state during tests. Not intended for general use. */ @VisibleForTesting + @Nullable public synchronized ManualTimeZoneSuggestion getLatestManualSuggestion() { return mLatestManualSuggestion.get(); } @@ -894,6 +941,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat * A method used to inspect strategy state during tests. Not intended for general use. */ @VisibleForTesting + @Nullable public synchronized QualifiedTelephonyTimeZoneSuggestion getLatestTelephonySuggestion( int slotIndex) { return mTelephonySuggestionsBySlotIndex.get(slotIndex); @@ -903,8 +951,9 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat * A method used to inspect strategy state during tests. Not intended for general use. */ @VisibleForTesting - public synchronized GeolocationTimeZoneSuggestion getLatestGeolocationSuggestion() { - return mLatestGeoLocationSuggestion.get(); + @Nullable + public synchronized LocationAlgorithmEvent getLatestLocationAlgorithmEvent() { + return mLatestLocationAlgorithmEvent.get(); } @VisibleForTesting @@ -917,6 +966,11 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat return mCurrentConfigurationInternal; } + @VisibleForTesting + public synchronized TimeZoneDetectorStatus getCachedDetectorStatusForTests() { + return mDetectorStatus; + } + /** * A {@link TelephonyTimeZoneSuggestion} with additional qualifying metadata. */ @@ -970,4 +1024,42 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat private static String formatDebugString(TimestampedValue<?> value) { return value.getValue() + " @ " + Duration.ofMillis(value.getReferenceTimeMillis()); } + + @NonNull + private static TimeZoneDetectorStatus createTimeZoneDetectorStatus( + @NonNull ConfigurationInternal currentConfigurationInternal, + @Nullable LocationAlgorithmEvent latestLocationAlgorithmEvent) { + + int detectorStatus; + if (!currentConfigurationInternal.isAutoDetectionSupported()) { + detectorStatus = DetectorStatusTypes.DETECTOR_STATUS_NOT_SUPPORTED; + } else if (currentConfigurationInternal.getAutoDetectionEnabledBehavior()) { + detectorStatus = DetectorStatusTypes.DETECTOR_STATUS_RUNNING; + } else { + detectorStatus = DetectorStatusTypes.DETECTOR_STATUS_NOT_RUNNING; + } + + TelephonyTimeZoneAlgorithmStatus telephonyAlgorithmStatus = + createTelephonyAlgorithmStatus(currentConfigurationInternal); + + LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = + latestLocationAlgorithmEvent == null ? LocationTimeZoneAlgorithmStatus.UNKNOWN + : latestLocationAlgorithmEvent.getAlgorithmStatus(); + + return new TimeZoneDetectorStatus( + detectorStatus, telephonyAlgorithmStatus, locationAlgorithmStatus); + } + + @NonNull + private static TelephonyTimeZoneAlgorithmStatus createTelephonyAlgorithmStatus( + @NonNull ConfigurationInternal currentConfigurationInternal) { + int algorithmStatus; + if (!currentConfigurationInternal.isTelephonyDetectionSupported()) { + algorithmStatus = DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED; + } else { + // The telephony detector is passive, so we treat it as "running". + algorithmStatus = DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; + } + return new TelephonyTimeZoneAlgorithmStatus(algorithmStatus); + } } diff --git a/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java index a1de2941808e..71aa10d8614a 100644 --- a/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java +++ b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java @@ -53,7 +53,7 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider { } @Override - void onInitialize() { + boolean onInitialize() { mProxy.initialize(new LocationTimeZoneProviderProxy.Listener() { @Override public void onReportTimeZoneProviderEvent( @@ -71,6 +71,7 @@ class BinderLocationTimeZoneProvider extends LocationTimeZoneProvider { handleTemporaryFailure("onProviderUnbound()"); } }); + return true; } @Override diff --git a/services/core/java/com/android/server/timezonedetector/location/DisabledLocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/DisabledLocationTimeZoneProvider.java new file mode 100644 index 000000000000..5d6184ec66d6 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/location/DisabledLocationTimeZoneProvider.java @@ -0,0 +1,86 @@ +/* + * 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.timezonedetector.location; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.IndentingPrintWriter; + +import java.time.Duration; + +/** + * A {@link LocationTimeZoneProvider} that provides minimal responses needed to operate correctly + * when there is no "real" provider configured / enabled. This is used when the platform supports + * more providers than are needed for an Android deployment. + * + * <p>That is, the {@link LocationTimeZoneProviderController} supports a primary and a secondary + * {@link LocationTimeZoneProvider}, but if only a primary is configured, the secondary provider + * config will marked as "disabled" and the {@link LocationTimeZoneProvider} implementation will use + * {@link DisabledLocationTimeZoneProvider}. The {@link DisabledLocationTimeZoneProvider} fails + * initialization and immediately moves to a "permanent failure" state, which ensures the {@link + * LocationTimeZoneProviderController} correctly categorizes it and won't attempt to use it. + */ +class DisabledLocationTimeZoneProvider extends LocationTimeZoneProvider { + + DisabledLocationTimeZoneProvider( + @NonNull ProviderMetricsLogger providerMetricsLogger, + @NonNull ThreadingDomain threadingDomain, + @NonNull String providerName, + boolean recordStateChanges) { + super(providerMetricsLogger, threadingDomain, providerName, x -> x, recordStateChanges); + } + + @Override + boolean onInitialize() { + // Fail initialization, preventing further use. + return false; + } + + @Override + void onDestroy() { + } + + @Override + void onStartUpdates(@NonNull Duration initializationTimeout, + @NonNull Duration eventFilteringAgeThreshold) { + throw new UnsupportedOperationException("Provider is disabled"); + } + + @Override + void onStopUpdates() { + throw new UnsupportedOperationException("Provider is disabled"); + } + + @Override + public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { + synchronized (mSharedLock) { + ipw.println("{DisabledLocationTimeZoneProvider}"); + ipw.println("mProviderName=" + mProviderName); + ipw.println("mCurrentState=" + mCurrentState); + } + } + + @Override + public String toString() { + synchronized (mSharedLock) { + return "DisabledLocationTimeZoneProvider{" + + "mProviderName=" + mProviderName + + ", mCurrentState=" + mCurrentState + + '}'; + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java index 36ab111dfccb..8d9854436e29 100644 --- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java @@ -447,11 +447,18 @@ public class LocationTimeZoneManagerService extends Binder { @NonNull LocationTimeZoneProvider createProvider() { - LocationTimeZoneProviderProxy proxy = createProxy(); ProviderMetricsLogger providerMetricsLogger = new RealProviderMetricsLogger(mIndex); - return new BinderLocationTimeZoneProvider( - providerMetricsLogger, mThreadingDomain, mName, proxy, - mServiceConfigAccessor.getRecordStateChangesForTests()); + + String mode = getMode(); + if (Objects.equals(mode, PROVIDER_MODE_DISABLED)) { + return new DisabledLocationTimeZoneProvider(providerMetricsLogger, mThreadingDomain, + mName, mServiceConfigAccessor.getRecordStateChangesForTests()); + } else { + LocationTimeZoneProviderProxy proxy = createBinderProxy(); + return new BinderLocationTimeZoneProvider( + providerMetricsLogger, mThreadingDomain, mName, proxy, + mServiceConfigAccessor.getRecordStateChangesForTests()); + } } @Override @@ -460,17 +467,6 @@ public class LocationTimeZoneManagerService extends Binder { ipw.printf("getPackageName()=%s\n", getPackageName()); } - @NonNull - private LocationTimeZoneProviderProxy createProxy() { - String mode = getMode(); - if (Objects.equals(mode, PROVIDER_MODE_DISABLED)) { - return new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain); - } else { - // mode == PROVIDER_MODE_OVERRIDE_ENABLED (or unknown). - return createRealProxy(); - } - } - /** Returns the mode of the provider (enabled/disabled). */ @NonNull private String getMode() { @@ -482,7 +478,7 @@ public class LocationTimeZoneManagerService extends Binder { } @NonNull - private RealLocationTimeZoneProviderProxy createRealProxy() { + private RealLocationTimeZoneProviderProxy createBinderProxy() { String providerServiceAction = mServiceAction; boolean isTestProvider = isTestProvider(); String providerPackageName = getPackageName(); diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java index 1f752f45fd51..e90a1fe34798 100644 --- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java @@ -19,7 +19,7 @@ package com.android.server.timezonedetector.location; import android.annotation.NonNull; import android.annotation.Nullable; -import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; +import com.android.server.timezonedetector.LocationAlgorithmEvent; import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState; import com.android.server.timezonedetector.location.LocationTimeZoneProviderController.State; @@ -32,14 +32,14 @@ import java.util.Objects; final class LocationTimeZoneManagerServiceState { private final @State String mControllerState; - @Nullable private final GeolocationTimeZoneSuggestion mLastSuggestion; + @Nullable private final LocationAlgorithmEvent mLastEvent; @NonNull private final List<@State String> mControllerStates; @NonNull private final List<ProviderState> mPrimaryProviderStates; @NonNull private final List<ProviderState> mSecondaryProviderStates; LocationTimeZoneManagerServiceState(@NonNull Builder builder) { mControllerState = builder.mControllerState; - mLastSuggestion = builder.mLastSuggestion; + mLastEvent = builder.mLastEvent; mControllerStates = Objects.requireNonNull(builder.mControllerStates); mPrimaryProviderStates = Objects.requireNonNull(builder.mPrimaryProviderStates); mSecondaryProviderStates = Objects.requireNonNull(builder.mSecondaryProviderStates); @@ -50,8 +50,8 @@ final class LocationTimeZoneManagerServiceState { } @Nullable - public GeolocationTimeZoneSuggestion getLastSuggestion() { - return mLastSuggestion; + public LocationAlgorithmEvent getLastEvent() { + return mLastEvent; } @NonNull @@ -73,7 +73,7 @@ final class LocationTimeZoneManagerServiceState { public String toString() { return "LocationTimeZoneManagerServiceState{" + "mControllerState=" + mControllerState - + ", mLastSuggestion=" + mLastSuggestion + + ", mLastEvent=" + mLastEvent + ", mControllerStates=" + mControllerStates + ", mPrimaryProviderStates=" + mPrimaryProviderStates + ", mSecondaryProviderStates=" + mSecondaryProviderStates @@ -83,7 +83,7 @@ final class LocationTimeZoneManagerServiceState { static final class Builder { private @State String mControllerState; - private GeolocationTimeZoneSuggestion mLastSuggestion; + private LocationAlgorithmEvent mLastEvent; private List<@State String> mControllerStates; private List<ProviderState> mPrimaryProviderStates; private List<ProviderState> mSecondaryProviderStates; @@ -95,8 +95,8 @@ final class LocationTimeZoneManagerServiceState { } @NonNull - Builder setLastSuggestion(@NonNull GeolocationTimeZoneSuggestion lastSuggestion) { - mLastSuggestion = Objects.requireNonNull(lastSuggestion); + Builder setLastEvent(@NonNull LocationAlgorithmEvent lastEvent) { + mLastEvent = Objects.requireNonNull(lastEvent); return this; } diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java index 60bbea77b636..cefd0b578df8 100644 --- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java @@ -15,6 +15,10 @@ */ package com.android.server.timezonedetector.location; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_UNKNOWN; import static android.app.time.LocationTimeZoneManager.DUMP_STATE_OPTION_PROTO; import static android.app.time.LocationTimeZoneManager.NULL_PACKAGE_NAME_TOKEN; import static android.app.time.LocationTimeZoneManager.SERVICE_NAME; @@ -51,9 +55,14 @@ import static com.android.server.timezonedetector.location.LocationTimeZoneProvi import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus; import android.app.time.GeolocationTimeZoneSuggestionProto; +import android.app.time.LocationTimeZoneAlgorithmStatus; +import android.app.time.LocationTimeZoneAlgorithmStatusProto; import android.app.time.LocationTimeZoneManagerProto; import android.app.time.LocationTimeZoneManagerServiceStateProto; +import android.app.time.LocationTimeZoneProviderEventProto; +import android.app.time.TimeZoneDetectorProto; import android.app.time.TimeZoneProviderStateProto; import android.app.timezonedetector.TimeZoneDetector; import android.os.ShellCommand; @@ -62,6 +71,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.util.dump.DualDumpOutputStream; import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; +import com.android.server.timezonedetector.LocationAlgorithmEvent; import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.ProviderStateEnum; import com.android.server.timezonedetector.location.LocationTimeZoneProviderController.State; @@ -239,19 +249,39 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand { outputStream = new DualDumpOutputStream( new IndentingPrintWriter(getOutPrintWriter(), " ")); } - if (state.getLastSuggestion() != null) { - GeolocationTimeZoneSuggestion lastSuggestion = state.getLastSuggestion(); - long lastSuggestionToken = outputStream.start( - "last_suggestion", LocationTimeZoneManagerServiceStateProto.LAST_SUGGESTION); - for (String zoneId : lastSuggestion.getZoneIds()) { - outputStream.write( - "zone_ids" , GeolocationTimeZoneSuggestionProto.ZONE_IDS, zoneId); + + if (state.getLastEvent() != null) { + LocationAlgorithmEvent lastEvent = state.getLastEvent(); + long lastEventToken = outputStream.start( + "last_event", LocationTimeZoneManagerServiceStateProto.LAST_EVENT); + + // lastEvent.algorithmStatus + LocationTimeZoneAlgorithmStatus algorithmStatus = lastEvent.getAlgorithmStatus(); + long algorithmStatusToken = outputStream.start( + "algorithm_status", LocationTimeZoneProviderEventProto.ALGORITHM_STATUS); + outputStream.write("status", LocationTimeZoneAlgorithmStatusProto.STATUS, + convertDetectionAlgorithmStatusToEnumToProtoEnum(algorithmStatus.getStatus())); + outputStream.end(algorithmStatusToken); + + // lastEvent.suggestion + if (lastEvent.getSuggestion() != null) { + long suggestionToken = outputStream.start( + "suggestion", LocationTimeZoneProviderEventProto.SUGGESTION); + GeolocationTimeZoneSuggestion lastSuggestion = lastEvent.getSuggestion(); + for (String zoneId : lastSuggestion.getZoneIds()) { + outputStream.write( + "zone_ids", GeolocationTimeZoneSuggestionProto.ZONE_IDS, zoneId); + } + outputStream.end(suggestionToken); } - for (String debugInfo : lastSuggestion.getDebugInfo()) { + + // lastEvent.debugInfo + for (String debugInfo : lastEvent.getDebugInfo()) { outputStream.write( - "debug_info", GeolocationTimeZoneSuggestionProto.DEBUG_INFO, debugInfo); + "debug_info", LocationTimeZoneProviderEventProto.DEBUG_INFO, debugInfo); } - outputStream.end(lastSuggestionToken); + + outputStream.end(lastEventToken); } writeControllerStates(outputStream, state.getControllerStates()); @@ -330,6 +360,22 @@ class LocationTimeZoneManagerShellCommand extends ShellCommand { } } + private static int convertDetectionAlgorithmStatusToEnumToProtoEnum( + @DetectionAlgorithmStatus int statusEnum) { + switch (statusEnum) { + case DETECTION_ALGORITHM_STATUS_UNKNOWN: + return TimeZoneDetectorProto.DETECTION_ALGORITHM_STATUS_UNKNOWN; + case DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED: + return TimeZoneDetectorProto.DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED; + case DETECTION_ALGORITHM_STATUS_NOT_RUNNING: + return TimeZoneDetectorProto.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; + case DETECTION_ALGORITHM_STATUS_RUNNING: + return TimeZoneDetectorProto.DETECTION_ALGORITHM_STATUS_RUNNING; + default: + throw new IllegalArgumentException("Unknown statusEnum=" + statusEnum); + } + } + private void reportError(@NonNull Throwable e) { PrintWriter errPrintWriter = getErrPrintWriter(); errPrintWriter.println("Error: "); diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java index b1fc4f561033..ba7c328f1e80 100644 --- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java @@ -16,6 +16,10 @@ package com.android.server.timezonedetector.location; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_UNCERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY; import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE; import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION; import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN; @@ -33,6 +37,7 @@ import android.annotation.ElapsedRealtimeLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.time.LocationTimeZoneAlgorithmStatus.ProviderStatus; import android.os.Handler; import android.os.SystemClock; import android.service.timezone.TimeZoneProviderEvent; @@ -295,6 +300,34 @@ abstract class LocationTimeZoneProvider implements Dumpable { || stateEnum == PROVIDER_STATE_DESTROYED; } + /** + * Maps the internal state enum value to one of the status values exposed to the layers + * above. + */ + public @ProviderStatus int getProviderStatus() { + switch (stateEnum) { + case PROVIDER_STATE_STARTED_INITIALIZING: + return PROVIDER_STATUS_NOT_READY; + case PROVIDER_STATE_STARTED_CERTAIN: + return PROVIDER_STATUS_IS_CERTAIN; + case PROVIDER_STATE_STARTED_UNCERTAIN: + return PROVIDER_STATUS_IS_UNCERTAIN; + case PROVIDER_STATE_PERM_FAILED: + // Perm failed means the providers wasn't configured, configured properly, + // or has removed itself for other reasons, e.g. turned-down server. + return PROVIDER_STATUS_NOT_PRESENT; + case PROVIDER_STATE_STOPPED: + case PROVIDER_STATE_DESTROYED: + // This is a "safe" default that best describes a provider that isn't in one of + // the more obviously mapped states. + return PROVIDER_STATUS_NOT_READY; + case PROVIDER_STATE_UNKNOWN: + default: + throw new IllegalStateException( + "Unknown state enum:" + prettyPrintStateEnum(stateEnum)); + } + } + /** Returns the status reported by the provider, if available. */ @Nullable TimeZoneProviderStatus getReportedStatus() { @@ -415,13 +448,21 @@ abstract class LocationTimeZoneProvider implements Dumpable { currentState = currentState.newState(PROVIDER_STATE_STOPPED, null, null, "initialize"); setCurrentState(currentState, false); + boolean initializationSuccess; + String initializationFailureReason; // Guard against uncaught exceptions due to initialization problems. try { - onInitialize(); + initializationSuccess = onInitialize(); + initializationFailureReason = "onInitialize() returned false"; } catch (RuntimeException e) { - warnLog("Unable to initialize the provider", e); + warnLog("Unable to initialize the provider due to exception", e); + initializationSuccess = false; + initializationFailureReason = "onInitialize() threw exception:" + e.getMessage(); + } + + if (!initializationSuccess) { currentState = currentState.newState(PROVIDER_STATE_PERM_FAILED, null, null, - "Failed to initialize: " + e.getMessage()); + "Failed to initialize: " + initializationFailureReason); setCurrentState(currentState, true); } } @@ -429,9 +470,12 @@ abstract class LocationTimeZoneProvider implements Dumpable { /** * Implemented by subclasses to do work during {@link #initialize}. + * + * @return returns {@code true} on success, {@code false} if the provider should be considered + * "permanently failed" / disabled */ @GuardedBy("mSharedLock") - abstract void onInitialize(); + abstract boolean onInitialize(); /** * Destroys the provider. Called after the provider is stopped. This instance will not be called diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java index a9b9884e0074..ed7ea00ec8f5 100644 --- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java @@ -35,6 +35,10 @@ import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringDef; +import android.app.time.DetectorStatusTypes; +import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus; +import android.app.time.LocationTimeZoneAlgorithmStatus; +import android.app.time.LocationTimeZoneAlgorithmStatus.ProviderStatus; import android.service.timezone.TimeZoneProviderEvent; import android.service.timezone.TimeZoneProviderSuggestion; import android.util.IndentingPrintWriter; @@ -44,6 +48,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.timezonedetector.ConfigurationInternal; import com.android.server.timezonedetector.Dumpable; import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; +import com.android.server.timezonedetector.LocationAlgorithmEvent; import com.android.server.timezonedetector.ReferenceWithHistory; import com.android.server.timezonedetector.location.ThreadingDomain.SingleRunnableQueue; @@ -83,8 +88,7 @@ import java.util.Objects; * <p>All incoming calls except for {@link * LocationTimeZoneProviderController#dump(android.util.IndentingPrintWriter, String[])} must be * made on the {@link android.os.Handler} thread of the {@link ThreadingDomain} passed to {@link - * #LocationTimeZoneProviderController(ThreadingDomain, LocationTimeZoneProvider, - * LocationTimeZoneProvider)}. + * #LocationTimeZoneProviderController}. * * <p>Provider / controller integration notes: * @@ -172,10 +176,10 @@ class LocationTimeZoneProviderController implements Dumpable { @GuardedBy("mSharedLock") private final ReferenceWithHistory<@State String> mState = new ReferenceWithHistory<>(10); - /** Contains the last suggestion actually made, if there is one. */ + /** Contains the last event reported, if there is one. */ @GuardedBy("mSharedLock") @Nullable - private GeolocationTimeZoneSuggestion mLastSuggestion; + private LocationAlgorithmEvent mLastEvent; LocationTimeZoneProviderController(@NonNull ThreadingDomain threadingDomain, @NonNull MetricsLogger metricsLogger, @@ -213,7 +217,7 @@ class LocationTimeZoneProviderController implements Dumpable { setState(STATE_PROVIDERS_INITIALIZING); mPrimaryProvider.initialize(providerListener); mSecondaryProvider.initialize(providerListener); - setState(STATE_STOPPED); + setStateAndReportStatusOnlyEvent(STATE_STOPPED, "initialize()"); alterProvidersStartedStateIfRequired( null /* oldConfiguration */, mCurrentUserConfiguration); @@ -273,13 +277,51 @@ class LocationTimeZoneProviderController implements Dumpable { // Enter destroyed state. mPrimaryProvider.destroy(); mSecondaryProvider.destroy(); - setState(STATE_DESTROYED); + setStateAndReportStatusOnlyEvent(STATE_DESTROYED, "destroy()"); } } /** - * Updates {@link #mState} if needed, and performs all the record-keeping / callbacks associated - * with state changes. + * Sets the state and reports an event containing the algorithm status and a {@code null} + * suggestion. + */ + @GuardedBy("mSharedLock") + private void setStateAndReportStatusOnlyEvent(@State String state, @NonNull String reason) { + setState(state); + + final GeolocationTimeZoneSuggestion suggestion = null; + LocationAlgorithmEvent event = + new LocationAlgorithmEvent(generateCurrentAlgorithmStatus(), suggestion); + event.addDebugInfo(reason); + reportEvent(event); + } + + /** + * Reports an event containing the algorithm status and the supplied suggestion. + */ + @GuardedBy("mSharedLock") + private void reportSuggestionEvent( + @NonNull GeolocationTimeZoneSuggestion suggestion, @NonNull String reason) { + LocationTimeZoneAlgorithmStatus algorithmStatus = generateCurrentAlgorithmStatus(); + LocationAlgorithmEvent event = new LocationAlgorithmEvent( + algorithmStatus, suggestion); + event.addDebugInfo(reason); + reportEvent(event); + } + + /** + * Sends an event immediately. This method updates {@link #mLastEvent}. + */ + @GuardedBy("mSharedLock") + private void reportEvent(@NonNull LocationAlgorithmEvent event) { + debugLog("makeSuggestion: suggestion=" + event); + mCallback.sendEvent(event); + mLastEvent = event; + } + + /** + * Updates the state if needed. This includes setting {@link #mState} and performing all the + * record-keeping / callbacks associated with state changes. */ @GuardedBy("mSharedLock") private void setState(@State String state) { @@ -300,17 +342,7 @@ class LocationTimeZoneProviderController implements Dumpable { // By definition, if both providers are stopped, the controller is uncertain. cancelUncertaintyTimeout(); - // If a previous "certain" suggestion has been made, then a new "uncertain" - // suggestion must now be made to indicate the controller {does not / no longer has} - // an opinion and will not be sending further updates (until at least the providers are - // re-started). - if (Objects.equals(mState.get(), STATE_CERTAIN)) { - GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - mEnvironment.elapsedRealtimeMillis(), - "Withdraw previous suggestion, providers are stopping: " + reason); - makeSuggestion(suggestion, STATE_UNCERTAIN); - } - setState(STATE_STOPPED); + setStateAndReportStatusOnlyEvent(STATE_STOPPED, "Providers stopped: " + reason); } @GuardedBy("mSharedLock") @@ -381,7 +413,7 @@ class LocationTimeZoneProviderController implements Dumpable { // timeout started when the primary entered {started uncertain} should be cancelled. if (newIsGeoDetectionExecutionEnabled) { - setState(STATE_INITIALIZING); + setStateAndReportStatusOnlyEvent(STATE_INITIALIZING, "initializing()"); // Try to start the primary provider. tryStartProvider(mPrimaryProvider, newConfiguration); @@ -397,13 +429,11 @@ class LocationTimeZoneProviderController implements Dumpable { ProviderState newSecondaryState = mSecondaryProvider.getCurrentState(); if (!newSecondaryState.isStarted()) { // If both providers are {perm failed} then the controller immediately - // reports uncertain. - GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - mEnvironment.elapsedRealtimeMillis(), - "Providers are failed:" - + " primary=" + mPrimaryProvider.getCurrentState() - + " secondary=" + mPrimaryProvider.getCurrentState()); - makeSuggestion(suggestion, STATE_FAILED); + // reports the failure. + String reason = "Providers are failed:" + + " primary=" + mPrimaryProvider.getCurrentState() + + " secondary=" + mPrimaryProvider.getCurrentState(); + setStateAndReportStatusOnlyEvent(STATE_FAILED, reason); } } } else { @@ -537,12 +567,10 @@ class LocationTimeZoneProviderController implements Dumpable { // If both providers are now terminated, then a suggestion must be sent informing the // time zone detector that there are no further updates coming in the future. - GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - mEnvironment.elapsedRealtimeMillis(), - "Both providers are terminated:" - + " primary=" + primaryCurrentState.provider - + ", secondary=" + secondaryCurrentState.provider); - makeSuggestion(suggestion, STATE_FAILED); + String reason = "Both providers are terminated:" + + " primary=" + primaryCurrentState.provider + + ", secondary=" + secondaryCurrentState.provider; + setStateAndReportStatusOnlyEvent(STATE_FAILED, reason); } } @@ -615,6 +643,9 @@ class LocationTimeZoneProviderController implements Dumpable { TimeZoneProviderSuggestion providerSuggestion = providerEvent.getSuggestion(); + // Set the current state so it is correct when the suggestion event is created. + setState(STATE_CERTAIN); + // For the suggestion's effectiveFromElapsedMillis, use the time embedded in the provider's // suggestion (which indicates the time when the provider detected the location used to // establish the time zone). @@ -623,15 +654,13 @@ class LocationTimeZoneProviderController implements Dumpable { // this would hinder the ability for the time_zone_detector to judge which suggestions are // based on newer information when comparing suggestions between different sources. long effectiveFromElapsedMillis = providerSuggestion.getElapsedRealtimeMillis(); - GeolocationTimeZoneSuggestion geoSuggestion = + GeolocationTimeZoneSuggestion suggestion = GeolocationTimeZoneSuggestion.createCertainSuggestion( effectiveFromElapsedMillis, providerSuggestion.getTimeZoneIds()); - - String debugInfo = "Event received provider=" + provider + String debugInfo = "Provider event received: provider=" + provider + ", providerEvent=" + providerEvent + ", suggestionCreationTime=" + mEnvironment.elapsedRealtimeMillis(); - geoSuggestion.addDebugInfo(debugInfo); - makeSuggestion(geoSuggestion, STATE_CERTAIN); + reportSuggestionEvent(suggestion, debugInfo); } @Override @@ -647,7 +676,7 @@ class LocationTimeZoneProviderController implements Dumpable { + mEnvironment.getProviderInitializationTimeoutFuzz()); ipw.println("uncertaintyDelay=" + mEnvironment.getUncertaintyDelay()); ipw.println("mState=" + mState.get()); - ipw.println("mLastSuggestion=" + mLastSuggestion); + ipw.println("mLastEvent=" + mLastEvent); ipw.println("State history:"); ipw.increaseIndent(); // level 2 @@ -668,19 +697,6 @@ class LocationTimeZoneProviderController implements Dumpable { } } - /** - * Sends an immediate suggestion and enters a new state if needed. This method updates - * mLastSuggestion and changes mStateEnum / reports the new state for metrics. - */ - @GuardedBy("mSharedLock") - private void makeSuggestion(@NonNull GeolocationTimeZoneSuggestion suggestion, - @State String newState) { - debugLog("makeSuggestion: suggestion=" + suggestion); - mCallback.suggest(suggestion); - mLastSuggestion = suggestion; - setState(newState); - } - /** Clears the uncertainty timeout. */ @GuardedBy("mSharedLock") private void cancelUncertaintyTimeout() { @@ -688,18 +704,16 @@ class LocationTimeZoneProviderController implements Dumpable { } /** - * Called when a provider has become "uncertain" about the time zone. + * Called when a provider has reported it is "uncertain" about the time zone. * * <p>A provider is expected to report its uncertainty as soon as it becomes uncertain, as * this enables the most flexibility for the controller to start other providers when there are - * multiple ones available. The controller is therefore responsible for deciding when to make a - * "uncertain" suggestion to the downstream time zone detector. + * multiple ones available. The controller is therefore responsible for deciding when to pass + * the "uncertain" suggestion to the downstream time zone detector. * * <p>This method schedules an "uncertainty" timeout (if one isn't already scheduled) to be * triggered later if nothing else preempts it. It can be preempted if the provider becomes - * certain (or does anything else that calls {@link - * #makeSuggestion(GeolocationTimeZoneSuggestion, String)}) within {@link - * Environment#getUncertaintyDelay()}. Preemption causes the scheduled + * certain within {@link Environment#getUncertaintyDelay()}. Preemption causes the scheduled * "uncertainty" timeout to be cancelled. If the provider repeatedly sends uncertainty events * within the uncertainty delay period, those events are effectively ignored (i.e. the timeout * is not reset each time). @@ -741,6 +755,8 @@ class LocationTimeZoneProviderController implements Dumpable { synchronized (mSharedLock) { long afterUncertaintyTimeoutElapsedMillis = mEnvironment.elapsedRealtimeMillis(); + setState(STATE_UNCERTAIN); + // For the effectiveFromElapsedMillis suggestion property, use the // uncertaintyStartedElapsedMillis. This is the time when the provider first reported // uncertainty, i.e. before the uncertainty timeout. @@ -749,30 +765,65 @@ class LocationTimeZoneProviderController implements Dumpable { // the location_time_zone_manager finally confirms that the time zone was uncertain, // but the suggestion property allows the information to be back-dated, which should // help when comparing suggestions from different sources. - GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion( - uncertaintyStartedElapsedMillis, - "Uncertainty timeout triggered for " + provider.getName() + ":" - + " primary=" + mPrimaryProvider - + ", secondary=" + mSecondaryProvider - + ", uncertaintyStarted=" - + Duration.ofMillis(uncertaintyStartedElapsedMillis) - + ", afterUncertaintyTimeout=" - + Duration.ofMillis(afterUncertaintyTimeoutElapsedMillis) - + ", uncertaintyDelay=" + uncertaintyDelay - ); - makeSuggestion(suggestion, STATE_UNCERTAIN); + GeolocationTimeZoneSuggestion suggestion = + GeolocationTimeZoneSuggestion.createUncertainSuggestion( + uncertaintyStartedElapsedMillis); + String debugInfo = "Uncertainty timeout triggered for " + provider.getName() + ":" + + " primary=" + mPrimaryProvider + + ", secondary=" + mSecondaryProvider + + ", uncertaintyStarted=" + + Duration.ofMillis(uncertaintyStartedElapsedMillis) + + ", afterUncertaintyTimeout=" + + Duration.ofMillis(afterUncertaintyTimeoutElapsedMillis) + + ", uncertaintyDelay=" + uncertaintyDelay; + reportSuggestionEvent(suggestion, debugInfo); } } + @GuardedBy("mSharedLock") @NonNull - private static GeolocationTimeZoneSuggestion createUncertainSuggestion( - @ElapsedRealtimeLong long effectiveFromElapsedMillis, - @NonNull String reason) { - GeolocationTimeZoneSuggestion suggestion = - GeolocationTimeZoneSuggestion.createUncertainSuggestion( - effectiveFromElapsedMillis); - suggestion.addDebugInfo(reason); - return suggestion; + private LocationTimeZoneAlgorithmStatus generateCurrentAlgorithmStatus() { + @State String controllerState = mState.get(); + ProviderState primaryProviderState = mPrimaryProvider.getCurrentState(); + ProviderState secondaryProviderState = mSecondaryProvider.getCurrentState(); + return createAlgorithmStatus(controllerState, primaryProviderState, secondaryProviderState); + } + + @NonNull + private static LocationTimeZoneAlgorithmStatus createAlgorithmStatus( + @NonNull @State String controllerState, + @NonNull ProviderState primaryProviderState, + @NonNull ProviderState secondaryProviderState) { + + @DetectionAlgorithmStatus int algorithmStatus = + mapControllerStateToDetectionAlgorithmStatus(controllerState); + @ProviderStatus int primaryProviderStatus = primaryProviderState.getProviderStatus(); + @ProviderStatus int secondaryProviderStatus = secondaryProviderState.getProviderStatus(); + + // Neither provider is running. The algorithm is not running. + return new LocationTimeZoneAlgorithmStatus(algorithmStatus, + primaryProviderStatus, primaryProviderState.getReportedStatus(), + secondaryProviderStatus, secondaryProviderState.getReportedStatus()); + } + + /** + * Maps the internal state enum value to one of the status values exposed to the layers above. + */ + private static @DetectionAlgorithmStatus int mapControllerStateToDetectionAlgorithmStatus( + @NonNull @State String controllerState) { + switch (controllerState) { + case STATE_INITIALIZING: + case STATE_PROVIDERS_INITIALIZING: + case STATE_CERTAIN: + case STATE_UNCERTAIN: + return DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; + case STATE_STOPPED: + case STATE_DESTROYED: + case STATE_FAILED: + case STATE_UNKNOWN: + default: + return DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; + } } /** @@ -798,8 +849,8 @@ class LocationTimeZoneProviderController implements Dumpable { synchronized (mSharedLock) { LocationTimeZoneManagerServiceState.Builder builder = new LocationTimeZoneManagerServiceState.Builder(); - if (mLastSuggestion != null) { - builder.setLastSuggestion(mLastSuggestion); + if (mLastEvent != null) { + builder.setLastEvent(mLastEvent); } builder.setControllerState(mState.get()) .setStateChanges(mRecordedStates) @@ -867,17 +918,15 @@ class LocationTimeZoneProviderController implements Dumpable { abstract static class Callback { @NonNull protected final ThreadingDomain mThreadingDomain; - @NonNull protected final Object mSharedLock; Callback(@NonNull ThreadingDomain threadingDomain) { mThreadingDomain = Objects.requireNonNull(threadingDomain); - mSharedLock = threadingDomain.getLockObject(); } /** * Suggests the latest time zone state for the device. */ - abstract void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion); + abstract void sendEvent(@NonNull LocationAlgorithmEvent event); } /** diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerCallbackImpl.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerCallbackImpl.java index 0c751aaa62c7..7eb7e01b539a 100644 --- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerCallbackImpl.java +++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerCallbackImpl.java @@ -19,7 +19,7 @@ package com.android.server.timezonedetector.location; import android.annotation.NonNull; import com.android.server.LocalServices; -import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; +import com.android.server.timezonedetector.LocationAlgorithmEvent; import com.android.server.timezonedetector.TimeZoneDetectorInternal; /** @@ -34,11 +34,11 @@ class LocationTimeZoneProviderControllerCallbackImpl } @Override - void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion) { + void sendEvent(@NonNull LocationAlgorithmEvent event) { mThreadingDomain.assertCurrentThread(); TimeZoneDetectorInternal timeZoneDetector = LocalServices.getService(TimeZoneDetectorInternal.class); - timeZoneDetector.suggestGeolocationTimeZone(suggestion); + timeZoneDetector.handleLocationAlgorithmEvent(event); } } diff --git a/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java deleted file mode 100644 index 9cb1813df6db..000000000000 --- a/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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. - */ - -package com.android.server.timezonedetector.location; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.os.SystemClock; -import android.service.timezone.TimeZoneProviderEvent; -import android.util.IndentingPrintWriter; - -/** - * A {@link LocationTimeZoneProviderProxy} that provides minimal responses needed for the {@link - * BinderLocationTimeZoneProvider} to operate correctly when there is no "real" provider - * configured / enabled. This can be used during development / testing, or in a production build - * when the platform supports more providers than are needed for an Android deployment. - * - * <p>For example, if the {@link LocationTimeZoneProviderController} supports a primary - * and a secondary {@link LocationTimeZoneProvider}, but only a primary is configured, the secondary - * config will be left null and the {@link LocationTimeZoneProviderProxy} implementation will be - * defaulted to a {@link NullLocationTimeZoneProviderProxy}. The {@link - * NullLocationTimeZoneProviderProxy} sends a "permanent failure" event immediately after being - * started for the first time, which ensures the {@link LocationTimeZoneProviderController} won't - * expect any further {@link TimeZoneProviderEvent}s to come from it, and won't attempt to use it - * again. - */ -class NullLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy { - - /** Creates the instance. */ - NullLocationTimeZoneProviderProxy( - @NonNull Context context, @NonNull ThreadingDomain threadingDomain) { - super(context, threadingDomain); - } - - @Override - void onInitialize() { - // No-op - } - - @Override - void onDestroy() { - // No-op - } - - @Override - void setRequest(@NonNull TimeZoneProviderRequest request) { - if (request.sendUpdates()) { - TimeZoneProviderEvent event = TimeZoneProviderEvent.createPermanentFailureEvent( - SystemClock.elapsedRealtime(), "Provider is disabled"); - handleTimeZoneProviderEvent(event); - } - } - - @Override - public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) { - synchronized (mSharedLock) { - ipw.println("{NullLocationTimeZoneProviderProxy}"); - } - } -} diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 3b7bf4880faa..715729310a06 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -1386,8 +1386,38 @@ class ActivityClientController extends IActivityClientController.Stub { } } + /** + * Return {@code true} when the given Activity is a relative Task root. That is, the rest of + * the Activities in the Task should be finished when it finishes. Otherwise, return {@code + * false}. + */ + private boolean isRelativeTaskRootActivity(ActivityRecord r, ActivityRecord taskRoot) { + // Not a relative root if the given Activity is not the root Activity of its TaskFragment. + final TaskFragment taskFragment = r.getTaskFragment(); + if (r != taskFragment.getActivity(ar -> !ar.finishing || ar == r, + false /* traverseTopToBottom */)) { + return false; + } + + // The given Activity is the relative Task root if its TaskFragment is a companion + // TaskFragment to the taskRoot (i.e. the taskRoot TF will be finished together). + return taskRoot.getTaskFragment().getCompanionTaskFragment() == taskFragment; + } + + private boolean isTopActivityInTaskFragment(ActivityRecord activity) { + return activity.getTaskFragment().topRunningActivity() == activity; + } + + private void requestCallbackFinish(IRequestFinishCallback callback) { + try { + callback.requestFinish(); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to invoke request finish callback", e); + } + } + @Override - public void onBackPressedOnTaskRoot(IBinder token, IRequestFinishCallback callback) { + public void onBackPressed(IBinder token, IRequestFinishCallback callback) { final long origId = Binder.clearCallingIdentity(); try { final Intent baseActivityIntent; @@ -1397,20 +1427,29 @@ class ActivityClientController extends IActivityClientController.Stub { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); if (r == null) return; - if (mService.mWindowOrganizerController.mTaskOrganizerController + final Task task = r.getTask(); + final ActivityRecord root = task.getRootActivity(false /*ignoreRelinquishIdentity*/, + true /*setToBottomIfNone*/); + final boolean isTaskRoot = r == root; + if (isTaskRoot) { + if (mService.mWindowOrganizerController.mTaskOrganizerController .handleInterceptBackPressedOnTaskRoot(r.getRootTask())) { - // This task is handled by a task organizer that has requested the back pressed - // callback. + // This task is handled by a task organizer that has requested the back + // pressed callback. + return; + } + } else if (!isRelativeTaskRootActivity(r, root)) { + // Finish the Activity if the activity is not the task root or relative root. + requestCallbackFinish(callback); return; } - final Task task = r.getTask(); - isLastRunningActivity = task.topRunningActivity() == r; + isLastRunningActivity = isTopActivityInTaskFragment(isTaskRoot ? root : r); - final boolean isBaseActivity = r.mActivityComponent.equals(task.realActivity); - baseActivityIntent = isBaseActivity ? r.intent : null; + final boolean isBaseActivity = root.mActivityComponent.equals(task.realActivity); + baseActivityIntent = isBaseActivity ? root.intent : null; - launchedFromHome = r.isLaunchSourceType(ActivityRecord.LAUNCH_SOURCE_TYPE_HOME); + launchedFromHome = root.isLaunchSourceType(ActivityRecord.LAUNCH_SOURCE_TYPE_HOME); } // If the activity is one of the main entry points for the application, then we should @@ -1425,16 +1464,12 @@ class ActivityClientController extends IActivityClientController.Stub { if (baseActivityIntent != null && isLastRunningActivity && ((launchedFromHome && ActivityRecord.isMainIntent(baseActivityIntent)) || isLauncherActivity(baseActivityIntent.getComponent()))) { - moveActivityTaskToBack(token, false /* nonRoot */); + moveActivityTaskToBack(token, true /* nonRoot */); return; } // The default option for handling the back button is to finish the Activity. - try { - callback.requestFinish(); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to invoke request finish callback", e); - } + requestCallbackFinish(callback); } finally { Binder.restoreCallingIdentity(origId); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 8f8b57fb8cf5..23ed188bca85 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -6867,6 +6867,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (r == null || r.getParent() == null) { return INVALID_TASK_ID; } + return getTaskForActivityLocked(r, onlyRoot); + } + + static int getTaskForActivityLocked(ActivityRecord r, boolean onlyRoot) { final Task task = r.task; if (onlyRoot && r.compareTo(task.getRootActivity( false /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/)) > 0) { diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index a857d900d771..3ec24d5d4d7d 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2136,7 +2136,7 @@ class ActivityStarter { mStartActivity.mUserId); if (act != null) { final Task task = act.getTask(); - boolean actuallyMoved = task.moveActivityToFrontLocked(act); + boolean actuallyMoved = task.moveActivityToFront(act); if (actuallyMoved) { // Only record if the activity actually moved. mMovedToTopActivity = act; diff --git a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java index 4c18d0b8a0dc..56edde09f747 100644 --- a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java +++ b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java @@ -57,7 +57,6 @@ import android.view.WindowInsets; import android.view.WindowInsets.Type; import android.view.WindowManager; import android.view.WindowManagerGlobal; -import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.Button; @@ -109,18 +108,13 @@ public class ImmersiveModeConfirmation { mContext = display.getDisplayId() == DEFAULT_DISPLAY ? uiContext : uiContext.createDisplayContext(display); mHandler = new H(looper); - mShowDelayMs = getNavBarExitDuration() * 3; + mShowDelayMs = context.getResources().getInteger(R.integer.dock_enter_exit_duration) * 3L; mPanicThresholdMs = context.getResources() .getInteger(R.integer.config_immersive_mode_confirmation_panic); mVrModeEnabled = vrModeEnabled; mCanSystemBarsBeShownByUser = canSystemBarsBeShownByUser; } - private long getNavBarExitDuration() { - Animation exit = AnimationUtils.loadAnimation(mContext, R.anim.dock_bottom_exit); - return exit != null ? exit.getDuration() : 0; - } - static boolean loadSetting(int currentUserId, Context context) { final boolean wasConfirmed = sConfirmed; sConfirmed = false; diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java index ccc71bb8c537..de42c55be4b3 100644 --- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java +++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java @@ -16,15 +16,21 @@ package com.android.server.wm; +import static android.hardware.display.DisplayManager.SWITCHING_TYPE_NONE; +import static android.hardware.display.DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY; + import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; +import android.hardware.display.DisplayManager; import android.view.Display; import android.view.Display.Mode; import android.view.DisplayInfo; +import android.view.Surface; import android.view.SurfaceControl.RefreshRateRange; import java.util.HashMap; +import java.util.Objects; /** * Policy to select a lower refresh rate for the display if applicable. @@ -154,39 +160,109 @@ class RefreshRatePolicy { return LAYER_PRIORITY_UNSET; } - float getPreferredRefreshRate(WindowState w) { + public static class FrameRateVote { + float mRefreshRate; + @Surface.FrameRateCompatibility int mCompatibility; + + FrameRateVote(float refreshRate, @Surface.FrameRateCompatibility int compatibility) { + update(refreshRate, compatibility); + } + + FrameRateVote() { + reset(); + } + + boolean update(float refreshRate, @Surface.FrameRateCompatibility int compatibility) { + if (!refreshRateEquals(refreshRate) || mCompatibility != compatibility) { + mRefreshRate = refreshRate; + mCompatibility = compatibility; + return true; + } + return false; + } + + boolean reset() { + return update(0, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof FrameRateVote)) { + return false; + } + + FrameRateVote other = (FrameRateVote) o; + return refreshRateEquals(other.mRefreshRate) + && mCompatibility == other.mCompatibility; + } + + @Override + public int hashCode() { + return Objects.hash(mRefreshRate, mCompatibility); + } + + @Override + public String toString() { + return "mRefreshRate=" + mRefreshRate + ", mCompatibility=" + mCompatibility; + } + + private boolean refreshRateEquals(float refreshRate) { + return mRefreshRate <= refreshRate + RefreshRateRange.FLOAT_TOLERANCE + && mRefreshRate >= refreshRate - RefreshRateRange.FLOAT_TOLERANCE; + } + } + + boolean updateFrameRateVote(WindowState w) { + @DisplayManager.SwitchingType int refreshRateSwitchingType = + mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType(); + + // If refresh rate switching is disabled there is no point to set the frame rate on the + // surface as the refresh rate will be limited by display manager to a single value + // and SurfaceFlinger wouldn't be able to change it anyways. + if (refreshRateSwitchingType == SWITCHING_TYPE_NONE) { + return w.mFrameRateVote.reset(); + } + // If app is animating, it's not able to control refresh rate because we want the animation // to run in default refresh rate. if (w.isAnimating(TRANSITION | PARENTS)) { - return 0; + return w.mFrameRateVote.reset(); } // If the app set a preferredDisplayModeId, the preferred refresh rate is the refresh rate // of that mode id. - final int preferredModeId = w.mAttrs.preferredDisplayModeId; - if (preferredModeId > 0) { - DisplayInfo info = w.getDisplayInfo(); - if (info != null) { - for (Display.Mode mode : info.supportedModes) { - if (preferredModeId == mode.getModeId()) { - return mode.getRefreshRate(); + if (refreshRateSwitchingType != SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY) { + final int preferredModeId = w.mAttrs.preferredDisplayModeId; + if (preferredModeId > 0) { + DisplayInfo info = w.getDisplayInfo(); + if (info != null) { + for (Display.Mode mode : info.supportedModes) { + if (preferredModeId == mode.getModeId()) { + return w.mFrameRateVote.update(mode.getRefreshRate(), + Surface.FRAME_RATE_COMPATIBILITY_EXACT); + + } } } } } if (w.mAttrs.preferredRefreshRate > 0) { - return w.mAttrs.preferredRefreshRate; + return w.mFrameRateVote.update(w.mAttrs.preferredRefreshRate, + Surface.FRAME_RATE_COMPATIBILITY_DEFAULT); } // If the app didn't set a preferred mode id or refresh rate, but it is part of the deny // list, we return the low refresh rate as the preferred one. - final String packageName = w.getOwningPackage(); - if (mHighRefreshRateDenylist.isDenylisted(packageName)) { - return mLowRefreshRateMode.getRefreshRate(); + if (refreshRateSwitchingType != SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY) { + final String packageName = w.getOwningPackage(); + if (mHighRefreshRateDenylist.isDenylisted(packageName)) { + return w.mFrameRateVote.update(mLowRefreshRateMode.getRefreshRate(), + Surface.FRAME_RATE_COMPATIBILITY_EXACT); + } } - return 0; + return w.mFrameRateVote.reset(); } float getPreferredMinRefreshRate(WindowState w) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index cdb332123fe2..b290bec8c4e0 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -1401,13 +1401,26 @@ class Task extends TaskFragment { * Reorder the history task so that the passed activity is brought to the front. * @return whether it was actually moved (vs already being top). */ - final boolean moveActivityToFrontLocked(ActivityRecord newTop) { + final boolean moveActivityToFront(ActivityRecord newTop) { ProtoLog.i(WM_DEBUG_ADD_REMOVE, "Removing and adding activity %s to root task at top " + "callers=%s", newTop, Debug.getCallers(4)); - int origDist = getDistanceFromTop(newTop); - positionChildAtTop(newTop); + final TaskFragment taskFragment = newTop.getTaskFragment(); + boolean moved; + if (taskFragment != this) { + if (taskFragment.isEmbedded() && taskFragment.getNonFinishingActivityCount() == 1) { + taskFragment.mClearedForReorderActivityToFront = true; + } + newTop.reparent(this, POSITION_TOP); + moved = true; + if (taskFragment.isEmbedded()) { + mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController + .onActivityReparentedToTask(newTop); + } + } else { + moved = moveChildToFront(newTop); + } updateEffectiveIntent(); - return getDistanceFromTop(newTop) != origDist; + return moved; } @Override @@ -1575,6 +1588,11 @@ class Task extends TaskFragment { removeChild(r, reason); }); } else { + // Finish or destroy apps from the bottom to ensure that all the other activity have + // been finished and the top task in another task gets resumed when a top activity is + // removed. Otherwise, shell transitions wouldn't run because there would be no event + // that sets the transition ready. + final boolean traverseTopToBottom = !mTransitionController.isShellTransitionsEnabled(); forAllActivities((r) -> { if (r.finishing || (excludingTaskOverlay && r.isTaskOverlay())) { return; @@ -1588,7 +1606,7 @@ class Task extends TaskFragment { } else { r.destroyIfPossible(reason); } - }); + }, traverseTopToBottom); } } @@ -3075,20 +3093,6 @@ class Task extends TaskFragment { }); } - void positionChildAtTop(ActivityRecord child) { - positionChildAt(child, POSITION_TOP); - } - - void positionChildAt(ActivityRecord child, int position) { - if (child == null) { - Slog.w(TAG_WM, - "Attempted to position of non-existing app"); - return; - } - - positionChildAt(position, child, false /* includeParents */); - } - void setTaskDescription(TaskDescription taskDescription) { mTaskDescription = taskDescription; } @@ -4291,13 +4295,14 @@ class Task extends TaskFragment { } /** - * @return true if the task is currently focused. + * @return {@code true} if the task is currently focused or one of its children is focused. */ boolean isFocused() { if (mDisplayContent == null || mDisplayContent.mFocusedApp == null) { return false; } - return mDisplayContent.mFocusedApp.getTask() == this; + final Task focusedTask = mDisplayContent.mFocusedApp.getTask(); + return focusedTask == this || (focusedTask != null && focusedTask.getParent() == this); } /** @@ -4317,6 +4322,8 @@ class Task extends TaskFragment { */ void onAppFocusChanged(boolean hasFocus) { dispatchTaskInfoChangedIfNeeded(false /* force */); + final Task parentTask = getParent().asTask(); + if (parentTask != null) parentTask.dispatchTaskInfoChangedIfNeeded(false /* force */); } void onPictureInPictureParamsChanged() { diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index dfcad48046b0..911a8da1db99 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -224,6 +224,14 @@ class TaskFragment extends WindowContainer<WindowContainer> { private TaskFragment mAdjacentTaskFragment; /** + * Unlike the {@link mAdjacentTaskFragment}, the companion TaskFragment is not always visually + * adjacent to this one, but this TaskFragment will be removed by the organizer if the + * companion TaskFragment is removed. + */ + @Nullable + private TaskFragment mCompanionTaskFragment; + + /** * Prevents duplicate calls to onTaskAppeared. */ boolean mTaskFragmentAppearedSent; @@ -241,6 +249,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { boolean mClearedTaskFragmentForPip; /** + * The last running activity of the TaskFragment was removed and added to the top-most of the + * Task because it was launched with FLAG_ACTIVITY_REORDER_TO_FRONT. + */ + boolean mClearedForReorderActivityToFront; + + /** * When we are in the process of pausing an activity, before starting the * next one, this variable holds the activity that is currently being paused. * @@ -396,6 +410,14 @@ class TaskFragment extends WindowContainer<WindowContainer> { } } + void setCompanionTaskFragment(@Nullable TaskFragment companionTaskFragment) { + mCompanionTaskFragment = companionTaskFragment; + } + + TaskFragment getCompanionTaskFragment() { + return mCompanionTaskFragment; + } + void resetAdjacentTaskFragment() { // Reset the adjacent TaskFragment if its adjacent TaskFragment is also this TaskFragment. if (mAdjacentTaskFragment != null && mAdjacentTaskFragment.mAdjacentTaskFragment == this) { @@ -1852,6 +1874,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { ActivityRecord r = topRunningActivity(); mClearedTaskForReuse = false; mClearedTaskFragmentForPip = false; + mClearedForReorderActivityToFront = false; final ActivityRecord addingActivity = child.asActivityRecord(); final boolean isAddingActivity = addingActivity != null; @@ -2440,6 +2463,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { positionInParent, mClearedTaskForReuse, mClearedTaskFragmentForPip, + mClearedForReorderActivityToFront, calculateMinDimension()); } @@ -2726,6 +2750,16 @@ class TaskFragment extends WindowContainer<WindowContainer> { return callback.test(this) ? this : null; } + /** + * Moves the passed child to front + * @return whether it was actually moved (vs already being top). + */ + boolean moveChildToFront(WindowContainer newTop) { + int origDist = getDistanceFromTop(newTop); + positionChildAt(POSITION_TOP, newTop, false /* includeParents */); + return getDistanceFromTop(newTop) != origDist; + } + String toFullString() { final StringBuilder sb = new StringBuilder(128); sb.append(this); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index de12a4ef7739..a15fc1201596 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -39,6 +39,7 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; @@ -996,6 +997,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub tf1.setAdjacentTaskFragment(tf2); effects |= TRANSACT_EFFECTS_LIFECYCLE; + // Clear the focused app if the focused app is no longer visible after reset the + // adjacent TaskFragments. + if (tf2 == null && tf1.getDisplayContent().mFocusedApp != null + && tf1.hasChild(tf1.getDisplayContent().mFocusedApp) + && !tf1.shouldBeVisible(null /* starting */)) { + tf1.getDisplayContent().setFocusedApp(null); + } + final Bundle bundle = hop.getLaunchOptions(); final WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = bundle != null ? new WindowContainerTransaction.TaskFragmentAdjacentParams( @@ -1109,6 +1118,22 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub effects |= sanitizeAndApplyHierarchyOp(wc, hop); break; } + case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: { + final IBinder fragmentToken = hop.getContainer(); + final IBinder companionToken = hop.getCompanionContainer(); + final TaskFragment fragment = mLaunchTaskFragments.get(fragmentToken); + final TaskFragment companion = companionToken != null ? mLaunchTaskFragments.get( + companionToken) : null; + if (fragment == null || !fragment.isAttached()) { + final Throwable exception = new IllegalArgumentException( + "Not allowed to set companion on invalid fragment tokens"); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, fragment, type, + exception); + break; + } + fragment.setCompanionTaskFragment(companion); + break; + } default: { // The other operations may change task order so they are skipped while in lock // task mode. The above operations are still allowed because they don't move @@ -1652,6 +1677,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: enforceTaskFragmentOrganized(func, hop.getNewParent(), organizer); break; + case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: + enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); + if (hop.getCompanionContainer() != null) { + enforceTaskFragmentOrganized(func, hop.getCompanionContainer(), organizer); + } + break; case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); if (hop.getAdjacentRoot() != null) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 86dd0b5452b5..19409b1f3636 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -24,8 +24,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.graphics.GraphicsProtos.dumpPointProto; -import static android.hardware.display.DisplayManager.SWITCHING_TYPE_NONE; -import static android.hardware.display.DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.PowerManager.DRAW_WAKE_LOCK; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; @@ -203,7 +201,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.gui.TouchOcclusionMode; -import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Build; import android.os.Debug; @@ -261,6 +258,7 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.ToBooleanFunction; import com.android.server.policy.WindowManagerPolicy; import com.android.server.wm.LocalAnimationAdapter.AnimationSpec; +import com.android.server.wm.RefreshRatePolicy.FrameRateVote; import com.android.server.wm.SurfaceAnimator.AnimationType; import dalvik.annotation.optimization.NeverCompile; @@ -792,7 +790,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * preferredDisplayModeId or is part of the high refresh rate deny list. * The variable is cached, so we do not send too many updates to SF. */ - float mAppPreferredFrameRate = 0f; + FrameRateVote mFrameRateVote = new FrameRateVote(); static final int BLAST_TIMEOUT_DURATION = 5000; /* milliseconds */ @@ -5507,20 +5505,12 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mFrameRateSelectionPriority); } - // If refresh rate switching is disabled there is no point to set the frame rate on the - // surface as the refresh rate will be limited by display manager to a single value - // and SurfaceFlinger wouldn't be able to change it anyways. - @DisplayManager.SwitchingType int refreshRateSwitchingType = - mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType(); - if (refreshRateSwitchingType != SWITCHING_TYPE_NONE - && refreshRateSwitchingType != SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY) { - final float refreshRate = refreshRatePolicy.getPreferredRefreshRate(this); - if (mAppPreferredFrameRate != refreshRate) { - mAppPreferredFrameRate = refreshRate; - getPendingTransaction().setFrameRate( - mSurfaceControl, mAppPreferredFrameRate, - Surface.FRAME_RATE_COMPATIBILITY_EXACT, Surface.CHANGE_FRAME_RATE_ALWAYS); - } + boolean voteChanged = refreshRatePolicy.updateFrameRateVote(this); + if (voteChanged) { + getPendingTransaction().setFrameRate( + mSurfaceControl, mFrameRateVote.mRefreshRate, + mFrameRateVote.mCompatibility, Surface.CHANGE_FRAME_RATE_ALWAYS); + } } @@ -6136,9 +6126,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mRedrawForSyncReported) { return false; } - if (mInRelayout && mPrepareSyncSeqId > 0) { - // The last sync seq id will return to the client, so there is no need to request the - // client to redraw. + if (mInRelayout && (mPrepareSyncSeqId > 0 || (mViewVisibility == View.VISIBLE + && mWinAnimator.mDrawState == DRAW_PENDING))) { + // The client will report draw if it gets the sync seq id from relayout or it is + // drawing for being visible, then no need to request redraw. return false; } return useBLASTSync(); diff --git a/services/core/jni/com_android_server_am_BatteryStatsService.cpp b/services/core/jni/com_android_server_am_BatteryStatsService.cpp index 16eaa77e0822..3678cedeb87f 100644 --- a/services/core/jni/com_android_server_am_BatteryStatsService.cpp +++ b/services/core/jni/com_android_server_am_BatteryStatsService.cpp @@ -98,7 +98,7 @@ class WakeupCallback : public BnSuspendCallback { public: binder::Status notifyWakeup(bool success, const std::vector<std::string>& wakeupReasons) override { - ALOGI("In wakeup_callback: %s", success ? "resumed from suspend" : "suspend aborted"); + ALOGV("In wakeup_callback: %s", success ? "resumed from suspend" : "suspend aborted"); bool reasonsCaptured = false; { std::unique_lock<std::mutex> reasonsLock(mReasonsMutex, std::defer_lock); diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 20303472e8db..5d0551b4b54c 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -1191,8 +1191,9 @@ bool NativeInputManager::filterInputEvent(const InputEvent* inputEvent, uint32_t static_cast<const KeyEvent*>(inputEvent)); break; case AINPUT_EVENT_TYPE_MOTION: - inputEventObj = android_view_MotionEvent_obtainAsCopy(env, - static_cast<const MotionEvent*>(inputEvent)); + inputEventObj = + android_view_MotionEvent_obtainAsCopy(env, + static_cast<const MotionEvent&>(*inputEvent)); break; default: return true; // dispatch the event normally diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index e07bc7761156..06d8e6280bef 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -18,12 +18,15 @@ package com.android.server.credentials; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.ComponentName; import android.content.Context; import android.credentials.CreateCredentialRequest; +import android.credentials.CreateCredentialResponse; import android.credentials.CredentialManager; import android.credentials.ICreateCredentialCallback; import android.credentials.ui.ProviderData; import android.credentials.ui.RequestInfo; +import android.os.RemoteException; import android.service.credentials.CredentialProviderInfo; import android.util.Log; @@ -35,7 +38,8 @@ import java.util.ArrayList; * provider(s) state maintained in {@link ProviderCreateSession}. */ public final class CreateRequestSession extends RequestSession<CreateCredentialRequest, - ICreateCredentialCallback> { + ICreateCredentialCallback> + implements ProviderSession.ProviderInternalCallback<CreateCredentialResponse> { private static final String TAG = "CreateRequestSession"; CreateRequestSession(@NonNull Context context, int userId, @@ -72,4 +76,29 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR mRequestId, mClientRequest, mIsFirstUiTurn, mClientCallingPackage), providerDataList)); } + + private void respondToClientAndFinish(CreateCredentialResponse response) { + Log.i(TAG, "respondToClientAndFinish"); + try { + mClientCallback.onResponse(response); + } catch (RemoteException e) { + e.printStackTrace(); + } + finishSession(); + } + + @Override + public void onProviderStatusChanged(ProviderSession.Status status, + ComponentName componentName) { + super.onProviderStatusChanged(status, componentName); + } + + @Override + public void onFinalResponseReceived(ComponentName componentName, + CreateCredentialResponse response) { + Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); + if (response != null) { + respondToClientAndFinish(response); + } + } } diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 8238632fc6c2..8a698cab92a1 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -17,16 +17,14 @@ package com.android.server.credentials; import android.annotation.Nullable; +import android.content.ComponentName; import android.content.Context; -import android.credentials.Credential; import android.credentials.GetCredentialRequest; import android.credentials.GetCredentialResponse; import android.credentials.IGetCredentialCallback; import android.credentials.ui.ProviderData; import android.credentials.ui.RequestInfo; -import android.credentials.ui.UserSelectionDialogResult; import android.os.RemoteException; -import android.service.credentials.CredentialEntry; import android.service.credentials.CredentialProviderInfo; import android.util.Log; @@ -37,7 +35,8 @@ import java.util.ArrayList; * responses from providers, and the UX app, and updates the provider(S) state. */ public final class GetRequestSession extends RequestSession<GetCredentialRequest, - IGetCredentialCallback> { + IGetCredentialCallback> + implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> { private static final String TAG = "GetRequestSession"; public GetRequestSession(Context context, int userId, @@ -67,23 +66,6 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest return providerGetSession; } - // TODO: Override for this method not needed once get selection logic is - // moved to ProviderGetSession - @Override - public void onUiSelection(UserSelectionDialogResult selection) { - String providerId = selection.getProviderId(); - ProviderGetSession providerSession = (ProviderGetSession) mProviders.get(providerId); - if (providerSession != null) { - CredentialEntry credentialEntry = providerSession.getCredentialEntry( - selection.getEntrySubkey()); - if (credentialEntry != null && credentialEntry.getCredential() != null) { - respondToClientAndFinish(credentialEntry.getCredential()); - } - // TODO : Handle action chips and authentication selection - } - // TODO : finish session and respond to client if provider not found - } - @Override protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) { mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newGetRequestInfo( @@ -91,9 +73,26 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest providerDataList)); } - private void respondToClientAndFinish(Credential credential) { + @Override // from provider session + public void onProviderStatusChanged(ProviderSession.Status status, + ComponentName componentName) { + super.onProviderStatusChanged(status, componentName); + } + + + @Override + public void onFinalResponseReceived(ComponentName componentName, + GetCredentialResponse response) { + Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); + if (response != null) { + respondToClientAndFinish(response); + } + } + + private void respondToClientAndFinish(GetCredentialResponse response) { + Log.i(TAG, "respondToClientAndFinish"); try { - mClientCallback.onResponse(new GetCredentialResponse(credential)); + mClientCallback.onResponse(response); } catch (RemoteException e) { e.printStackTrace(); } diff --git a/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java b/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java new file mode 100644 index 000000000000..4cdc4570ba90 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java @@ -0,0 +1,69 @@ +/* + * 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.credentials; + +import android.app.Activity; +import android.content.Intent; +import android.credentials.CreateCredentialResponse; +import android.credentials.Credential; +import android.credentials.ui.ProviderPendingIntentResponse; +import android.service.credentials.CredentialProviderService; +import android.service.credentials.CredentialsDisplayContent; + +/** + * Helper class for setting up pending intent, and extracting objects from it. + * + * @hide + */ +public class PendingIntentResultHandler { + /** Returns true if the result is successful and may contain result extras. */ + public static boolean isSuccessfulResponse( + ProviderPendingIntentResponse pendingIntentResponse) { + //TODO: Differentiate based on extra_error in the resultData + return pendingIntentResponse.getResultCode() == Activity.RESULT_OK; + } + + /** Extracts the {@link CredentialsDisplayContent} object added to the result data. */ + public static CredentialsDisplayContent extractCredentialsDisplayContent(Intent resultData) { + if (resultData == null) { + return null; + } + return resultData.getParcelableExtra( + CredentialProviderService.EXTRA_GET_CREDENTIALS_DISPLAY_CONTENT, + CredentialsDisplayContent.class); + } + + /** Extracts the {@link CreateCredentialResponse} object added to the result data. */ + public static CreateCredentialResponse extractCreateCredentialResponse(Intent resultData) { + if (resultData == null) { + return null; + } + return resultData.getParcelableExtra( + CredentialProviderService.EXTRA_CREATE_CREDENTIAL_RESPONSE, + CreateCredentialResponse.class); + } + + /** Extracts the {@link Credential} object added to the result data. */ + public static Credential extractCredential(Intent resultData) { + if (resultData == null) { + return null; + } + return resultData.getParcelableExtra( + CredentialProviderService.EXTRA_GET_CREDENTIAL, + Credential.class); + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 49c416f943c1..bf37bd2097e2 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -19,10 +19,12 @@ package com.android.server.credentials; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.app.PendingIntent; import android.content.Context; -import android.credentials.Credential; +import android.content.Intent; import android.credentials.ui.CreateCredentialProviderData; import android.credentials.ui.Entry; +import android.credentials.ui.ProviderPendingIntentResponse; import android.os.Bundle; import android.service.credentials.CreateCredentialRequest; import android.service.credentials.CreateCredentialResponse; @@ -166,35 +168,30 @@ public final class ProviderCreateSession extends ProviderSession< } @Override - public void onProviderIntentResult(Bundle resultData) { - Credential credential = resultData.getParcelable( - CredentialProviderService.EXTRA_SAVE_CREDENTIAL, - Credential.class); - if (credential == null) { - Log.i(TAG, "Credential returned from intent is null"); - return; - } - updateFinalCredentialResponse(credential); - } - - @Override - public void onUiEntrySelected(String entryType, String entryKey) { - if (entryType.equals(SAVE_ENTRY_KEY)) { - SaveEntry saveEntry = mUiSaveEntries.get(entryKey); - if (saveEntry == null) { - Log.i(TAG, "Save entry not found"); - return; - } - // TODO: Uncomment when pending intent works - // onSaveEntrySelected(saveEntry); + public void onUiEntrySelected(String entryType, String entryKey, + ProviderPendingIntentResponse providerPendingIntentResponse) { + switch (entryType) { + case SAVE_ENTRY_KEY: + if (mUiSaveEntries.containsKey(entryKey)) { + onSaveEntrySelected(providerPendingIntentResponse); + } else { + //TODO: Handle properly + Log.i(TAG, "Unexpected save entry key"); + } + break; + case REMOTE_ENTRY_KEY: + if (mUiRemoteEntry.first.equals(entryKey)) { + onRemoteEntrySelected(providerPendingIntentResponse); + } else { + //TODO: Handle properly + Log.i(TAG, "Unexpected remote entry key"); + } + break; + default: + Log.i(TAG, "Unsupported entry type selected"); } } - @Override - public void onProviderIntentCancelled() { - //TODO (Implement) - } - private List<Entry> prepareUiSaveEntries(@NonNull List<SaveEntry> saveEntries) { Log.i(TAG, "in populateUiSaveEntries"); List<Entry> uiSaveEntries = new ArrayList<>(); @@ -204,14 +201,17 @@ public final class ProviderCreateSession extends ProviderSession< String entryId = generateEntryId(); mUiSaveEntries.put(entryId, saveEntry); Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId); - uiSaveEntries.add(new Entry(SAVE_ENTRY_KEY, entryId, saveEntry.getSlice())); + uiSaveEntries.add(new Entry(SAVE_ENTRY_KEY, entryId, saveEntry.getSlice(), + saveEntry.getPendingIntent(), setUpFillInIntent(saveEntry.getPendingIntent()))); } return uiSaveEntries; } - private void updateFinalCredentialResponse(@NonNull Credential credential) { - mFinalCredentialResponse = credential; - updateStatusAndInvokeCallback(Status.CREDENTIAL_RECEIVED_FROM_INTENT); + private Intent setUpFillInIntent(PendingIntent pendingIntent) { + Intent intent = pendingIntent.getIntent(); + intent.putExtra(CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS, + mCompleteRequest.getData()); + return intent; } private CreateCredentialProviderData prepareUiProviderData(List<Entry> saveEntries, @@ -223,9 +223,20 @@ public final class ProviderCreateSession extends ProviderSession< .build(); } - private void onSaveEntrySelected(SaveEntry saveEntry) { - mProviderIntentController.setupAndInvokePendingIntent(saveEntry.getPendingIntent(), - mProviderRequest); - setStatus(Status.PENDING_INTENT_INVOKED); + private void onSaveEntrySelected(ProviderPendingIntentResponse pendingIntentResponse) { + if (pendingIntentResponse == null) { + return; + //TODO: Handle failure if pending intent is null + } + if (PendingIntentResultHandler.isSuccessfulResponse(pendingIntentResponse)) { + android.credentials.CreateCredentialResponse credentialResponse = + PendingIntentResultHandler.extractCreateCredentialResponse( + pendingIntentResponse.getResultData()); + if (credentialResponse != null) { + mCallbacks.onFinalResponseReceived(mComponentName, credentialResponse); + return; + } + } + //TODO: Handle failure case is pending intent response does not have a credential } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 362d98167462..d63cdebe0e1b 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -20,16 +20,20 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.Context; +import android.credentials.Credential; import android.credentials.GetCredentialOption; +import android.credentials.GetCredentialResponse; import android.credentials.ui.Entry; import android.credentials.ui.GetCredentialProviderData; -import android.os.Bundle; +import android.credentials.ui.ProviderPendingIntentResponse; import android.service.credentials.Action; import android.service.credentials.CredentialEntry; import android.service.credentials.CredentialProviderInfo; +import android.service.credentials.CredentialsDisplayContent; import android.service.credentials.GetCredentialsRequest; import android.service.credentials.GetCredentialsResponse; import android.util.Log; +import android.util.Pair; import android.util.Slog; import java.util.ArrayList; @@ -53,11 +57,17 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsRequ // Key to be used as an entry key for a credential entry private static final String CREDENTIAL_ENTRY_KEY = "credential_key"; + // Key to be used as the entry key for an action entry + private static final String ACTION_ENTRY_KEY = "action_key"; + // Key to be used as the entry key for the authentication entry + private static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key"; + @NonNull private final Map<String, CredentialEntry> mUiCredentialEntries = new HashMap<>(); @NonNull private final Map<String, Action> mUiActionsEntries = new HashMap<>(); - private Action mAuthenticationAction = null; + @Nullable + private Pair<String, Action> mUiAuthenticationAction = null; /** Creates a new provider session to be used by the request session. */ @Nullable public static ProviderGetSession createNewSession( @@ -85,7 +95,8 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsRequ List<GetCredentialOption> filteredOptions = new ArrayList<>(); for (GetCredentialOption option : clientRequest.getGetCredentialOptions()) { if (providerCapabilities.contains(option.getType())) { - Log.i(TAG, "In createProviderRequest - capability found : " + option.getType()); + Log.i(TAG, "In createProviderRequest - capability found : " + + option.getType()); filteredOptions.add(option); } else { Log.i(TAG, "In createProviderRequest - capability not " @@ -139,19 +150,47 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsRequ } } - @Override // Callback from the provider intent controller class - public void onProviderIntentResult(Bundle resultData) { - // TODO : Implement - } - - @Override - public void onProviderIntentCancelled() { - // TODO : Implement - } - @Override // Selection call from the request provider - protected void onUiEntrySelected(String entryType, String entryId) { - // TODO: Implement + protected void onUiEntrySelected(String entryType, String entryKey, + ProviderPendingIntentResponse providerPendingIntentResponse) { + switch (entryType) { + case CREDENTIAL_ENTRY_KEY: + CredentialEntry credentialEntry = mUiCredentialEntries.get(entryKey); + if (credentialEntry == null) { + Log.i(TAG, "Credential entry not found"); + //TODO: Handle properly + return; + } + onCredentialEntrySelected(credentialEntry, providerPendingIntentResponse); + break; + case ACTION_ENTRY_KEY: + Action actionEntry = mUiActionsEntries.get(entryKey); + if (actionEntry == null) { + Log.i(TAG, "Action entry not found"); + //TODO: Handle properly + return; + } + onActionEntrySelected(providerPendingIntentResponse); + break; + case AUTHENTICATION_ACTION_ENTRY_KEY: + if (mUiAuthenticationAction.first.equals(entryKey)) { + onAuthenticationEntrySelected(providerPendingIntentResponse); + } else { + //TODO: Handle properly + Log.i(TAG, "Authentication entry not found"); + } + break; + case REMOTE_ENTRY_KEY: + if (mUiRemoteEntry.first.equals(entryKey)) { + onRemoteEntrySelected(providerPendingIntentResponse); + } else { + //TODO: Handle properly + Log.i(TAG, "Remote entry not found"); + } + break; + default: + Log.i(TAG, "Unsupported entry type selected"); + } } @Override // Call from request session to data to be shown on the UI @@ -162,32 +201,46 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsRequ + mComponentName.flattenToString()); return null; } - GetCredentialsResponse response = getProviderResponse(); - if (response == null) { + if (mProviderResponse == null) { Log.i(TAG, "In prepareUiData response null"); throw new IllegalStateException("Response must be in completion mode"); } - if (response.getAuthenticationAction() != null) { + if (mProviderResponse.getAuthenticationAction() != null) { Log.i(TAG, "In prepareUiData - top level authentication mode"); return prepareUiProviderData(null, null, - prepareUiAuthenticationActionEntry(response.getAuthenticationAction()), + prepareUiAuthenticationAction(mProviderResponse.getAuthenticationAction()), /*remoteEntry=*/null); } - if (response.getCredentialsDisplayContent() != null){ + if (mProviderResponse.getCredentialsDisplayContent() != null) { Log.i(TAG, "In prepareUiData displayContent not null"); - return prepareUiProviderData(populateUiActionEntries( - response.getCredentialsDisplayContent().getActions()), - prepareUiCredentialEntries(response.getCredentialsDisplayContent() + return prepareUiProviderData(prepareUiActionEntries( + mProviderResponse.getCredentialsDisplayContent().getActions()), + prepareUiCredentialEntries(mProviderResponse.getCredentialsDisplayContent() .getCredentialEntries()), - /*authenticationActionEntry=*/null, /*remoteEntry=*/null); + /*authenticationAction=*/null, + prepareUiRemoteEntry(mProviderResponse + .getCredentialsDisplayContent().getRemoteCredentialEntry())); } return null; } - private Entry prepareUiAuthenticationActionEntry(@NonNull Action authenticationAction) { + private Entry prepareUiRemoteEntry(Action remoteCredentialEntry) { + if (remoteCredentialEntry == null) { + return null; + } + String entryId = generateEntryId(); + Entry remoteEntry = new Entry(REMOTE_ENTRY_KEY, entryId, remoteCredentialEntry.getSlice()); + mUiRemoteEntry = new Pair<>(entryId, remoteCredentialEntry); + return remoteEntry; + } + + private Entry prepareUiAuthenticationAction(@NonNull Action authenticationAction) { String entryId = generateEntryId(); - mUiActionsEntries.put(entryId, authenticationAction); - return new Entry(ACTION_ENTRY_KEY, entryId, authenticationAction.getSlice()); + Entry authEntry = new Entry( + AUTHENTICATION_ACTION_ENTRY_KEY, entryId, authenticationAction.getSlice(), + authenticationAction.getPendingIntent(), /*fillInIntent=*/null); + mUiAuthenticationAction = new Pair<>(entryId, authenticationAction); + return authEntry; } private List<Entry> prepareUiCredentialEntries(@NonNull @@ -200,19 +253,28 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsRequ String entryId = generateEntryId(); mUiCredentialEntries.put(entryId, credentialEntry); Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId); - credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, - credentialEntry.getSlice())); + if (credentialEntry.getPendingIntent() != null) { + credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, + credentialEntry.getSlice(), credentialEntry.getPendingIntent(), + /*fillInIntent=*/null)); + } else if (credentialEntry.getCredential() != null) { + credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, + credentialEntry.getSlice())); + } else { + Log.i(TAG, "No credential or pending intent. Should not happen."); + } } return credentialUiEntries; } - private List<Entry> populateUiActionEntries(@Nullable List<Action> actions) { + private List<Entry> prepareUiActionEntries(@Nullable List<Action> actions) { List<Entry> actionEntries = new ArrayList<>(); for (Action action : actions) { String entryId = UUID.randomUUID().toString(); mUiActionsEntries.put(entryId, action); // TODO : Remove conversion of string to int after change in Entry class - actionEntries.add(new Entry(ACTION_ENTRY_KEY, entryId, action.getSlice())); + actionEntries.add(new Entry(ACTION_ENTRY_KEY, entryId, action.getSlice(), + action.getPendingIntent(), /*fillInIntent=*/null)); } return actionEntries; } @@ -224,16 +286,61 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsRequ mComponentName.flattenToString()).setActionChips(actionEntries) .setCredentialEntries(credentialEntries) .setAuthenticationEntry(authenticationActionEntry) + .setRemoteEntry(remoteEntry) .build(); } + private void onCredentialEntrySelected(CredentialEntry credentialEntry, + ProviderPendingIntentResponse providerPendingIntentResponse) { + if (credentialEntry.getCredential() != null) { + mCallbacks.onFinalResponseReceived(mComponentName, new GetCredentialResponse( + credentialEntry.getCredential())); + return; + } else if (providerPendingIntentResponse != null) { + if (PendingIntentResultHandler.isSuccessfulResponse(providerPendingIntentResponse)) { + Credential credential = PendingIntentResultHandler.extractCredential( + providerPendingIntentResponse.getResultData()); + if (credential != null) { + mCallbacks.onFinalResponseReceived(mComponentName, + new GetCredentialResponse(credential)); + return; + } + } + // TODO: Handle other pending intent statuses + } + Log.i(TAG, "CredentialEntry does not have a credential or a pending intent result"); + // TODO: Propagate failure to client + } + + private void onAuthenticationEntrySelected( + @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) { + if (providerPendingIntentResponse != null) { + if (PendingIntentResultHandler.isSuccessfulResponse(providerPendingIntentResponse)) { + CredentialsDisplayContent content = PendingIntentResultHandler + .extractCredentialsDisplayContent(providerPendingIntentResponse + .getResultData()); + if (content != null) { + onUpdateResponse(GetCredentialsResponse.createWithDisplayContent(content)); + return; + } + } + //TODO: Other provider intent statuses + } + Log.i(TAG, "Display content not present in pending intent result"); + // TODO: Propagate error to client + } + + private void onActionEntrySelected(ProviderPendingIntentResponse + providerPendingIntentResponse) { + //TODO: Implement if any result expected after an action + } + + /** Updates the response being maintained in state by this provider session. */ private void onUpdateResponse(GetCredentialsResponse response) { mProviderResponse = response; if (response.getAuthenticationAction() != null) { Log.i(TAG , "updateResponse with authentication entry"); - // TODO validate authentication action - mAuthenticationAction = response.getAuthenticationAction(); updateStatusAndInvokeCallback(Status.REQUIRES_AUTHENTICATION); } else if (response.getCredentialsDisplayContent() != null) { Log.i(TAG , "updateResponse with credentialEntries"); diff --git a/services/credentials/java/com/android/server/credentials/ProviderIntentController.java b/services/credentials/java/com/android/server/credentials/ProviderIntentController.java deleted file mode 100644 index 0f2e8ecdbbc6..000000000000 --- a/services/credentials/java/com/android/server/credentials/ProviderIntentController.java +++ /dev/null @@ -1,117 +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.credentials; - -import android.annotation.NonNull; -import android.annotation.SuppressLint; -import android.annotation.UserIdInt; -import android.app.Activity; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.IntentSender; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.Parcel; -import android.os.ResultReceiver; -import android.service.credentials.CreateCredentialRequest; -import android.service.credentials.CredentialProviderService; -import android.util.Log; - -/** - * Class that invokes providers' pending intents and listens to the responses. - */ -@SuppressLint("LongLogTag") -public class ProviderIntentController { - private static final String TAG = "ProviderIntentController"; - /** - * Interface to be implemented by any class that wishes to get callbacks from the UI. - */ - public interface ProviderIntentControllerCallback { - /** Called when the user makes a selection. */ - void onProviderIntentResult(Bundle resultData); - /** Called when the user cancels the UI. */ - void onProviderIntentCancelled(); - } - - private final int mUserId; - private final Context mContext; - private final ProviderIntentControllerCallback mCallback; - private final ResultReceiver mResultReceiver = new ResultReceiver( - new Handler(Looper.getMainLooper())) { - @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - Log.i(TAG, "onReceiveResult in providerIntentController"); - - if (resultCode == Activity.RESULT_OK) { - Log.i(TAG, "onReceiveResult - ACTIVITYOK"); - mCallback.onProviderIntentResult(resultData); - } else if (resultCode == Activity.RESULT_CANCELED) { - Log.i(TAG, "onReceiveResult - RESULTCANCELED"); - mCallback.onProviderIntentCancelled(); - } - // Drop unknown result - } - }; - - public ProviderIntentController(@UserIdInt int userId, - Context context, - ProviderIntentControllerCallback callback) { - mUserId = userId; - mContext = context; - mCallback = callback; - } - - /** Sets up the request data and invokes the given pending intent. */ - public void setupAndInvokePendingIntent(@NonNull PendingIntent pendingIntent, - CreateCredentialRequest request) { - Log.i(TAG, "in invokePendingIntent"); - setupIntent(pendingIntent, request); - Log.i(TAG, "in invokePendingIntent receiver set up"); - Log.i(TAG, "creator package: " + pendingIntent.getIntentSender() - .getCreatorPackage()); - - try { - mContext.startIntentSender(pendingIntent.getIntentSender(), - null, 0, 0, 0); - } catch (IntentSender.SendIntentException e) { - Log.i(TAG, "Error while invoking pending intent"); - } - - } - - private void setupIntent(PendingIntent pendingIntent, CreateCredentialRequest request) { - pendingIntent.getIntent().putExtra(Intent.EXTRA_RESULT_RECEIVER, - toIpcFriendlyResultReceiver(mResultReceiver)); - pendingIntent.getIntent().putExtra( - CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS, - request.getData()); - } - - private <T extends ResultReceiver> ResultReceiver toIpcFriendlyResultReceiver( - T resultReceiver) { - final Parcel parcel = Parcel.obtain(); - resultReceiver.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - - final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel); - parcel.recycle(); - - return ipcFriendly; - } -} diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 14a915754863..4a07f0a4e305 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -22,9 +22,11 @@ import android.content.ComponentName; import android.content.Context; import android.credentials.Credential; import android.credentials.ui.ProviderData; -import android.os.Bundle; +import android.credentials.ui.ProviderPendingIntentResponse; +import android.service.credentials.Action; import android.service.credentials.CredentialProviderException; import android.service.credentials.CredentialProviderInfo; +import android.util.Pair; import java.util.UUID; @@ -33,10 +35,10 @@ import java.util.UUID; * @param <T> The request to be sent to the provider * @param <R> The response to be expected from the provider */ -public abstract class ProviderSession<T, R> implements RemoteCredentialService.ProviderCallbacks<R>, - ProviderIntentController.ProviderIntentControllerCallback { - // Key to be used as the entry key for an action entry - protected static final String ACTION_ENTRY_KEY = "action_key"; +public abstract class ProviderSession<T, R> + implements RemoteCredentialService.ProviderCallbacks<R> { + // Key to be used as an entry key for a remote entry + protected static final String REMOTE_ENTRY_KEY = "remote_entry_key"; @NonNull protected final Context mContext; @NonNull protected final ComponentName mComponentName; @@ -45,17 +47,18 @@ public abstract class ProviderSession<T, R> implements RemoteCredentialService.P @NonNull protected final int mUserId; @NonNull protected Status mStatus = Status.NOT_STARTED; @NonNull protected final ProviderInternalCallback mCallbacks; - @NonNull protected final ProviderIntentController mProviderIntentController; @Nullable protected Credential mFinalCredentialResponse; @NonNull protected final T mProviderRequest; @Nullable protected R mProviderResponse; + @Nullable protected Pair<String, Action> mUiRemoteEntry; /** * Returns true if the given status reflects that the provider state is ready to be shown * on the credMan UI. */ public static boolean isUiInvokingStatus(Status status) { - return status == Status.CREDENTIALS_RECEIVED || status == Status.SAVE_ENTRIES_RECEIVED; + return status == Status.CREDENTIALS_RECEIVED || status == Status.SAVE_ENTRIES_RECEIVED + || status == Status.REQUIRES_AUTHENTICATION; } /** @@ -86,12 +89,14 @@ public abstract class ProviderSession<T, R> implements RemoteCredentialService.P * Interface to be implemented by any class that wishes to get a callback when a particular * provider session's status changes. Typically, implemented by the {@link RequestSession} * class. + * @param <V> the type of the final response expected */ - public interface ProviderInternalCallback { - /** - * Called when status changes. - */ + public interface ProviderInternalCallback<V> { + /** Called when status changes. */ void onProviderStatusChanged(Status status, ComponentName componentName); + + /** Called when the final credential to be returned to the client has been received. */ + void onFinalResponseReceived(ComponentName componentName, V response); } protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info, @@ -106,7 +111,6 @@ public abstract class ProviderSession<T, R> implements RemoteCredentialService.P mUserId = userId; mComponentName = info.getServiceInfo().getComponentName(); mRemoteCredentialService = remoteCredentialService; - mProviderIntentController = new ProviderIntentController(userId, context, this); } /** Provider status at various states of the request session. */ @@ -164,6 +168,11 @@ public abstract class ProviderSession<T, R> implements RemoteCredentialService.P mCallbacks.onProviderStatusChanged(status, mComponentName); } + protected void onRemoteEntrySelected( + ProviderPendingIntentResponse providerPendingIntentResponse) { + //TODO: Implement + } + /** Get the request to be sent to the provider. */ protected T getProviderRequest() { return mProviderRequest; @@ -179,12 +188,6 @@ public abstract class ProviderSession<T, R> implements RemoteCredentialService.P @Nullable protected abstract ProviderData prepareUiData(); /** Should be overridden to handle the selected entry from the UI. */ - protected abstract void onUiEntrySelected(String entryType, String entryId); - - @Override - public abstract void onProviderIntentResult(Bundle resultData); - - @Override - public abstract void onProviderIntentCancelled(); - + protected abstract void onUiEntrySelected(String entryType, String entryId, + ProviderPendingIntentResponse providerPendingIntentResponse); } diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index 056d0e8718be..71fc67ce5afd 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -37,8 +37,7 @@ import java.util.Map; * Base class of a request session, that listens to UI events. This class must be extended * every time a new response type is expected from the providers. */ -abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialManagerUiCallback, - ProviderSession.ProviderInternalCallback { +abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialManagerUiCallback{ private static final String TAG = "RequestSession"; // TODO: Revise access levels of attributes @@ -89,7 +88,7 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan } Log.i(TAG, "Provider session found"); providerSession.onUiEntrySelected(selection.getEntryKey(), - selection.getEntrySubkey()); + selection.getEntrySubkey(), selection.getPendingIntentProviderResponse()); } @Override // from CredentialManagerUiCallbacks @@ -98,8 +97,7 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan finishSession(); } - @Override // from provider session - public void onProviderStatusChanged(ProviderSession.Status status, + protected void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName) { Log.i(TAG, "in onStatusChanged with status: " + status); if (ProviderSession.isTerminatingStatus(status)) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 89cbf5324ed4..c58e8d500eb2 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -23,6 +23,7 @@ import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_DEFAULT; +import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_APP_STANDBY; import static android.app.admin.DeviceAdminReceiver.ACTION_COMPLIANCE_ACKNOWLEDGEMENT_REQUIRED; import static android.app.admin.DeviceAdminReceiver.EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE; import static android.app.admin.DevicePolicyManager.ACTION_CHECK_POLICY_COMPLIANCE; @@ -46,6 +47,7 @@ import static android.app.admin.DevicePolicyManager.DELEGATION_SECURITY_LOGGING; import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT; import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED; import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER; +import static android.app.admin.DevicePolicyManager.EXEMPT_FROM_APP_STANDBY; import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE; import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_IDS; import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE; @@ -330,6 +332,7 @@ import android.util.ArraySet; 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.SparseArray; @@ -671,6 +674,17 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { private @interface CopyAccountStatus {} /** + * Mapping of {@link android.app.admin.DevicePolicyManager.ApplicationExemptionConstants} to + * corresponding app-ops. + */ + private static final Map<Integer, String> APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS = + new ArrayMap<>(); + static { + APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.put( + EXEMPT_FROM_APP_STANDBY, OPSTR_SYSTEM_EXEMPT_FROM_APP_STANDBY); + } + + /** * Admin apps targeting Android S+ may not use * {@link android.app.admin.DevicePolicyManager#setPasswordQuality} to set password quality * on the {@code DevicePolicyManager} instance obtained by calling @@ -17016,6 +17030,88 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { }); } + @Override + public void setApplicationExemptions(String packageName, int[] exemptions) { + if (!mHasFeature) { + return; + } + Preconditions.checkStringNotEmpty(packageName, "Package name cannot be empty."); + Objects.requireNonNull(exemptions, "Application exemptions must not be null."); + Preconditions.checkArgument(areApplicationExemptionsValid(exemptions), + "Invalid application exemption constant found in application exemptions set."); + Preconditions.checkCallAuthorization( + hasCallingOrSelfPermission(permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS)); + + final CallerIdentity caller = getCallerIdentity(); + final ApplicationInfo packageInfo; + packageInfo = getPackageInfoWithNullCheck(packageName, caller); + + for (Map.Entry<Integer, String> entry : + APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.entrySet()) { + int currentMode = mInjector.getAppOpsManager().unsafeCheckOpNoThrow( + entry.getValue(), packageInfo.uid, packageInfo.packageName); + int newMode = ArrayUtils.contains(exemptions, entry.getKey()) + ? MODE_ALLOWED : MODE_DEFAULT; + mInjector.binderWithCleanCallingIdentity(() -> { + if (currentMode != newMode) { + mInjector.getAppOpsManager() + .setMode(entry.getValue(), + packageInfo.uid, + packageName, + newMode); + } + }); + } + } + + @Override + public int[] getApplicationExemptions(String packageName) { + if (!mHasFeature) { + return new int[0]; + } + Preconditions.checkStringNotEmpty(packageName, "Package name cannot be empty."); + Preconditions.checkCallAuthorization( + hasCallingOrSelfPermission(permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS)); + + final CallerIdentity caller = getCallerIdentity(); + final ApplicationInfo packageInfo; + packageInfo = getPackageInfoWithNullCheck(packageName, caller); + + IntArray appliedExemptions = new IntArray(0); + for (Map.Entry<Integer, String> entry : + APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.entrySet()) { + if (mInjector.getAppOpsManager().unsafeCheckOpNoThrow( + entry.getValue(), packageInfo.uid, packageInfo.packageName) == MODE_ALLOWED) { + appliedExemptions.add(entry.getKey()); + } + } + return appliedExemptions.toArray(); + } + + private ApplicationInfo getPackageInfoWithNullCheck(String packageName, CallerIdentity caller) { + final ApplicationInfo packageInfo = + mInjector.getPackageManagerInternal().getApplicationInfo( + packageName, + /* flags= */ 0, + caller.getUid(), + caller.getUserId()); + if (packageInfo == null) { + throw new ServiceSpecificException( + DevicePolicyManager.ERROR_PACKAGE_NAME_NOT_FOUND, + "Package name not found."); + } + return packageInfo; + } + + private boolean areApplicationExemptionsValid(int[] exemptions) { + for (int exemption : exemptions) { + if (!APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.containsKey(exemption)) { + return false; + } + } + return true; + } + private boolean isCallingFromPackage(String packageName, int callingUid) { return mInjector.binderWithCleanCallingIdentity(() -> { try { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index d406e300a0eb..433c170cfc92 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1798,17 +1798,18 @@ public final class SystemServer implements Dumpable { dpms = mSystemServiceManager.startService(DevicePolicyManagerService.Lifecycle.class); t.traceEnd(); - if (!isWatch) { - t.traceBegin("StartStatusBarManagerService"); - try { - statusBar = new StatusBarManagerService(context); - ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar, false, - DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO); - } catch (Throwable e) { - reportWtf("starting StatusBarManagerService", e); + t.traceBegin("StartStatusBarManagerService"); + try { + statusBar = new StatusBarManagerService(context); + if (!isWatch) { + statusBar.publishGlobalActionsProvider(); } - t.traceEnd(); + ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar, false, + DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO); + } catch (Throwable e) { + reportWtf("starting StatusBarManagerService", e); } + t.traceEnd(); if (deviceHasConfigString(context, R.string.config_defaultMusicRecognitionService)) { 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 0f7c0d73ad08..2f6b07bfb6f7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -466,7 +466,7 @@ public class BroadcastQueueModernImplTest { @Test public void testRunnableAt_Cached_Interactive() { final BroadcastOptions options = BroadcastOptions.makeBasic(); - options.setInteractiveBroadcast(true); + options.setInteractive(true); doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options, List.of(makeMockRegisteredReceiver()), null, false), REASON_CONTAINS_INTERACTIVE); } diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/OWNERS b/services/tests/mockingservicestests/src/com/android/server/backup/OWNERS new file mode 100644 index 000000000000..d99779e3d9da --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/backup/OWNERS @@ -0,0 +1 @@ +include /services/backup/OWNERS diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java index dc49a94eb5c5..4c28c51f7e62 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java @@ -144,7 +144,7 @@ public final class DisplayPowerController2Test { SensorManager sensorManager) { return new DisplayPowerProximityStateController(wakelockController, displayDeviceConfig, looper, nudgeUpdatePowerState, displayId, - sensorManager); + sensorManager, /* injector= */ null); } }; diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java new file mode 100644 index 000000000000..6e91b249b490 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java @@ -0,0 +1,407 @@ +/* + * 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.display; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.hardware.Sensor; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.display.DisplayManagerInternal; +import android.os.test.TestLooper; +import android.view.Display; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.testutils.OffsettableClock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class DisplayPowerProximityStateControllerTest { + @Mock + WakelockController mWakelockController; + + @Mock + DisplayDeviceConfig mDisplayDeviceConfig; + + @Mock + Runnable mNudgeUpdatePowerState; + + @Mock + SensorManager mSensorManager; + + private Sensor mProximitySensor; + private OffsettableClock mClock; + private TestLooper mTestLooper; + private SensorEventListener mSensorEventListener; + private DisplayPowerProximityStateController mDisplayPowerProximityStateController; + + @Before + public void before() throws Exception { + MockitoAnnotations.initMocks(this); + mClock = new OffsettableClock.Stopped(); + mTestLooper = new TestLooper(mClock::now); + when(mDisplayDeviceConfig.getProximitySensor()).thenReturn( + new DisplayDeviceConfig.SensorData() { + { + type = Sensor.STRING_TYPE_PROXIMITY; + // This is kept null because currently there is no way to define a sensor + // name in TestUtils + name = null; + } + }); + setUpProxSensor(); + DisplayPowerProximityStateController.Injector injector = + new DisplayPowerProximityStateController.Injector() { + @Override + DisplayPowerProximityStateController.Clock createClock() { + return new DisplayPowerProximityStateController.Clock() { + @Override + public long uptimeMillis() { + return mClock.now(); + } + }; + } + }; + mDisplayPowerProximityStateController = new DisplayPowerProximityStateController( + mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(), + mNudgeUpdatePowerState, 0, + mSensorManager, injector); + mSensorEventListener = mDisplayPowerProximityStateController.getProximitySensorListener(); + } + + @Test + public void updatePendingProximityRequestsWorksAsExpectedWhenPending() { + // Set the system to pending wait for proximity + assertTrue(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked( + true)); + assertTrue( + mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked()); + + // Update the pending proximity wait request + mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked(); + assertTrue(mDisplayPowerProximityStateController.getWaitingForNegativeProximity()); + assertFalse( + mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked()); + } + + @Test + public void updatePendingProximityRequestsWorksAsExpectedWhenNotPending() { + // Will not wait or be in the pending wait state of not already pending + mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked(); + assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity()); + assertFalse( + mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked()); + } + + @Test + public void updatePendingProximityRequestsWorksAsExpectedWhenPendingAndProximityIgnored() + throws Exception { + // Set the system to the state where it will ignore proximity unless changed + enableProximitySensor(); + emitAndValidatePositiveProximityEvent(); + mDisplayPowerProximityStateController.ignoreProximitySensorUntilChangedInternal(); + advanceTime(1); + assertTrue(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged()); + verify(mNudgeUpdatePowerState, times(2)).run(); + + // Do not set the system to pending wait for proximity + mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked(); + assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity()); + assertFalse( + mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked()); + + // Set the system to pending wait for proximity. But because the proximity is being + // ignored, it will not wait or not set the pending wait + assertTrue(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked( + true)); + mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked(); + assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity()); + assertFalse( + mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked()); + } + + @Test + public void cleanupDisablesTheProximitySensor() { + enableProximitySensor(); + mDisplayPowerProximityStateController.cleanup(); + verify(mSensorManager).unregisterListener( + mSensorEventListener); + assertFalse(mDisplayPowerProximityStateController.isProximitySensorEnabled()); + assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity()); + assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged()); + assertEquals(mDisplayPowerProximityStateController.getProximity(), + DisplayPowerProximityStateController.PROXIMITY_UNKNOWN); + when(mWakelockController.releaseWakelock( + WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE)).thenReturn(true); + assertEquals(mDisplayPowerProximityStateController.getPendingProximityDebounceTime(), -1); + } + + @Test + public void isProximitySensorAvailableReturnsTrueWhenAvailable() { + assertTrue(mDisplayPowerProximityStateController.isProximitySensorAvailable()); + } + + @Test + public void isProximitySensorAvailableReturnsFalseWhenNotAvailable() { + when(mDisplayDeviceConfig.getProximitySensor()).thenReturn( + new DisplayDeviceConfig.SensorData() { + { + type = null; + name = null; + } + }); + mDisplayPowerProximityStateController = new DisplayPowerProximityStateController( + mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(), + mNudgeUpdatePowerState, 1, + mSensorManager, null); + assertFalse(mDisplayPowerProximityStateController.isProximitySensorAvailable()); + } + + @Test + public void notifyDisplayDeviceChangedReloadsTheProximitySensor() throws Exception { + DisplayDeviceConfig updatedDisplayDeviceConfig = mock(DisplayDeviceConfig.class); + when(updatedDisplayDeviceConfig.getProximitySensor()).thenReturn( + new DisplayDeviceConfig.SensorData() { + { + type = Sensor.STRING_TYPE_PROXIMITY; + name = null; + } + }); + Sensor newProxSensor = TestUtils.createSensor( + Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY, 4.0f); + when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL))) + .thenReturn(List.of(newProxSensor)); + mDisplayPowerProximityStateController.notifyDisplayDeviceChanged( + updatedDisplayDeviceConfig); + assertTrue(mDisplayPowerProximityStateController.isProximitySensorAvailable()); + } + + @Test + public void setPendingWaitForNegativeProximityLockedWorksAsExpected() { + // Doesn't do anything not asked to wait + assertFalse(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked( + false)); + assertFalse( + mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked()); + + // Sets pending wait negative proximity if not already waiting + assertTrue(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked( + true)); + assertTrue( + mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked()); + + // Will not set pending wait negative proximity if already waiting + assertFalse(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked( + true)); + assertTrue( + mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked()); + + } + + @Test + public void evaluateProximityStateWhenRequestedUseOfProximitySensor() throws Exception { + // Enable the proximity sensor + enableProximitySensor(); + + // Emit a positive proximity event to move the system to a state to mimic a scenario + // where the system is in positive proximity + emitAndValidatePositiveProximityEvent(); + + // Again evaluate the proximity state, with system having positive proximity + setScreenOffBecauseOfPositiveProximityState(); + } + + @Test + public void evaluateProximityStateWhenScreenOffBecauseOfPositiveProximity() throws Exception { + // Enable the proximity sensor + enableProximitySensor(); + + // Emit a positive proximity event to move the system to a state to mimic a scenario + // where the system is in positive proximity + emitAndValidatePositiveProximityEvent(); + + // Again evaluate the proximity state, with system having positive proximity + setScreenOffBecauseOfPositiveProximityState(); + + // Set the system to pending wait for proximity + mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked(true); + // Update the pending proximity wait request + mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked(); + + // Start ignoring proximity sensor + mDisplayPowerProximityStateController.ignoreProximitySensorUntilChangedInternal(); + // Re-evaluate the proximity state, such that the system is detecting the positive + // proximity, and screen is off because of that + when(mWakelockController.getOnProximityNegativeRunnable()).thenReturn(mock(Runnable.class)); + mDisplayPowerProximityStateController.updateProximityState(mock( + DisplayManagerInternal.DisplayPowerRequest.class), Display.STATE_ON); + assertTrue(mDisplayPowerProximityStateController.isProximitySensorEnabled()); + assertFalse(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()); + assertTrue( + mDisplayPowerProximityStateController + .shouldSkipRampBecauseOfProximityChangeToNegative()); + verify(mWakelockController).acquireWakelock( + WakelockController.WAKE_LOCK_PROXIMITY_NEGATIVE); + } + + @Test + public void evaluateProximityStateWhenDisplayIsTurningOff() throws Exception { + // Enable the proximity sensor + enableProximitySensor(); + + // Emit a positive proximity event to move the system to a state to mimic a scenario + // where the system is in positive proximity + emitAndValidatePositiveProximityEvent(); + + // Again evaluate the proximity state, with system having positive proximity + setScreenOffBecauseOfPositiveProximityState(); + + // Re-evaluate the proximity state, such that the system is detecting the positive + // proximity, and screen is off because of that + mDisplayPowerProximityStateController.updateProximityState(mock( + DisplayManagerInternal.DisplayPowerRequest.class), Display.STATE_OFF); + verify(mSensorManager).unregisterListener( + mSensorEventListener); + assertFalse(mDisplayPowerProximityStateController.isProximitySensorEnabled()); + assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity()); + assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged()); + assertEquals(mDisplayPowerProximityStateController.getProximity(), + DisplayPowerProximityStateController.PROXIMITY_UNKNOWN); + when(mWakelockController.releaseWakelock( + WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE)).thenReturn(true); + assertEquals(mDisplayPowerProximityStateController.getPendingProximityDebounceTime(), -1); + } + + @Test + public void evaluateProximityStateNotWaitingForNegativeProximityAndNotUsingProxSensor() + throws Exception { + // Enable the proximity sensor + enableProximitySensor(); + + // Emit a positive proximity event to move the system to a state to mimic a scenario + // where the system is in positive proximity + emitAndValidatePositiveProximityEvent(); + + // Re-evaluate the proximity state, such that the system is detecting the positive + // proximity, and screen is off because of that + mDisplayPowerProximityStateController.updateProximityState(mock( + DisplayManagerInternal.DisplayPowerRequest.class), Display.STATE_ON); + verify(mSensorManager).unregisterListener( + mSensorEventListener); + assertFalse(mDisplayPowerProximityStateController.isProximitySensorEnabled()); + assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity()); + assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged()); + assertEquals(mDisplayPowerProximityStateController.getProximity(), + DisplayPowerProximityStateController.PROXIMITY_UNKNOWN); + when(mWakelockController.releaseWakelock( + WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE)).thenReturn(true); + assertEquals(mDisplayPowerProximityStateController.getPendingProximityDebounceTime(), -1); + } + + private void advanceTime(long timeMs) { + mClock.fastForward(timeMs); + mTestLooper.dispatchAll(); + } + + private void setUpProxSensor() throws Exception { + mProximitySensor = TestUtils.createSensor( + Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY, 5.0f); + when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL))) + .thenReturn(List.of(mProximitySensor)); + } + + private void emitAndValidatePositiveProximityEvent() throws Exception { + // Emit a positive proximity event to move the system to a state to mimic a scenario + // where the system is in positive proximity + when(mWakelockController.releaseWakelock( + WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE)).thenReturn(true); + mSensorEventListener.onSensorChanged(TestUtils.createSensorEvent(mProximitySensor, 4)); + verify(mSensorManager).registerListener(mSensorEventListener, + mProximitySensor, SensorManager.SENSOR_DELAY_NORMAL, + mDisplayPowerProximityStateController.getHandler()); + verify(mWakelockController).acquireWakelock( + WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE); + assertEquals(mDisplayPowerProximityStateController.getPendingProximity(), + DisplayPowerProximityStateController.PROXIMITY_POSITIVE); + assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged()); + assertEquals(mDisplayPowerProximityStateController.getProximity(), + DisplayPowerProximityStateController.PROXIMITY_POSITIVE); + verify(mNudgeUpdatePowerState).run(); + assertEquals(mDisplayPowerProximityStateController.getPendingProximityDebounceTime(), -1); + } + + // Call evaluateProximityState with the request for using the proximity sensor. This will + // register the proximity sensor listener, which will be needed for mocking positive + // proximity scenarios. + private void enableProximitySensor() { + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock( + DisplayManagerInternal.DisplayPowerRequest.class); + displayPowerRequest.useProximitySensor = true; + mDisplayPowerProximityStateController.updateProximityState(displayPowerRequest, + Display.STATE_ON); + verify(mSensorManager).registerListener( + mSensorEventListener, + mProximitySensor, SensorManager.SENSOR_DELAY_NORMAL, + mDisplayPowerProximityStateController.getHandler()); + assertTrue(mDisplayPowerProximityStateController.isProximitySensorEnabled()); + assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged()); + assertFalse(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()); + verifyZeroInteractions(mWakelockController); + } + + private void setScreenOffBecauseOfPositiveProximityState() { + // Prepare a request to indicate that the proximity sensor is to be used + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock( + DisplayManagerInternal.DisplayPowerRequest.class); + displayPowerRequest.useProximitySensor = true; + + Runnable onProximityPositiveRunnable = mock(Runnable.class); + when(mWakelockController.getOnProximityPositiveRunnable()).thenReturn( + onProximityPositiveRunnable); + + mDisplayPowerProximityStateController.updateProximityState(displayPowerRequest, + Display.STATE_ON); + verify(mSensorManager).registerListener( + mSensorEventListener, + mProximitySensor, SensorManager.SENSOR_DELAY_NORMAL, + mDisplayPowerProximityStateController.getHandler()); + assertTrue(mDisplayPowerProximityStateController.isProximitySensorEnabled()); + assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged()); + assertTrue(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()); + verify(mWakelockController).acquireWakelock( + WakelockController.WAKE_LOCK_PROXIMITY_POSITIVE); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java index 6279b87eb603..6e4d21456cd0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java @@ -17,6 +17,7 @@ package com.android.server.job; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -34,16 +35,20 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AppGlobals; +import android.app.IActivityManager; import android.app.job.JobInfo; import android.content.ComponentName; import android.content.Context; @@ -51,6 +56,8 @@ import android.content.pm.IPackageManager; import android.content.pm.UserInfo; import android.content.res.Resources; import android.os.Looper; +import android.os.PowerManager; +import android.os.RemoteException; import android.os.UserHandle; import android.provider.DeviceConfig; import android.util.ArrayMap; @@ -149,12 +156,14 @@ public final class JobConcurrencyManagerTest { R.bool.config_jobSchedulerRestrictBackgroundUser); when(mContext.getResources()).thenReturn(mResources); doReturn(mContext).when(jobSchedulerService).getTestableContext(); + doReturn(jobSchedulerService).when(jobSchedulerService).getLock(); mConfigBuilder = new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER); doAnswer((Answer<DeviceConfig.Properties>) invocationOnMock -> mConfigBuilder.build()) .when(() -> DeviceConfig.getProperties(eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER))); mPendingJobQueue = new PendingJobQueue(); doReturn(mPendingJobQueue).when(jobSchedulerService).getPendingJobQueue(); doReturn(mIPackageManager).when(AppGlobals::getPackageManager); + doReturn(mock(PowerManager.class)).when(mContext).getSystemService(PowerManager.class); mInjector = new InjectorForTest(); doAnswer((Answer<Long>) invocationOnMock -> { Object[] args = invocationOnMock.getArguments(); @@ -171,6 +180,16 @@ public final class JobConcurrencyManagerTest { createCurrentUser(true); mNextUserId = 10; mJobConcurrencyManager.mGracePeriodObserver = mGracePeriodObserver; + + IActivityManager activityManager = ActivityManager.getService(); + spyOn(activityManager); + try { + doNothing().when(activityManager).registerUserSwitchObserver(any(), anyString()); + } catch (RemoteException e) { + fail("registerUserSwitchObserver threw exception: " + e.getMessage()); + } + + mJobConcurrencyManager.onSystemReady(); } @After @@ -188,13 +207,16 @@ public final class JobConcurrencyManagerTest { final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>(); final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>(); final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>(); - final long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager - .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); + final JobConcurrencyManager.AssignmentInfo assignmentInfo = + new JobConcurrencyManager.AssignmentInfo(); + mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( + idle, preferredUidOnly, stoppable, assignmentInfo); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size()); assertEquals(0, preferredUidOnly.size()); assertEquals(0, stoppable.size()); - assertEquals(0, minPreferredUidOnlyWaitingTimeMs); + assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); + assertEquals(0, assignmentInfo.numRunningTopEj); } @Test @@ -207,13 +229,16 @@ public final class JobConcurrencyManagerTest { final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>(); final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>(); final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>(); - final long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager - .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); + final JobConcurrencyManager.AssignmentInfo assignmentInfo = + new JobConcurrencyManager.AssignmentInfo(); + mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( + idle, preferredUidOnly, stoppable, assignmentInfo); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size()); assertEquals(0, preferredUidOnly.size()); assertEquals(0, stoppable.size()); - assertEquals(0, minPreferredUidOnlyWaitingTimeMs); + assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); + assertEquals(0, assignmentInfo.numRunningTopEj); } @Test @@ -230,13 +255,45 @@ public final class JobConcurrencyManagerTest { final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>(); final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>(); final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>(); - final long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager - .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); + final JobConcurrencyManager.AssignmentInfo assignmentInfo = + new JobConcurrencyManager.AssignmentInfo(); + mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( + idle, preferredUidOnly, stoppable, assignmentInfo); assertEquals(0, idle.size()); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); assertEquals(0, stoppable.size()); - assertEquals(0, minPreferredUidOnlyWaitingTimeMs); + assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); + assertEquals(0, assignmentInfo.numRunningTopEj); + } + + @Test + public void testPrepareForAssignmentDetermination_onlyRunningTopEjs() { + for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { + JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i); + job.startedAsExpeditedJob = true; + job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP; + mJobConcurrencyManager.addRunningJobForTesting(job); + } + + for (int i = 0; i < mInjector.contexts.size(); ++i) { + doReturn(i % 2 == 0).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime(); + } + + final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>(); + final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>(); + final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>(); + final JobConcurrencyManager.AssignmentInfo assignmentInfo = + new JobConcurrencyManager.AssignmentInfo(); + mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( + idle, preferredUidOnly, stoppable, assignmentInfo); + + assertEquals(0, idle.size()); + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT / 2, preferredUidOnly.size()); + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT / 2, stoppable.size()); + assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); + assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, + assignmentInfo.numRunningTopEj); } @Test @@ -257,11 +314,13 @@ public final class JobConcurrencyManagerTest { final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>(); final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>(); final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>(); - mJobConcurrencyManager - .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); + final JobConcurrencyManager.AssignmentInfo assignmentInfo = + new JobConcurrencyManager.AssignmentInfo(); + mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( + idle, preferredUidOnly, stoppable, assignmentInfo); mJobConcurrencyManager .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, - Long.MAX_VALUE); + assignmentInfo); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, changed.size()); for (int i = changed.size() - 1; i >= 0; --i) { @@ -301,15 +360,17 @@ public final class JobConcurrencyManagerTest { final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>(); final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>(); final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>(); + final JobConcurrencyManager.AssignmentInfo assignmentInfo = + new JobConcurrencyManager.AssignmentInfo(); - long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager - .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); - assertEquals(remainingTimeMs, minPreferredUidOnlyWaitingTimeMs); + mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( + idle, preferredUidOnly, stoppable, assignmentInfo); + assertEquals(remainingTimeMs, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); mJobConcurrencyManager .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, - minPreferredUidOnlyWaitingTimeMs); + assignmentInfo); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); assertEquals(0, changed.size()); @@ -350,15 +411,17 @@ public final class JobConcurrencyManagerTest { final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>(); final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>(); final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>(); + final JobConcurrencyManager.AssignmentInfo assignmentInfo = + new JobConcurrencyManager.AssignmentInfo(); - long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager - .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); - assertEquals(remainingTimeMs, minPreferredUidOnlyWaitingTimeMs); + mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( + idle, preferredUidOnly, stoppable, assignmentInfo); + assertEquals(remainingTimeMs, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); mJobConcurrencyManager .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, - minPreferredUidOnlyWaitingTimeMs); + assignmentInfo); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); for (int i = changed.size() - 1; i >= 0; --i) { @@ -404,15 +467,17 @@ public final class JobConcurrencyManagerTest { final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>(); final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>(); final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>(); + final JobConcurrencyManager.AssignmentInfo assignmentInfo = + new JobConcurrencyManager.AssignmentInfo(); - long minPreferredUidOnlyWaitingTimeMs = mJobConcurrencyManager - .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable); - assertEquals(remainingTimeMs, minPreferredUidOnlyWaitingTimeMs); + mJobConcurrencyManager.prepareForAssignmentDeterminationLocked( + idle, preferredUidOnly, stoppable, assignmentInfo); + assertEquals(remainingTimeMs, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); mJobConcurrencyManager .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable, - minPreferredUidOnlyWaitingTimeMs); + assignmentInfo); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); // Depending on iteration order, we may create 1 or 2 contexts. diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt index 7ccd6d993f00..e0662c44b972 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt @@ -51,6 +51,7 @@ class DeletePackageHelperTest { mUserManagerInternal = rule.mocks().injector.userManagerInternal whenever(mUserManagerInternal.getUserIds()).thenReturn(intArrayOf(0, 1)) + whenever(mUserManagerInternal.getUserTypesForStatsd(any())).thenReturn(intArrayOf(1, 1)) mPms = createPackageManagerService() doAnswer { false }.`when`(mPms).isPackageDeviceAdmin(any(), any()) diff --git a/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java b/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java index 42c4129513d0..653ed1a144c0 100644 --- a/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java @@ -23,6 +23,7 @@ import android.app.job.JobScheduler; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.os.Bundle; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SystemProperties; @@ -36,8 +37,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.FileDescriptor; -import java.util.HashMap; -import java.util.Map; +import java.util.List; @RunWith(AndroidJUnit4.class) public class BinaryTransparencyServiceTest { @@ -96,7 +96,7 @@ public class BinaryTransparencyServiceTest { @Test public void getApexInfo_postInitialize_returnsValidEntries() throws RemoteException { prepApexInfo(); - Map result = mTestInterface.getApexInfo(); + List result = mTestInterface.getApexInfo(); Assert.assertNotNull("Apex info map should not be null", result); Assert.assertFalse("Apex info map should not be empty", result.isEmpty()); } @@ -105,13 +105,18 @@ public class BinaryTransparencyServiceTest { public void getApexInfo_postInitialize_returnsActualApexs() throws RemoteException, PackageManager.NameNotFoundException { prepApexInfo(); - Map result = mTestInterface.getApexInfo(); + List resultList = mTestInterface.getApexInfo(); PackageManager pm = mContext.getPackageManager(); Assert.assertNotNull(pm); - HashMap<PackageInfo, String> castedResult = (HashMap<PackageInfo, String>) result; - for (PackageInfo packageInfo : castedResult.keySet()) { - Assert.assertTrue(packageInfo.packageName + "is not an APEX!", packageInfo.isApex); + List<Bundle> castedResult = (List<Bundle>) resultList; + for (Bundle resultBundle : castedResult) { + PackageInfo resultPackageInfo = resultBundle.getParcelable( + BinaryTransparencyService.BUNDLE_PACKAGE_INFO, PackageInfo.class); + Assert.assertNotNull("PackageInfo for APEX should not be null", + resultPackageInfo); + Assert.assertTrue(resultPackageInfo.packageName + "is not an APEX!", + resultPackageInfo.isApex); } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java index 0cff4f14bf23..bb0063427339 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java @@ -125,12 +125,9 @@ public class ALSProbeTest { mProbe.destroy(); mProbe.enable(); - AtomicInteger lux = new AtomicInteger(10); - mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); - verify(mSensorManager, never()).registerListener(any(), any(), anyInt()); verifyNoMoreInteractions(mSensorManager); - assertThat(lux.get()).isLessThan(0); + assertThat(mProbe.getMostRecentLux()).isLessThan(0); } @Test @@ -323,15 +320,27 @@ public class ALSProbeTest { } @Test - public void testNoNextLuxWhenDestroyed() { + public void testDestroyAllowsAwaitLuxExactlyOnce() { + final float lastValue = 5.5f; mProbe.destroy(); - AtomicInteger lux = new AtomicInteger(-20); + AtomicInteger lux = new AtomicInteger(10); mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); - assertThat(lux.get()).isEqualTo(-1); - verify(mSensorManager, never()).registerListener( + verify(mSensorManager).registerListener( mSensorEventListenerCaptor.capture(), any(), anyInt()); + mSensorEventListenerCaptor.getValue().onSensorChanged( + new SensorEvent(mLightSensor, 1, 1, new float[]{lastValue})); + + assertThat(lux.get()).isEqualTo(Math.round(lastValue)); + verify(mSensorManager).unregisterListener(eq(mSensorEventListenerCaptor.getValue())); + + lux.set(22); + mProbe.enable(); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + mProbe.enable(); + + assertThat(lux.get()).isEqualTo(Math.round(lastValue)); verifyNoMoreInteractions(mSensorManager); } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 5fda3d6b36ab..c715a217f221 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -16,6 +16,9 @@ package com.android.server.companion.virtual; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES; import static com.google.common.truth.Truth.assertThat; @@ -44,6 +47,7 @@ import android.app.WindowConfiguration; import android.app.admin.DevicePolicyManager; import android.companion.AssociationInfo; import android.companion.virtual.IVirtualDeviceActivityListener; +import android.companion.virtual.VirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.audio.IAudioConfigChangedCallback; import android.companion.virtual.audio.IAudioRoutingCallback; @@ -240,6 +244,55 @@ public class VirtualDeviceManagerServiceTest { mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1, mInputController, (int associationId) -> {}, mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params); + mVdms.addVirtualDevice(mDeviceImpl); + } + + @Test + public void getDevicePolicy_invalidDeviceId_returnsDefault() { + assertThat( + mLocalService.getDevicePolicy( + VirtualDeviceManager.INVALID_DEVICE_ID, POLICY_TYPE_SENSORS)) + .isEqualTo(DEVICE_POLICY_DEFAULT); + } + + @Test + public void getDevicePolicy_defaultDeviceId_returnsDefault() { + assertThat( + mLocalService.getDevicePolicy( + VirtualDeviceManager.DEFAULT_DEVICE_ID, POLICY_TYPE_SENSORS)) + .isEqualTo(DEVICE_POLICY_DEFAULT); + } + + @Test + public void getDevicePolicy_nonExistentDeviceId_returnsDefault() { + assertThat( + mLocalService.getDevicePolicy(mDeviceImpl.getDeviceId() + 1, POLICY_TYPE_SENSORS)) + .isEqualTo(DEVICE_POLICY_DEFAULT); + } + + @Test + public void getDevicePolicy_unspecifiedPolicy_returnsDefault() { + assertThat( + mLocalService.getDevicePolicy(mDeviceImpl.getDeviceId(), POLICY_TYPE_SENSORS)) + .isEqualTo(DEVICE_POLICY_DEFAULT); + } + + @Test + public void getDevicePolicy_returnsCustom() { + VirtualDeviceParams params = new VirtualDeviceParams + .Builder() + .setBlockedActivities(getBlockedActivities()) + .addDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM) + .build(); + mDeviceImpl = new VirtualDeviceImpl(mContext, + mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1, + mInputController, (int associationId) -> {}, mPendingTrampolineCallback, + mActivityListener, mRunningAppsChangedCallback, params); + mVdms.addVirtualDevice(mDeviceImpl); + + assertThat( + mLocalService.getDevicePolicy(mDeviceImpl.getDeviceId(), POLICY_TYPE_SENSORS)) + .isEqualTo(DEVICE_POLICY_CUSTOM); } @Test diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java index 77f1e24ee771..036b6df92ef9 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java @@ -37,6 +37,8 @@ public class VirtualDeviceParamsTest { VirtualDeviceParams originalParams = new VirtualDeviceParams.Builder() .setLockState(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) .setUsersWithMatchingAccounts(Set.of(UserHandle.of(123), UserHandle.of(456))) + .addDevicePolicy(VirtualDeviceParams.POLICY_TYPE_SENSORS, + VirtualDeviceParams.DEVICE_POLICY_CUSTOM) .build(); Parcel parcel = Parcel.obtain(); originalParams.writeToParcel(parcel, 0); @@ -47,5 +49,7 @@ public class VirtualDeviceParamsTest { assertThat(params.getLockState()).isEqualTo(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED); assertThat(params.getUsersWithMatchingAccounts()) .containsExactly(UserHandle.of(123), UserHandle.of(456)); + assertThat(params.getDevicePolicy(VirtualDeviceParams.POLICY_TYPE_SENSORS)) + .isEqualTo(VirtualDeviceParams.DEVICE_POLICY_CUSTOM); } } diff --git a/services/tests/servicestests/src/com/android/server/display/TestUtils.java b/services/tests/servicestests/src/com/android/server/display/TestUtils.java index 0454587bfefe..a419b3f80aac 100644 --- a/services/tests/servicestests/src/com/android/server/display/TestUtils.java +++ b/services/tests/servicestests/src/com/android/server/display/TestUtils.java @@ -51,6 +51,12 @@ public final class TestUtils { } } + public static void setMaximumRange(Sensor sensor, float maximumRange) throws Exception { + Method setter = Sensor.class.getDeclaredMethod("setRange", Float.TYPE, Float.TYPE); + setter.setAccessible(true); + setter.invoke(sensor, maximumRange, 1); + } + public static Sensor createSensor(int type, String strType) throws Exception { Constructor<Sensor> constr = Sensor.class.getDeclaredConstructor(); constr.setAccessible(true); @@ -59,6 +65,16 @@ public final class TestUtils { return sensor; } + public static Sensor createSensor(int type, String strType, float maximumRange) + throws Exception { + Constructor<Sensor> constr = Sensor.class.getDeclaredConstructor(); + constr.setAccessible(true); + Sensor sensor = constr.newInstance(); + setSensorType(sensor, type, strType); + setMaximumRange(sensor, maximumRange); + return sensor; + } + /** * Create a custom {@link DisplayAddress} to ensure we're not relying on any specific * display-address implementation in our code. Intentionally uses default object (reference) diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java index fabf535b729a..d332b3081fdf 100644 --- a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java +++ b/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java @@ -39,8 +39,6 @@ public final class BrightnessEventTest { getReason(BrightnessReason.REASON_DOZE, BrightnessReason.MODIFIER_LOW_POWER)); mBrightnessEvent.setPhysicalDisplayId("test"); mBrightnessEvent.setLux(100.0f); - mBrightnessEvent.setFastAmbientLux(90.0f); - mBrightnessEvent.setSlowAmbientLux(85.0f); mBrightnessEvent.setPreThresholdLux(150.0f); mBrightnessEvent.setTime(System.currentTimeMillis()); mBrightnessEvent.setInitialBrightness(25.0f); @@ -50,6 +48,7 @@ public final class BrightnessEventTest { mBrightnessEvent.setRbcStrength(-1); mBrightnessEvent.setThermalMax(0.65f); mBrightnessEvent.setPowerFactor(0.2f); + mBrightnessEvent.setWasShortTermModelActive(true); mBrightnessEvent.setHbmMode(BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF); mBrightnessEvent.setFlags(0); mBrightnessEvent.setAdjustmentFlags(0); @@ -69,9 +68,9 @@ public final class BrightnessEventTest { String actualString = mBrightnessEvent.toString(false); String expectedString = "BrightnessEvent: disp=1, physDisp=test, brt=0.6, initBrt=25.0, rcmdBrt=0.6," - + " preBrt=NaN, lux=100.0, fastLux=90.0, slowLux=85.0, preLux=150.0, hbmMax=0.62," - + " hbmMode=off, rbcStrength=-1, thrmMax=0.65, powerFactor=0.2, flags=, reason=doze" - + " [ low_pwr ], autoBrightness=true"; + + " preBrt=NaN, lux=100.0, preLux=150.0, hbmMax=0.62, hbmMode=off, rbcStrength=-1," + + " thrmMax=0.65, powerFactor=0.2, wasShortTermModelActive=true, flags=," + + " reason=doze [ low_pwr ], autoBrightness=true"; assertEquals(expectedString, actualString); } 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 54baf18da92a..82c340145f8e 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -849,4 +849,53 @@ public class HdmiCecLocalDeviceTvTest { verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(), anyInt()); } + + @Test + public void tvSendRequestArcTerminationOnSleep() { + // Emulate Audio device on port 0x2000 (supports ARC) + + mNativeWrapper.setPortConnectionStatus(2, true); + HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( + ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); + mNativeWrapper.onCecMessage(hdmiCecMessage); + mTestLooper.dispatchAll(); + + mHdmiCecLocalDeviceTv.startArcAction(true); + mTestLooper.dispatchAll(); + HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation( + ADDR_TV, + ADDR_AUDIO_SYSTEM); + HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination( + ADDR_TV, + ADDR_AUDIO_SYSTEM); + HdmiCecMessage initiateArc = HdmiCecMessageBuilder.buildInitiateArc( + ADDR_AUDIO_SYSTEM, + ADDR_TV); + HdmiCecMessage reportArcInitiated = HdmiCecMessageBuilder.buildReportArcInitiated( + ADDR_TV, + ADDR_AUDIO_SYSTEM); + + assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcTermination); + + mNativeWrapper.onCecMessage(initiateArc); + mTestLooper.dispatchAll(); + + // Finish querying SADs + assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + + // ARC should be established after RequestSadAction is finished + assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated); + + mHdmiControlService.onStandby(HdmiControlService.STANDBY_SCREEN_OFF); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination); + } + } diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java index 164161e34b6f..dc47b5eaea0e 100644 --- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java @@ -8,7 +8,6 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -20,8 +19,6 @@ import android.content.Context; import android.content.pm.PackageManagerInternal; import android.net.NetworkRequest; import android.os.Build; -import android.os.Parcel; -import android.os.Parcelable; import android.os.PersistableBundle; import android.os.SystemClock; import android.test.RenamingDelegatingContext; @@ -32,7 +29,6 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.internal.util.HexDump; import com.android.server.LocalServices; import com.android.server.job.JobStore.JobSet; import com.android.server.job.controllers.JobStatus; @@ -44,7 +40,6 @@ import org.junit.runner.RunWith; import java.time.Clock; import java.time.ZoneOffset; -import java.util.Arrays; import java.util.Iterator; /** @@ -143,15 +138,8 @@ public class JobStoreTest { assertEquals("Didn't get expected number of persisted tasks.", 1, jobStatusSet.size()); final JobStatus loadedTaskStatus = jobStatusSet.getAllJobs().get(0); - assertTasksEqual(task, loadedTaskStatus.getJob()); + assertJobsEqual(ts, loadedTaskStatus); assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(ts)); - assertEquals("Different uids.", SOME_UID, loadedTaskStatus.getUid()); - assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION, - loadedTaskStatus.getInternalFlags()); - compareTimestampsSubjectToIoLatency("Early run-times not the same after read.", - ts.getEarliestRunTime(), loadedTaskStatus.getEarliestRunTime()); - compareTimestampsSubjectToIoLatency("Late run-times not the same after read.", - ts.getLatestRunTimeElapsed(), loadedTaskStatus.getLatestRunTimeElapsed()); } @Test @@ -202,19 +190,10 @@ public class JobStoreTest { loaded2 = tmp; } - assertTasksEqual(task1, loaded1.getJob()); - assertTasksEqual(task2, loaded2.getJob()); + assertJobsEqual(taskStatus1, loaded1); + assertJobsEqual(taskStatus2, loaded2); assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus1)); assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus2)); - // Check that the loaded task has the correct runtimes. - compareTimestampsSubjectToIoLatency("Early run-times not the same after read.", - taskStatus1.getEarliestRunTime(), loaded1.getEarliestRunTime()); - compareTimestampsSubjectToIoLatency("Late run-times not the same after read.", - taskStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed()); - compareTimestampsSubjectToIoLatency("Early run-times not the same after read.", - taskStatus2.getEarliestRunTime(), loaded2.getEarliestRunTime()); - compareTimestampsSubjectToIoLatency("Late run-times not the same after read.", - taskStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed()); } @Test @@ -240,7 +219,7 @@ public class JobStoreTest { mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size()); JobStatus loaded = jobStatusSet.getAllJobs().iterator().next(); - assertTasksEqual(task, loaded.getJob()); + assertJobsEqual(taskStatus, loaded); } @Test @@ -544,71 +523,30 @@ public class JobStoreTest { final JobSet jobStatusSet = new JobSet(); mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); final JobStatus second = jobStatusSet.getAllJobs().iterator().next(); - assertTasksEqual(first.getJob(), second.getJob()); + assertJobsEqual(first, second); } /** - * Helper function to throw an error if the provided task and TaskStatus objects are not equal. + * Helper function to throw an error if the provided JobStatus objects are not equal. */ - private void assertTasksEqual(JobInfo first, JobInfo second) { - assertEquals("Different task ids.", first.getId(), second.getId()); - assertEquals("Different components.", first.getService(), second.getService()); - assertEquals("Different periodic status.", first.isPeriodic(), second.isPeriodic()); - assertEquals("Different period.", first.getIntervalMillis(), second.getIntervalMillis()); - assertEquals("Different inital backoff.", first.getInitialBackoffMillis(), - second.getInitialBackoffMillis()); - assertEquals("Different backoff policy.", first.getBackoffPolicy(), - second.getBackoffPolicy()); - - assertEquals("Invalid charging constraint.", first.isRequireCharging(), - second.isRequireCharging()); - assertEquals("Invalid battery not low constraint.", first.isRequireBatteryNotLow(), - second.isRequireBatteryNotLow()); - assertEquals("Invalid idle constraint.", first.isRequireDeviceIdle(), - second.isRequireDeviceIdle()); - assertEquals("Invalid network type.", - first.getNetworkType(), second.getNetworkType()); - assertEquals("Invalid network.", - first.getRequiredNetwork(), second.getRequiredNetwork()); - assertEquals("Download bytes don't match", - first.getEstimatedNetworkDownloadBytes(), - second.getEstimatedNetworkDownloadBytes()); - assertEquals("Upload bytes don't match", - first.getEstimatedNetworkUploadBytes(), - second.getEstimatedNetworkUploadBytes()); - assertEquals("Minimum chunk bytes don't match", - first.getMinimumNetworkChunkBytes(), - second.getMinimumNetworkChunkBytes()); - assertEquals("Invalid deadline constraint.", - first.hasLateConstraint(), - second.hasLateConstraint()); - assertEquals("Invalid delay constraint.", - first.hasEarlyConstraint(), - second.hasEarlyConstraint()); - assertEquals("Extras don't match", - first.getExtras().toString(), second.getExtras().toString()); - assertEquals("Transient xtras don't match", - first.getTransientExtras().toString(), second.getTransientExtras().toString()); - - // Since people can forget to add tests here for new fields, do one last - // validity check based on bits-on-wire equality. - final byte[] firstBytes = marshall(first); - final byte[] secondBytes = marshall(second); - if (!Arrays.equals(firstBytes, secondBytes)) { - Log.w(TAG, "First: " + HexDump.dumpHexString(firstBytes)); - Log.w(TAG, "Second: " + HexDump.dumpHexString(secondBytes)); - fail("Raw JobInfo aren't equal; see logs for details"); - } - } + private void assertJobsEqual(JobStatus expected, JobStatus actual) { + assertEquals(expected.getJob(), actual.getJob()); - private static byte[] marshall(Parcelable p) { - final Parcel parcel = Parcel.obtain(); - try { - p.writeToParcel(parcel, 0); - return parcel.marshall(); - } finally { - parcel.recycle(); - } + // Source UID isn't persisted, but the rest of the app info is. + assertEquals("Source package not equal", + expected.getSourcePackageName(), actual.getSourcePackageName()); + assertEquals("Source user not equal", expected.getSourceUserId(), actual.getSourceUserId()); + assertEquals("Calling UID not equal", expected.getUid(), actual.getUid()); + assertEquals("Calling user not equal", expected.getUserId(), actual.getUserId()); + + assertEquals("Internal flags not equal", + expected.getInternalFlags(), actual.getInternalFlags()); + + // Check that the loaded task has the correct runtimes. + compareTimestampsSubjectToIoLatency("Early run-times not the same after read.", + expected.getEarliestRunTime(), actual.getEarliestRunTime()); + compareTimestampsSubjectToIoLatency("Late run-times not the same after read.", + expected.getLatestRunTimeElapsed(), actual.getLatestRunTimeElapsed()); } /** @@ -623,5 +561,4 @@ public class JobStoreTest { } private static class StubClass {} - } diff --git a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java index 83139b02430a..5a482fc37998 100644 --- a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java @@ -44,6 +44,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.om.IOverlayManager; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; @@ -669,7 +670,10 @@ public class StatusBarManagerServiceTest { } @Test - public void testSetNavBarMode_setsModeKids() throws RemoteException { + public void testSetNavBarMode_setsModeKids() throws Exception { + mContext.setMockPackageManager(mPackageManager); + when(mPackageManager.getPackageInfo(anyString(), + any(PackageManager.PackageInfoFlags.class))).thenReturn(new PackageInfo()); int navBarModeKids = StatusBarManager.NAV_BAR_MODE_KIDS; mStatusBarManagerService.setNavBarMode(navBarModeKids); diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java index fed8b4040aba..bcdc65c19330 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java @@ -21,12 +21,14 @@ import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.time.TimeZoneCapabilitiesAndConfig; import android.app.time.TimeZoneConfiguration; +import android.app.time.TimeZoneDetectorStatus; import android.app.time.TimeZoneState; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.util.IndentingPrintWriter; import java.util.ArrayList; +import java.util.Objects; public class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { @@ -34,14 +36,17 @@ public class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { new FakeServiceConfigAccessor(); private final ArrayList<StateChangeListener> mListeners = new ArrayList<>(); private TimeZoneState mTimeZoneState; + private TimeZoneDetectorStatus mStatus; public FakeTimeZoneDetectorStrategy() { mFakeServiceConfigAccessor.addConfigurationInternalChangeListener( this::notifyChangeListeners); } - public void initializeConfiguration(ConfigurationInternal configuration) { + public void initializeConfigurationAndStatus( + ConfigurationInternal configuration, TimeZoneDetectorStatus status) { mFakeServiceConfigAccessor.initializeCurrentUserConfiguration(configuration); + mStatus = Objects.requireNonNull(status); } @Override @@ -57,6 +62,7 @@ public class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { assertEquals("Multi-user testing not supported", configurationInternal.getUserId(), userId); return new TimeZoneCapabilitiesAndConfig( + mStatus, configurationInternal.asCapabilities(bypassUserPolicyChecks), configurationInternal.asConfiguration()); } @@ -90,7 +96,7 @@ public class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { } @Override - public void suggestGeolocationTimeZone(GeolocationTimeZoneSuggestion timeZoneSuggestion) { + public void handleLocationAlgorithmEvent(LocationAlgorithmEvent locationAlgorithmEvent) { } @Override diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/GeolocationTimeZoneSuggestionTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/GeolocationTimeZoneSuggestionTest.java index 0f667b3a690b..602842addff2 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/GeolocationTimeZoneSuggestionTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/GeolocationTimeZoneSuggestionTest.java @@ -16,13 +16,8 @@ package com.android.server.timezonedetector; -import static com.android.server.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNull; - -import android.os.ShellCommand; import org.junit.Test; @@ -49,11 +44,6 @@ public class GeolocationTimeZoneSuggestionTest { assertEquals(certain1v1, certain1v2); assertEquals(certain1v2, certain1v1); - // DebugInfo must not be considered in equals(). - certain1v1.addDebugInfo("Debug info 1"); - certain1v2.addDebugInfo("Debug info 2"); - assertEquals(certain1v1, certain1v2); - long time2 = 2222L; GeolocationTimeZoneSuggestion certain2 = GeolocationTimeZoneSuggestion.createCertainSuggestion(time2, ARBITRARY_ZONE_IDS1); @@ -71,40 +61,4 @@ public class GeolocationTimeZoneSuggestionTest { assertNotEquals(certain1v1, certain3); assertNotEquals(certain3, certain1v1); } - - @Test(expected = IllegalArgumentException.class) - public void testParseCommandLineArg_noZoneIdsArg() { - ShellCommand testShellCommand = - createShellCommandWithArgsAndOptions(Collections.emptyList()); - GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand); - } - - @Test - public void testParseCommandLineArg_zoneIdsUncertain() { - ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( - "--zone_ids UNCERTAIN"); - assertNull(GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand) - .getZoneIds()); - } - - @Test - public void testParseCommandLineArg_zoneIdsEmpty() { - ShellCommand testShellCommand = createShellCommandWithArgsAndOptions("--zone_ids EMPTY"); - assertEquals(Collections.emptyList(), - GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand).getZoneIds()); - } - - @Test - public void testParseCommandLineArg_zoneIdsPresent() { - ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( - "--zone_ids Europe/London,Europe/Paris"); - assertEquals(Arrays.asList("Europe/London", "Europe/Paris"), - GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand).getZoneIds()); - } - - @Test(expected = IllegalArgumentException.class) - public void testParseCommandLineArg_unknownArgument() { - ShellCommand testShellCommand = createShellCommandWithArgsAndOptions("--bad_arg 0"); - GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand); - } } diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/LocationAlgorithmEventTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/LocationAlgorithmEventTest.java new file mode 100644 index 000000000000..4c14014405f4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/LocationAlgorithmEventTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 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.timezonedetector; + +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_NOT_APPLICABLE; +import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK; +import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK; + +import static com.android.server.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import android.app.time.LocationTimeZoneAlgorithmStatus; +import android.os.ShellCommand; +import android.service.timezone.TimeZoneProviderStatus; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +public class LocationAlgorithmEventTest { + + public static final TimeZoneProviderStatus ARBITRARY_PROVIDER_STATUS = + new TimeZoneProviderStatus.Builder() + .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK) + .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_NOT_APPLICABLE) + .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK) + .build(); + + public static final LocationTimeZoneAlgorithmStatus ARBITRARY_LOCATION_ALGORITHM_STATUS = + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_STATUS, + PROVIDER_STATUS_NOT_PRESENT, null); + + @Test + public void testEquals() { + GeolocationTimeZoneSuggestion suggestion1 = + GeolocationTimeZoneSuggestion.createUncertainSuggestion(1111L); + LocationTimeZoneAlgorithmStatus status1 = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, PROVIDER_STATUS_NOT_PRESENT, null); + LocationAlgorithmEvent event1v1 = new LocationAlgorithmEvent(status1, suggestion1); + assertEqualsAndHashCode(event1v1, event1v1); + + LocationAlgorithmEvent event1v2 = new LocationAlgorithmEvent(status1, suggestion1); + assertEqualsAndHashCode(event1v1, event1v2); + + GeolocationTimeZoneSuggestion suggestion2 = + GeolocationTimeZoneSuggestion.createUncertainSuggestion(2222L); + LocationAlgorithmEvent event2 = new LocationAlgorithmEvent(status1, suggestion2); + assertNotEquals(event1v1, event2); + + LocationTimeZoneAlgorithmStatus status2 = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_NOT_PRESENT, null, PROVIDER_STATUS_NOT_READY, null); + LocationAlgorithmEvent event3 = new LocationAlgorithmEvent(status2, suggestion1); + assertNotEquals(event1v1, event3); + + // DebugInfo must not be considered in equals(). + event1v1.addDebugInfo("Debug info 1"); + event1v2.addDebugInfo("Debug info 2"); + assertEquals(event1v1, event1v2); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseCommandLineArg_noStatus() { + GeolocationTimeZoneSuggestion suggestion = + GeolocationTimeZoneSuggestion.createUncertainSuggestion(1111L); + ShellCommand testShellCommand = + createShellCommandWithArgsAndOptions( + Arrays.asList("--suggestion", suggestion.toString())); + + LocationAlgorithmEvent.parseCommandLineArg(testShellCommand); + } + + @Test + public void testParseCommandLineArg_noSuggestion() { + GeolocationTimeZoneSuggestion suggestion = null; + LocationAlgorithmEvent event = new LocationAlgorithmEvent( + ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion); + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( + Arrays.asList("--status", event.getAlgorithmStatus().toString())); + + assertEquals(event, LocationAlgorithmEvent.parseCommandLineArg(testShellCommand)); + } + + @Test + public void testParseCommandLineArg_suggestionUncertain() { + GeolocationTimeZoneSuggestion suggestion = + GeolocationTimeZoneSuggestion.createUncertainSuggestion(1111L); + LocationAlgorithmEvent event = new LocationAlgorithmEvent( + ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion); + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( + Arrays.asList("--status", event.getAlgorithmStatus().toString(), + "--suggestion", "UNCERTAIN")); + + LocationAlgorithmEvent parsedEvent = + LocationAlgorithmEvent.parseCommandLineArg(testShellCommand); + assertEquals(event.getAlgorithmStatus(), parsedEvent.getAlgorithmStatus()); + assertEquals(event.getSuggestion().getZoneIds(), parsedEvent.getSuggestion().getZoneIds()); + } + + @Test + public void testParseCommandLineArg_suggestionEmpty() { + GeolocationTimeZoneSuggestion suggestion = + GeolocationTimeZoneSuggestion.createCertainSuggestion( + 1111L, Collections.emptyList()); + LocationAlgorithmEvent event = new LocationAlgorithmEvent( + ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion); + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( + Arrays.asList("--status", event.getAlgorithmStatus().toString(), + "--suggestion", "EMPTY")); + + LocationAlgorithmEvent parsedEvent = + LocationAlgorithmEvent.parseCommandLineArg(testShellCommand); + assertEquals(event.getAlgorithmStatus(), parsedEvent.getAlgorithmStatus()); + assertEquals(event.getSuggestion().getZoneIds(), parsedEvent.getSuggestion().getZoneIds()); + } + + @Test + public void testParseCommandLineArg_suggestionPresent() { + GeolocationTimeZoneSuggestion suggestion = + GeolocationTimeZoneSuggestion.createCertainSuggestion( + 1111L, Arrays.asList("Europe/London", "Europe/Paris")); + LocationAlgorithmEvent event = new LocationAlgorithmEvent( + ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion); + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( + Arrays.asList("--status", event.getAlgorithmStatus().toString(), + "--suggestion", "Europe/London,Europe/Paris")); + + LocationAlgorithmEvent parsedEvent = + LocationAlgorithmEvent.parseCommandLineArg(testShellCommand); + assertEquals(event.getAlgorithmStatus(), parsedEvent.getAlgorithmStatus()); + assertEquals(event.getSuggestion().getZoneIds(), parsedEvent.getSuggestion().getZoneIds()); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseCommandLineArg_unknownArgument() { + GeolocationTimeZoneSuggestion suggestion = + GeolocationTimeZoneSuggestion.createCertainSuggestion( + 1111L, Arrays.asList("Europe/London", "Europe/Paris")); + LocationAlgorithmEvent event = new LocationAlgorithmEvent( + ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion); + ShellCommand testShellCommand = createShellCommandWithArgsAndOptions( + Arrays.asList("--status", event.getAlgorithmStatus().toString(), + "--suggestion", "Europe/London,Europe/Paris", "--bad_arg")); + LocationAlgorithmEvent.parseCommandLineArg(testShellCommand); + } + + private static void assertEqualsAndHashCode(Object one, Object two) { + assertEquals(one, two); + assertEquals(two, one); + assertEquals(one.hashCode(), two.hashCode()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/MetricsTimeZoneDetectorStateTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/MetricsTimeZoneDetectorStateTest.java index 223c53233065..ea801e887c4c 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/MetricsTimeZoneDetectorStateTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/MetricsTimeZoneDetectorStateTest.java @@ -16,6 +16,10 @@ package com.android.server.timezonedetector; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT; + import static com.android.server.timezonedetector.MetricsTimeZoneDetectorState.DETECTION_MODE_GEO; import static org.junit.Assert.assertEquals; @@ -23,6 +27,7 @@ import static org.junit.Assert.assertNull; import android.annotation.ElapsedRealtimeLong; import android.annotation.UserIdInt; +import android.app.time.LocationTimeZoneAlgorithmStatus; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; @@ -31,6 +36,7 @@ import com.android.server.timezonedetector.MetricsTimeZoneDetectorState.MetricsT import org.junit.Test; import java.util.Arrays; +import java.util.List; import java.util.function.Function; /** Tests for {@link MetricsTimeZoneDetectorState}. */ @@ -38,6 +44,9 @@ public class MetricsTimeZoneDetectorStateTest { private static final @UserIdInt int ARBITRARY_USER_ID = 1; private static final @ElapsedRealtimeLong long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234L; + private static final LocationTimeZoneAlgorithmStatus ARBITRARY_CERTAIN_STATUS = + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, null, PROVIDER_STATUS_NOT_PRESENT, null); private static final String DEVICE_TIME_ZONE_ID = "DeviceTimeZoneId"; private static final ManualTimeZoneSuggestion MANUAL_TIME_ZONE_SUGGESTION = @@ -50,11 +59,14 @@ public class MetricsTimeZoneDetectorStateTest { .setQuality(TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE) .build(); - private static final GeolocationTimeZoneSuggestion GEOLOCATION_TIME_ZONE_SUGGESTION = + public static final GeolocationTimeZoneSuggestion GEOLOCATION_SUGGESTION_CERTAIN = GeolocationTimeZoneSuggestion.createCertainSuggestion( ARBITRARY_ELAPSED_REALTIME_MILLIS, Arrays.asList("GeoTimeZoneId1", "GeoTimeZoneId2")); + private static final LocationAlgorithmEvent LOCATION_ALGORITHM_EVENT = + new LocationAlgorithmEvent(ARBITRARY_CERTAIN_STATUS, GEOLOCATION_SUGGESTION_CERTAIN); + private final OrdinalGenerator<String> mOrdinalGenerator = new OrdinalGenerator<>(Function.identity()); @@ -68,7 +80,7 @@ public class MetricsTimeZoneDetectorStateTest { MetricsTimeZoneDetectorState metricsTimeZoneDetectorState = MetricsTimeZoneDetectorState.create(mOrdinalGenerator, configurationInternal, DEVICE_TIME_ZONE_ID, MANUAL_TIME_ZONE_SUGGESTION, - TELEPHONY_TIME_ZONE_SUGGESTION, GEOLOCATION_TIME_ZONE_SUGGESTION); + TELEPHONY_TIME_ZONE_SUGGESTION, LOCATION_ALGORITHM_EVENT); // Assert the content. assertCommonConfiguration(configurationInternal, metricsTimeZoneDetectorState); @@ -88,9 +100,10 @@ public class MetricsTimeZoneDetectorStateTest { assertEquals(expectedTelephonySuggestion, metricsTimeZoneDetectorState.getLatestTelephonySuggestion()); + List<String> expectedZoneIds = LOCATION_ALGORITHM_EVENT.getSuggestion().getZoneIds(); MetricsTimeZoneSuggestion expectedGeoSuggestion = MetricsTimeZoneSuggestion.createCertain( - GEOLOCATION_TIME_ZONE_SUGGESTION.getZoneIds().toArray(new String[0]), + expectedZoneIds.toArray(new String[0]), new int[] { 3, 4 }); assertEquals(expectedGeoSuggestion, metricsTimeZoneDetectorState.getLatestGeolocationSuggestion()); @@ -106,7 +119,7 @@ public class MetricsTimeZoneDetectorStateTest { MetricsTimeZoneDetectorState metricsTimeZoneDetectorState = MetricsTimeZoneDetectorState.create(mOrdinalGenerator, configurationInternal, DEVICE_TIME_ZONE_ID, MANUAL_TIME_ZONE_SUGGESTION, - TELEPHONY_TIME_ZONE_SUGGESTION, GEOLOCATION_TIME_ZONE_SUGGESTION); + TELEPHONY_TIME_ZONE_SUGGESTION, LOCATION_ALGORITHM_EVENT); // Assert the content. assertCommonConfiguration(configurationInternal, metricsTimeZoneDetectorState); diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java index 8909832391a4..a02c8ca001ce 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java @@ -16,14 +16,22 @@ package com.android.server.timezonedetector; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.app.time.LocationTimeZoneAlgorithmStatus; +import android.app.time.TelephonyTimeZoneAlgorithmStatus; import android.app.time.TimeZoneCapabilitiesAndConfig; import android.app.time.TimeZoneConfiguration; +import android.app.time.TimeZoneDetectorStatus; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.content.Context; import android.os.HandlerThread; @@ -41,6 +49,15 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public class TimeZoneDetectorInternalImplTest { + private static final TelephonyTimeZoneAlgorithmStatus ARBITRARY_TELEPHONY_STATUS = + new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING); + private static final LocationTimeZoneAlgorithmStatus ARBITRARY_LOCATION_CERTAIN_STATUS = + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, null, PROVIDER_STATUS_NOT_PRESENT, null); + private static final TimeZoneDetectorStatus ARBITRARY_DETECTOR_STATUS = + new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING, ARBITRARY_TELEPHONY_STATUS, + ARBITRARY_LOCATION_CERTAIN_STATUS); + private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234L; private static final String ARBITRARY_ZONE_ID = "TestZoneId"; private static final List<String> ARBITRARY_ZONE_IDS = Arrays.asList(ARBITRARY_ZONE_ID); @@ -81,7 +98,8 @@ public class TimeZoneDetectorInternalImplTest { public void testGetCapabilitiesAndConfigForDpm() throws Exception { final boolean autoDetectionEnabled = true; ConfigurationInternal testConfig = createConfigurationInternal(autoDetectionEnabled); - mFakeTimeZoneDetectorStrategySpy.initializeConfiguration(testConfig); + TimeZoneDetectorStatus testStatus = ARBITRARY_DETECTOR_STATUS; + mFakeTimeZoneDetectorStrategySpy.initializeConfigurationAndStatus(testConfig, testStatus); TimeZoneCapabilitiesAndConfig actualCapabilitiesAndConfig = mTimeZoneDetectorInternal.getCapabilitiesAndConfigForDpm(); @@ -93,6 +111,7 @@ public class TimeZoneDetectorInternalImplTest { TimeZoneCapabilitiesAndConfig expectedCapabilitiesAndConfig = new TimeZoneCapabilitiesAndConfig( + testStatus, testConfig.asCapabilities(expectedBypassUserPolicyChecks), testConfig.asConfiguration()); assertEquals(expectedCapabilitiesAndConfig, actualCapabilitiesAndConfig); @@ -103,7 +122,9 @@ public class TimeZoneDetectorInternalImplTest { final boolean autoDetectionEnabled = false; ConfigurationInternal initialConfigurationInternal = createConfigurationInternal(autoDetectionEnabled); - mFakeTimeZoneDetectorStrategySpy.initializeConfiguration(initialConfigurationInternal); + TimeZoneDetectorStatus testStatus = ARBITRARY_DETECTOR_STATUS; + mFakeTimeZoneDetectorStrategySpy.initializeConfigurationAndStatus( + initialConfigurationInternal, testStatus); TimeZoneConfiguration timeConfiguration = new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(true) @@ -131,13 +152,15 @@ public class TimeZoneDetectorInternalImplTest { } @Test - public void testSuggestGeolocationTimeZone() throws Exception { + public void testHandleLocationAlgorithmEvent() throws Exception { GeolocationTimeZoneSuggestion timeZoneSuggestion = createGeolocationTimeZoneSuggestion(); - mTimeZoneDetectorInternal.suggestGeolocationTimeZone(timeZoneSuggestion); + LocationAlgorithmEvent suggestionEvent = new LocationAlgorithmEvent( + ARBITRARY_LOCATION_CERTAIN_STATUS, timeZoneSuggestion); + mTimeZoneDetectorInternal.handleLocationAlgorithmEvent(suggestionEvent); mTestHandler.assertTotalMessagesEnqueued(1); mTestHandler.waitForMessagesToBeProcessed(); - verify(mFakeTimeZoneDetectorStrategySpy).suggestGeolocationTimeZone(timeZoneSuggestion); + verify(mFakeTimeZoneDetectorStrategySpy).handleLocationAlgorithmEvent(suggestionEvent); } private static ManualTimeZoneSuggestion createManualTimeZoneSuggestion() { return new ManualTimeZoneSuggestion(ARBITRARY_ZONE_ID); diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java index d8346ee4355b..d9d8053e6220 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java @@ -16,6 +16,11 @@ package com.android.server.timezonedetector; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -34,8 +39,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.app.time.ITimeZoneDetectorListener; +import android.app.time.LocationTimeZoneAlgorithmStatus; +import android.app.time.TelephonyTimeZoneAlgorithmStatus; import android.app.time.TimeZoneCapabilitiesAndConfig; import android.app.time.TimeZoneConfiguration; +import android.app.time.TimeZoneDetectorStatus; import android.app.time.TimeZoneState; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; @@ -59,6 +67,13 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public class TimeZoneDetectorServiceTest { + private static final LocationTimeZoneAlgorithmStatus ARBITRARY_LOCATION_CERTAIN_STATUS = + new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING, + PROVIDER_STATUS_IS_CERTAIN, null, PROVIDER_STATUS_NOT_PRESENT, null); + private static final TimeZoneDetectorStatus ARBITRARY_DETECTOR_STATUS = + new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING, + new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING), + ARBITRARY_LOCATION_CERTAIN_STATUS); private static final int ARBITRARY_USER_ID = 9999; private static final List<String> ARBITRARY_TIME_ZONE_IDS = Arrays.asList("TestZoneId"); private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234L; @@ -113,7 +128,8 @@ public class TimeZoneDetectorServiceTest { ConfigurationInternal configuration = createConfigurationInternal(true /* autoDetectionEnabled*/); - mFakeTimeZoneDetectorStrategySpy.initializeConfiguration(configuration); + mFakeTimeZoneDetectorStrategySpy.initializeConfigurationAndStatus(configuration, + ARBITRARY_DETECTOR_STATUS); TimeZoneCapabilitiesAndConfig actualCapabilitiesAndConfig = mTimeZoneDetectorService.getCapabilitiesAndConfig(); @@ -128,6 +144,7 @@ public class TimeZoneDetectorServiceTest { TimeZoneCapabilitiesAndConfig expectedCapabilitiesAndConfig = new TimeZoneCapabilitiesAndConfig( + ARBITRARY_DETECTOR_STATUS, configuration.asCapabilities(expectedBypassUserPolicyChecks), configuration.asConfiguration()); assertEquals(expectedCapabilitiesAndConfig, actualCapabilitiesAndConfig); @@ -161,7 +178,9 @@ public class TimeZoneDetectorServiceTest { public void testListenerRegistrationAndCallbacks() throws Exception { ConfigurationInternal initialConfiguration = createConfigurationInternal(false /* autoDetectionEnabled */); - mFakeTimeZoneDetectorStrategySpy.initializeConfiguration(initialConfiguration); + + mFakeTimeZoneDetectorStrategySpy.initializeConfigurationAndStatus( + initialConfiguration, ARBITRARY_DETECTOR_STATUS); IBinder mockListenerBinder = mock(IBinder.class); ITimeZoneDetectorListener mockListener = mock(ITimeZoneDetectorListener.class); @@ -231,31 +250,35 @@ public class TimeZoneDetectorServiceTest { } @Test - public void testSuggestGeolocationTimeZone_withoutPermission() { + public void testHandleLocationAlgorithmEvent_withoutPermission() { doThrow(new SecurityException("Mock")) .when(mMockContext).enforceCallingPermission(anyString(), any()); GeolocationTimeZoneSuggestion timeZoneSuggestion = createGeolocationTimeZoneSuggestion(); + LocationAlgorithmEvent event = new LocationAlgorithmEvent( + ARBITRARY_LOCATION_CERTAIN_STATUS, timeZoneSuggestion); assertThrows(SecurityException.class, - () -> mTimeZoneDetectorService.suggestGeolocationTimeZone(timeZoneSuggestion)); + () -> mTimeZoneDetectorService.handleLocationAlgorithmEvent(event)); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.SET_TIME_ZONE), anyString()); } @Test - public void testSuggestGeolocationTimeZone() throws Exception { + public void testHandleLocationAlgorithmEvent() throws Exception { doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); GeolocationTimeZoneSuggestion timeZoneSuggestion = createGeolocationTimeZoneSuggestion(); + LocationAlgorithmEvent event = new LocationAlgorithmEvent( + ARBITRARY_LOCATION_CERTAIN_STATUS, timeZoneSuggestion); - mTimeZoneDetectorService.suggestGeolocationTimeZone(timeZoneSuggestion); + mTimeZoneDetectorService.handleLocationAlgorithmEvent(event); mTestHandler.assertTotalMessagesEnqueued(1); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.SET_TIME_ZONE), anyString()); mTestHandler.waitForMessagesToBeProcessed(); - verify(mFakeTimeZoneDetectorStrategySpy).suggestGeolocationTimeZone(timeZoneSuggestion); + verify(mFakeTimeZoneDetectorStrategySpy).handleLocationAlgorithmEvent(event); } @Test diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java index f50e7fbc76bb..b991c5a30415 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java @@ -16,6 +16,12 @@ package com.android.server.timezonedetector; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_UNCERTAIN; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT; +import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY; import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID; import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET; import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY; @@ -35,6 +41,7 @@ import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.T import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -47,8 +54,11 @@ import static org.mockito.Mockito.verify; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.UserIdInt; +import android.app.time.LocationTimeZoneAlgorithmStatus; +import android.app.time.TelephonyTimeZoneAlgorithmStatus; import android.app.time.TimeZoneCapabilitiesAndConfig; import android.app.time.TimeZoneConfiguration; +import android.app.time.TimeZoneDetectorStatus; import android.app.time.TimeZoneState; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; @@ -189,6 +199,9 @@ public class TimeZoneDetectorStrategyImplTest { .setGeoDetectionEnabledSetting(true) .build(); + private static final TelephonyTimeZoneAlgorithmStatus TELEPHONY_ALGORITHM_RUNNING_STATUS = + new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING); + private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy; private FakeEnvironment mFakeEnvironment; private HandlerThread mHandlerThread; @@ -233,9 +246,7 @@ public class TimeZoneDetectorStrategyImplTest { { mFakeServiceConfigAccessorSpy.simulateCurrentUserConfigurationInternalChange( CONFIG_AUTO_DISABLED_GEO_DISABLED); - mTestHandler.waitForMessagesToBeProcessed(); - - stateChangeListener.assertNotificationsReceived(0); + assertStateChangeNotificationsSent(stateChangeListener, 0); assertEquals(CONFIG_AUTO_DISABLED_GEO_DISABLED, mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests()); } @@ -244,10 +255,7 @@ public class TimeZoneDetectorStrategyImplTest { { mFakeServiceConfigAccessorSpy.simulateCurrentUserConfigurationInternalChange( CONFIG_AUTO_ENABLED_GEO_ENABLED); - mTestHandler.waitForMessagesToBeProcessed(); - - stateChangeListener.assertNotificationsReceived(1); - stateChangeListener.resetNotificationsReceivedCount(); + assertStateChangeNotificationsSent(stateChangeListener, 1); assertEquals(CONFIG_AUTO_ENABLED_GEO_ENABLED, mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests()); } @@ -258,10 +266,7 @@ public class TimeZoneDetectorStrategyImplTest { new TimeZoneConfiguration.Builder().setGeoDetectionEnabled(false).build(); mTimeZoneDetectorStrategy.updateConfiguration( USER_ID, requestedChanges, bypassUserPolicyChecks); - mTestHandler.waitForMessagesToBeProcessed(); - - stateChangeListener.assertNotificationsReceived(1); - stateChangeListener.resetNotificationsReceivedCount(); + assertStateChangeNotificationsSent(stateChangeListener, 1); } } @@ -290,11 +295,9 @@ public class TimeZoneDetectorStrategyImplTest { new TimeZoneConfiguration.Builder().setGeoDetectionEnabled(false).build(); mTimeZoneDetectorStrategy.updateConfiguration( otherUserId, requestedChanges, bypassUserPolicyChecks); - mTestHandler.waitForMessagesToBeProcessed(); // Only changes to the current user's config are notified. - stateChangeListener.assertNotificationsReceived(0); - stateChangeListener.resetNotificationsReceivedCount(); + assertStateChangeNotificationsSent(stateChangeListener, 0); } // Current user behavior: the strategy caches and returns the latest configuration. @@ -426,9 +429,9 @@ public class TimeZoneDetectorStrategyImplTest { QualifiedTelephonyTimeZoneSuggestion expectedSlotIndex1ScoredSuggestion = new QualifiedTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion, TELEPHONY_SCORE_NONE); - assertEquals(expectedSlotIndex1ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); - assertNull(mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion) + .verifyLatestQualifiedTelephonySuggestionReceived(SLOT_INDEX2, null); assertEquals(expectedSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); @@ -439,10 +442,10 @@ public class TimeZoneDetectorStrategyImplTest { QualifiedTelephonyTimeZoneSuggestion expectedSlotIndex2ScoredSuggestion = new QualifiedTelephonyTimeZoneSuggestion(slotIndex2TimeZoneSuggestion, TELEPHONY_SCORE_NONE); - assertEquals(expectedSlotIndex1ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); - assertEquals(expectedSlotIndex2ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion) + .verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX2, expectedSlotIndex2ScoredSuggestion); // SlotIndex1 should always beat slotIndex2, all other things being equal. assertEquals(expectedSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); @@ -477,8 +480,8 @@ public class TimeZoneDetectorStrategyImplTest { QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion = new QualifiedTelephonyTimeZoneSuggestion( lowQualitySuggestion, testCase.expectedScore); - assertEquals(expectedScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); } @@ -494,8 +497,8 @@ public class TimeZoneDetectorStrategyImplTest { QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion = new QualifiedTelephonyTimeZoneSuggestion( goodQualitySuggestion, testCase2.expectedScore); - assertEquals(expectedScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); } @@ -511,8 +514,8 @@ public class TimeZoneDetectorStrategyImplTest { QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion = new QualifiedTelephonyTimeZoneSuggestion( lowQualitySuggestion2, testCase.expectedScore); - assertEquals(expectedScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); } @@ -543,8 +546,8 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion = new QualifiedTelephonyTimeZoneSuggestion(suggestion, testCase.expectedScore); - assertEquals(expectedScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); @@ -560,8 +563,8 @@ public class TimeZoneDetectorStrategyImplTest { } // Assert internal service state. - assertEquals(expectedScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); @@ -570,8 +573,8 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged(); // Assert internal service state. - assertEquals(expectedScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); } @@ -622,8 +625,8 @@ public class TimeZoneDetectorStrategyImplTest { } // Assert internal service state. - assertEquals(expectedZoneSlotIndex1ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion); assertEquals(expectedZoneSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); } @@ -677,10 +680,10 @@ public class TimeZoneDetectorStrategyImplTest { } // Assert internal service state. - assertEquals(expectedZoneSlotIndex1ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); - assertEquals(expectedEmptySlotIndex2ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion) + .verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion); assertEquals(expectedZoneSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); @@ -690,10 +693,10 @@ public class TimeZoneDetectorStrategyImplTest { script.verifyTimeZoneNotChanged(); // Assert internal service state. - assertEquals(expectedZoneSlotIndex1ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); - assertEquals(expectedZoneSlotIndex2ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion) + .verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX2, expectedZoneSlotIndex2ScoredSuggestion); // SlotIndex1 should always beat slotIndex2, all other things being equal. assertEquals(expectedZoneSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); @@ -709,20 +712,20 @@ public class TimeZoneDetectorStrategyImplTest { } // Assert internal service state. - assertEquals(expectedEmptySlotIndex1ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); - assertEquals(expectedZoneSlotIndex2ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedEmptySlotIndex1ScoredSuggestion) + .verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX2, expectedZoneSlotIndex2ScoredSuggestion); assertEquals(expectedZoneSlotIndex2ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); // Reset the state for the next loop. script.simulateTelephonyTimeZoneSuggestion(emptySlotIndex2Suggestion) .verifyTimeZoneNotChanged(); - assertEquals(expectedEmptySlotIndex1ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1)); - assertEquals(expectedEmptySlotIndex2ScoredSuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2)); + script.verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX1, expectedEmptySlotIndex1ScoredSuggestion) + .verifyLatestQualifiedTelephonySuggestionReceived( + SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion); } } @@ -866,53 +869,185 @@ public class TimeZoneDetectorStrategyImplTest { } @Test - public void testGeoSuggestion_uncertain() { + public void testLocationAlgorithmEvent_statusChangesOnly() { + TestStateChangeListener stateChangeListener = new TestStateChangeListener(); Script script = new Script() .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED) - .resetConfigurationTracking(); + .resetConfigurationTracking() + .registerStateChangeListener(stateChangeListener); + + TimeZoneDetectorStatus expectedInitialDetectorStatus = new TimeZoneDetectorStatus( + DETECTOR_STATUS_RUNNING, + TELEPHONY_ALGORITHM_RUNNING_STATUS, + LocationTimeZoneAlgorithmStatus.UNKNOWN); + script.verifyCachedDetectorStatus(expectedInitialDetectorStatus); + + LocationTimeZoneAlgorithmStatus algorithmStatus1 = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, PROVIDER_STATUS_NOT_READY, null, + PROVIDER_STATUS_NOT_PRESENT, null); + LocationTimeZoneAlgorithmStatus algorithmStatus2 = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, PROVIDER_STATUS_NOT_PRESENT, null, + PROVIDER_STATUS_NOT_PRESENT, null); + assertNotEquals(algorithmStatus1, algorithmStatus2); + + { + LocationAlgorithmEvent locationAlgorithmEvent = + new LocationAlgorithmEvent(algorithmStatus1, null); + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneNotChanged(); - GeolocationTimeZoneSuggestion uncertainSuggestion = createUncertainGeolocationSuggestion(); + assertStateChangeNotificationsSent(stateChangeListener, 1); - script.simulateGeolocationTimeZoneSuggestion(uncertainSuggestion) + // Assert internal service state. + TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus( + DETECTOR_STATUS_RUNNING, + TELEPHONY_ALGORITHM_RUNNING_STATUS, + algorithmStatus1); + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + // Repeat the event to demonstrate the state change notifier is not triggered. + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneNotChanged(); + + assertStateChangeNotificationsSent(stateChangeListener, 0); + + // Assert internal service state. + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + } + + { + LocationAlgorithmEvent locationAlgorithmEvent = + new LocationAlgorithmEvent(algorithmStatus2, null); + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneNotChanged(); + + assertStateChangeNotificationsSent(stateChangeListener, 1); + + // Assert internal service state. + TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus( + DETECTOR_STATUS_RUNNING, + TELEPHONY_ALGORITHM_RUNNING_STATUS, + algorithmStatus2); + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + // Repeat the event to demonstrate the state change notifier is not triggered. + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneNotChanged(); + + assertStateChangeNotificationsSent(stateChangeListener, 0); + + // Assert internal service state. + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + } + } + + @Test + public void testLocationAlgorithmEvent_uncertain() { + TestStateChangeListener stateChangeListener = new TestStateChangeListener(); + Script script = new Script() + .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) + .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED) + .resetConfigurationTracking() + .registerStateChangeListener(stateChangeListener); + + LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent(); + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged(); + assertStateChangeNotificationsSent(stateChangeListener, 1); + // Assert internal service state. - assertEquals(uncertainSuggestion, - mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus( + DETECTOR_STATUS_RUNNING, + TELEPHONY_ALGORITHM_RUNNING_STATUS, + locationAlgorithmEvent.getAlgorithmStatus()); + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + // Repeat the event to demonstrate the state change notifier is not triggered. + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneNotChanged(); + + // Detector remains running and location algorithm is still uncertain so nothing to report. + assertStateChangeNotificationsSent(stateChangeListener, 0); + + // Assert internal service state. + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); } @Test - public void testGeoSuggestion_noZones() { + public void testLocationAlgorithmEvent_noZones() { + TestStateChangeListener stateChangeListener = new TestStateChangeListener(); Script script = new Script() .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED) - .resetConfigurationTracking(); + .resetConfigurationTracking() + .registerStateChangeListener(stateChangeListener); + + LocationAlgorithmEvent locationAlgorithmEvent = createCertainLocationAlgorithmEvent(); + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneNotChanged(); - GeolocationTimeZoneSuggestion noZonesSuggestion = createCertainGeolocationSuggestion(); + assertStateChangeNotificationsSent(stateChangeListener, 1); - script.simulateGeolocationTimeZoneSuggestion(noZonesSuggestion) + // Assert internal service state. + TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus( + DETECTOR_STATUS_RUNNING, + TELEPHONY_ALGORITHM_RUNNING_STATUS, + locationAlgorithmEvent.getAlgorithmStatus()); + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + // Repeat the event to demonstrate the state change notifier is not triggered. + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged(); + assertStateChangeNotificationsSent(stateChangeListener, 0); + // Assert internal service state. - assertEquals(noZonesSuggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); } @Test - public void testGeoSuggestion_oneZone() { - GeolocationTimeZoneSuggestion suggestion = - createCertainGeolocationSuggestion("Europe/London"); - + public void testLocationAlgorithmEvent_oneZone() { + TestStateChangeListener stateChangeListener = new TestStateChangeListener(); Script script = new Script() .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED) - .resetConfigurationTracking(); + .resetConfigurationTracking() + .registerStateChangeListener(stateChangeListener); + + LocationAlgorithmEvent locationAlgorithmEvent = + createCertainLocationAlgorithmEvent("Europe/London"); + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneChangedAndReset(locationAlgorithmEvent); + + assertStateChangeNotificationsSent(stateChangeListener, 1); + + // Assert internal service state. + TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus( + DETECTOR_STATUS_RUNNING, + TELEPHONY_ALGORITHM_RUNNING_STATUS, + locationAlgorithmEvent.getAlgorithmStatus()); + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + // Repeat the event to demonstrate the state change notifier is not triggered. + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneNotChanged(); - script.simulateGeolocationTimeZoneSuggestion(suggestion) - .verifyTimeZoneChangedAndReset(suggestion); + assertStateChangeNotificationsSent(stateChangeListener, 0); // Assert internal service state. - assertEquals(suggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + script.verifyCachedDetectorStatus(expectedDetectorStatus) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); } /** @@ -921,41 +1056,35 @@ public class TimeZoneDetectorStrategyImplTest { * set to until that unambiguously can't be correct. */ @Test - public void testGeoSuggestion_multiZone() { - GeolocationTimeZoneSuggestion londonOnlySuggestion = - createCertainGeolocationSuggestion("Europe/London"); - GeolocationTimeZoneSuggestion londonOrParisSuggestion = - createCertainGeolocationSuggestion("Europe/Paris", "Europe/London"); - GeolocationTimeZoneSuggestion parisOnlySuggestion = - createCertainGeolocationSuggestion("Europe/Paris"); - + public void testLocationAlgorithmEvent_multiZone() { Script script = new Script() .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED) .resetConfigurationTracking(); - script.simulateGeolocationTimeZoneSuggestion(londonOnlySuggestion) - .verifyTimeZoneChangedAndReset(londonOnlySuggestion); - assertEquals(londonOnlySuggestion, - mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + LocationAlgorithmEvent londonOnlyEvent = + createCertainLocationAlgorithmEvent("Europe/London"); + script.simulateLocationAlgorithmEvent(londonOnlyEvent) + .verifyTimeZoneChangedAndReset(londonOnlyEvent) + .verifyLatestLocationAlgorithmEventReceived(londonOnlyEvent); // Confirm bias towards the current device zone when there's multiple zones to choose from. - script.simulateGeolocationTimeZoneSuggestion(londonOrParisSuggestion) - .verifyTimeZoneNotChanged(); - assertEquals(londonOrParisSuggestion, - mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + LocationAlgorithmEvent londonOrParisEvent = + createCertainLocationAlgorithmEvent("Europe/Paris", "Europe/London"); + script.simulateLocationAlgorithmEvent(londonOrParisEvent) + .verifyTimeZoneNotChanged() + .verifyLatestLocationAlgorithmEventReceived(londonOrParisEvent); - script.simulateGeolocationTimeZoneSuggestion(parisOnlySuggestion) - .verifyTimeZoneChangedAndReset(parisOnlySuggestion); - assertEquals(parisOnlySuggestion, - mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + LocationAlgorithmEvent parisOnlyEvent = createCertainLocationAlgorithmEvent("Europe/Paris"); + script.simulateLocationAlgorithmEvent(parisOnlyEvent) + .verifyTimeZoneChangedAndReset(parisOnlyEvent) + .verifyLatestLocationAlgorithmEventReceived(parisOnlyEvent); // Now the suggestion that previously left the device on Europe/London will leave the device // on Europe/Paris. - script.simulateGeolocationTimeZoneSuggestion(londonOrParisSuggestion) - .verifyTimeZoneNotChanged(); - assertEquals(londonOrParisSuggestion, - mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + script.simulateLocationAlgorithmEvent(londonOrParisEvent) + .verifyTimeZoneNotChanged() + .verifyLatestLocationAlgorithmEventReceived(londonOrParisEvent); } /** @@ -964,8 +1093,9 @@ public class TimeZoneDetectorStrategyImplTest { */ @Test public void testChangingGeoDetectionEnabled() { - GeolocationTimeZoneSuggestion geolocationSuggestion = - createCertainGeolocationSuggestion("Europe/London"); + TestStateChangeListener stateChangeListener = new TestStateChangeListener(); + LocationAlgorithmEvent locationAlgorithmEvent = + createCertainLocationAlgorithmEvent("Europe/London"); TelephonyTimeZoneSuggestion telephonySuggestion = createTelephonySuggestion( SLOT_INDEX1, MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE, "Europe/Paris"); @@ -973,20 +1103,22 @@ public class TimeZoneDetectorStrategyImplTest { Script script = new Script() .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED_GEO_DISABLED) - .resetConfigurationTracking(); + .resetConfigurationTracking() + .registerStateChangeListener(stateChangeListener); // Add suggestions. Nothing should happen as time zone detection is disabled. - script.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion) - .verifyTimeZoneNotChanged(); + script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneNotChanged() + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); - assertEquals(geolocationSuggestion, - mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + // A detector status change is considered a "state change". + assertStateChangeNotificationsSent(stateChangeListener, 1); script.simulateTelephonyTimeZoneSuggestion(telephonySuggestion) - .verifyTimeZoneNotChanged(); + .verifyTimeZoneNotChanged() + .verifyLatestTelephonySuggestionReceived(SLOT_INDEX1, telephonySuggestion); - assertEquals(telephonySuggestion, - mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1).suggestion); + assertStateChangeNotificationsSent(stateChangeListener, 0); // Toggling the time zone detection enabled setting on should cause the device setting to be // set from the telephony signal, as we've started with geolocation time zone detection @@ -994,18 +1126,25 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateSetAutoMode(true) .verifyTimeZoneChangedAndReset(telephonySuggestion); + // A configuration change is considered a "state change". + assertStateChangeNotificationsSent(stateChangeListener, 1); + // Changing the detection to enable geo detection will cause the device tz setting to // change to use the latest geolocation suggestion. script.simulateSetGeoDetectionEnabled(true) - .verifyTimeZoneChangedAndReset(geolocationSuggestion); + .verifyTimeZoneChangedAndReset(locationAlgorithmEvent); + + // A configuration change is considered a "state change". + assertStateChangeNotificationsSent(stateChangeListener, 1); // Changing the detection to disable geo detection should cause the device tz setting to // change to the telephony suggestion. script.simulateSetGeoDetectionEnabled(false) - .verifyTimeZoneChangedAndReset(telephonySuggestion); + .verifyTimeZoneChangedAndReset(telephonySuggestion) + .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); - assertEquals(geolocationSuggestion, - mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + // A configuration change is considered a "state change". + assertStateChangeNotificationsSent(stateChangeListener, 1); } @Test @@ -1039,21 +1178,20 @@ public class TimeZoneDetectorStrategyImplTest { // Receiving an "uncertain" geolocation suggestion should have no effect. { - GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion = - createUncertainGeolocationSuggestion(); + LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent(); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion) + .simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); } // Receiving a "certain" geolocation suggestion should disable telephony fallback mode. { - GeolocationTimeZoneSuggestion geolocationSuggestion = - createCertainGeolocationSuggestion("Europe/London"); + LocationAlgorithmEvent locationAlgorithmEvent = + createCertainLocationAlgorithmEvent("Europe/London"); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion) - .verifyTimeZoneChangedAndReset(geolocationSuggestion) + .simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); } @@ -1076,22 +1214,22 @@ public class TimeZoneDetectorStrategyImplTest { // Geolocation suggestions should continue to be used as normal (previous telephony // suggestions are not used, even when the geolocation suggestion is uncertain). { - GeolocationTimeZoneSuggestion geolocationSuggestion = - createCertainGeolocationSuggestion("Europe/Rome"); + LocationAlgorithmEvent certainLocationAlgorithmEvent = + createCertainLocationAlgorithmEvent("Europe/Rome"); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion) - .verifyTimeZoneChangedAndReset(geolocationSuggestion) + .simulateLocationAlgorithmEvent(certainLocationAlgorithmEvent) + .verifyTimeZoneChangedAndReset(certainLocationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); - GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion = - createUncertainGeolocationSuggestion(); + LocationAlgorithmEvent uncertainLocationAlgorithmEvent = + createUncertainLocationAlgorithmEvent(); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion) + .simulateLocationAlgorithmEvent(uncertainLocationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion) + .simulateLocationAlgorithmEvent(certainLocationAlgorithmEvent) // No change needed, device will already be set to Europe/Rome. .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); @@ -1108,21 +1246,20 @@ public class TimeZoneDetectorStrategyImplTest { // Make the geolocation algorithm uncertain. { - GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion = - createUncertainGeolocationSuggestion(); + LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent(); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion) + .simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); } // Make the geolocation algorithm certain, disabling telephony fallback. { - GeolocationTimeZoneSuggestion geolocationSuggestion = - createCertainGeolocationSuggestion("Europe/Lisbon"); + LocationAlgorithmEvent locationAlgorithmEvent = + createCertainLocationAlgorithmEvent("Europe/Lisbon"); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion) - .verifyTimeZoneChangedAndReset(geolocationSuggestion) + .simulateLocationAlgorithmEvent(locationAlgorithmEvent) + .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); } @@ -1130,10 +1267,9 @@ public class TimeZoneDetectorStrategyImplTest { // Demonstrate what happens when geolocation is uncertain when telephony fallback is // enabled. { - GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion = - createUncertainGeolocationSuggestion(); + LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent(); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion) + .simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false) .simulateEnableTelephonyFallback() @@ -1161,10 +1297,9 @@ public class TimeZoneDetectorStrategyImplTest { // Receiving an "uncertain" geolocation suggestion should have no effect. { - GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion = - createUncertainGeolocationSuggestion(); + LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent(); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion) + .simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); } @@ -1172,10 +1307,9 @@ public class TimeZoneDetectorStrategyImplTest { // Make an uncertain geolocation suggestion, there is no telephony suggestion to fall back // to { - GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion = - createUncertainGeolocationSuggestion(); + LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent(); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion) + .simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); } @@ -1185,17 +1319,16 @@ public class TimeZoneDetectorStrategyImplTest { // Geolocation suggestions should continue to be used as normal (previous telephony // suggestions are not used, even when the geolocation suggestion is uncertain). { - GeolocationTimeZoneSuggestion geolocationSuggestion = - createCertainGeolocationSuggestion("Europe/Rome"); + LocationAlgorithmEvent certainEvent = + createCertainLocationAlgorithmEvent("Europe/Rome"); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion) - .verifyTimeZoneChangedAndReset(geolocationSuggestion) + .simulateLocationAlgorithmEvent(certainEvent) + .verifyTimeZoneChangedAndReset(certainEvent) .verifyTelephonyFallbackIsEnabled(false); - GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion = - createUncertainGeolocationSuggestion(); + LocationAlgorithmEvent uncertainEvent = createUncertainLocationAlgorithmEvent(); script.simulateIncrementClock() - .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion) + .simulateLocationAlgorithmEvent(uncertainEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); @@ -1319,15 +1452,15 @@ public class TimeZoneDetectorStrategyImplTest { TelephonyTimeZoneSuggestion telephonySuggestion = createTelephonySuggestion(0 /* slotIndex */, MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_SINGLE_ZONE, "Zone2"); - GeolocationTimeZoneSuggestion geolocationTimeZoneSuggestion = - createCertainGeolocationSuggestion("Zone3", "Zone2"); + LocationAlgorithmEvent locationAlgorithmEvent = + createCertainLocationAlgorithmEvent("Zone3", "Zone2"); script.simulateTelephonyTimeZoneSuggestion(telephonySuggestion) .verifyTimeZoneNotChanged() - .simulateGeolocationTimeZoneSuggestion(geolocationTimeZoneSuggestion) + .simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged(); assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId, - manualSuggestion, telephonySuggestion, geolocationTimeZoneSuggestion, + manualSuggestion, telephonySuggestion, locationAlgorithmEvent, MetricsTimeZoneDetectorState.DETECTION_MODE_MANUAL); // Update the config and confirm that the config metrics state updates also. @@ -1336,11 +1469,11 @@ public class TimeZoneDetectorStrategyImplTest { .setGeoDetectionEnabledSetting(true) .build(); - expectedDeviceTimeZoneId = geolocationTimeZoneSuggestion.getZoneIds().get(0); + expectedDeviceTimeZoneId = locationAlgorithmEvent.getSuggestion().getZoneIds().get(0); script.simulateConfigurationInternalChange(expectedInternalConfig) .verifyTimeZoneChangedAndReset(expectedDeviceTimeZoneId, TIME_ZONE_CONFIDENCE_HIGH); assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId, - manualSuggestion, telephonySuggestion, geolocationTimeZoneSuggestion, + manualSuggestion, telephonySuggestion, locationAlgorithmEvent, MetricsTimeZoneDetectorState.DETECTION_MODE_GEO); } @@ -1352,7 +1485,7 @@ public class TimeZoneDetectorStrategyImplTest { ConfigurationInternal expectedInternalConfig, String expectedDeviceTimeZoneId, ManualTimeZoneSuggestion expectedManualSuggestion, TelephonyTimeZoneSuggestion expectedTelephonySuggestion, - GeolocationTimeZoneSuggestion expectedGeolocationTimeZoneSuggestion, + LocationAlgorithmEvent expectedLocationAlgorithmEvent, int expectedDetectionMode) { MetricsTimeZoneDetectorState actualState = mTimeZoneDetectorStrategy.generateMetricsState(); @@ -1365,7 +1498,7 @@ public class TimeZoneDetectorStrategyImplTest { MetricsTimeZoneDetectorState.create( tzIdOrdinalGenerator, expectedInternalConfig, expectedDeviceTimeZoneId, expectedManualSuggestion, expectedTelephonySuggestion, - expectedGeolocationTimeZoneSuggestion); + expectedLocationAlgorithmEvent); // Rely on MetricsTimeZoneDetectorState.equals() for time zone ID / ID ordinal comparisons. assertEquals(expectedState, actualState); } @@ -1405,20 +1538,37 @@ public class TimeZoneDetectorStrategyImplTest { return new TelephonyTimeZoneSuggestion.Builder(SLOT_INDEX2).build(); } + private LocationAlgorithmEvent createCertainLocationAlgorithmEvent(@NonNull String... zoneIds) { + GeolocationTimeZoneSuggestion suggestion = createCertainGeolocationSuggestion(zoneIds); + LocationTimeZoneAlgorithmStatus algorithmStatus = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, PROVIDER_STATUS_IS_CERTAIN, null, + PROVIDER_STATUS_NOT_PRESENT, null); + LocationAlgorithmEvent event = new LocationAlgorithmEvent(algorithmStatus, suggestion); + event.addDebugInfo("Test certain event"); + return event; + } + + private LocationAlgorithmEvent createUncertainLocationAlgorithmEvent() { + GeolocationTimeZoneSuggestion suggestion = createUncertainGeolocationSuggestion(); + LocationTimeZoneAlgorithmStatus algorithmStatus = new LocationTimeZoneAlgorithmStatus( + DETECTION_ALGORITHM_STATUS_RUNNING, PROVIDER_STATUS_IS_UNCERTAIN, null, + PROVIDER_STATUS_NOT_PRESENT, null); + LocationAlgorithmEvent event = new LocationAlgorithmEvent(algorithmStatus, suggestion); + event.addDebugInfo("Test uncertain event"); + return event; + } + private GeolocationTimeZoneSuggestion createUncertainGeolocationSuggestion() { - return GeolocationTimeZoneSuggestion.createCertainSuggestion( - mFakeEnvironment.elapsedRealtimeMillis(), null); + return GeolocationTimeZoneSuggestion.createUncertainSuggestion( + mFakeEnvironment.elapsedRealtimeMillis()); } private GeolocationTimeZoneSuggestion createCertainGeolocationSuggestion( @NonNull String... zoneIds) { assertNotNull(zoneIds); - GeolocationTimeZoneSuggestion suggestion = - GeolocationTimeZoneSuggestion.createCertainSuggestion( - mFakeEnvironment.elapsedRealtimeMillis(), Arrays.asList(zoneIds)); - suggestion.addDebugInfo("Test suggestion"); - return suggestion; + return GeolocationTimeZoneSuggestion.createCertainSuggestion( + mFakeEnvironment.elapsedRealtimeMillis(), Arrays.asList(zoneIds)); } static class FakeEnvironment implements TimeZoneDetectorStrategyImpl.Environment { @@ -1499,6 +1649,14 @@ public class TimeZoneDetectorStrategyImplTest { } } + private void assertStateChangeNotificationsSent( + TestStateChangeListener stateChangeListener, int expectedCount) { + // State change notifications are asynchronous, so we have to wait. + mTestHandler.waitForMessagesToBeProcessed(); + + stateChangeListener.assertNotificationsReceivedAndReset(expectedCount); + } + /** * A "fluent" class allows reuse of code in tests: initialization, simulation and verification * logic. @@ -1516,6 +1674,11 @@ public class TimeZoneDetectorStrategyImplTest { return this; } + Script registerStateChangeListener(StateChangeListener stateChangeListener) { + mTimeZoneDetectorStrategy.addChangeListener(stateChangeListener); + return this; + } + Script simulateIncrementClock() { mFakeEnvironment.incrementClock(); return this; @@ -1555,11 +1718,10 @@ public class TimeZoneDetectorStrategyImplTest { } /** - * Simulates the time zone detection strategy receiving a geolocation-originated - * suggestion. + * Simulates the time zone detection strategy receiving a location algorithm event. */ - Script simulateGeolocationTimeZoneSuggestion(GeolocationTimeZoneSuggestion suggestion) { - mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(suggestion); + Script simulateLocationAlgorithmEvent(LocationAlgorithmEvent event) { + mTimeZoneDetectorStrategy.handleLocationAlgorithmEvent(event); return this; } @@ -1616,7 +1778,9 @@ public class TimeZoneDetectorStrategyImplTest { return this; } - Script verifyTimeZoneChangedAndReset(GeolocationTimeZoneSuggestion suggestion) { + Script verifyTimeZoneChangedAndReset(LocationAlgorithmEvent event) { + GeolocationTimeZoneSuggestion suggestion = event.getSuggestion(); + assertNotNull("Only events with suggestions can change the time zone", suggestion); assertEquals("Only use this method with unambiguous geo suggestions", 1, suggestion.getZoneIds().size()); verifyTimeZoneChangedAndReset( @@ -1631,6 +1795,32 @@ public class TimeZoneDetectorStrategyImplTest { return this; } + Script verifyCachedDetectorStatus(TimeZoneDetectorStatus expectedStatus) { + assertEquals(expectedStatus, + mTimeZoneDetectorStrategy.getCachedDetectorStatusForTests()); + return this; + } + + Script verifyLatestLocationAlgorithmEventReceived(LocationAlgorithmEvent expectedEvent) { + assertEquals(expectedEvent, + mTimeZoneDetectorStrategy.getLatestLocationAlgorithmEvent()); + return this; + } + + Script verifyLatestTelephonySuggestionReceived(int slotIndex, + TelephonyTimeZoneSuggestion expectedSuggestion) { + assertEquals(expectedSuggestion, + mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(slotIndex).suggestion); + return this; + } + + Script verifyLatestQualifiedTelephonySuggestionReceived(int slotIndex, + QualifiedTelephonyTimeZoneSuggestion expectedQualifiedSuggestion) { + assertEquals(expectedQualifiedSuggestion, + mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(slotIndex)); + return this; + } + Script resetConfigurationTracking() { mFakeEnvironment.commitAllChanges(); return this; @@ -1671,11 +1861,16 @@ public class TimeZoneDetectorStrategyImplTest { mNotificationsReceived++; } - public void resetNotificationsReceivedCount() { + public void assertNotificationsReceivedAndReset(int expectedCount) { + assertNotificationsReceived(expectedCount); + resetNotificationsReceivedCount(); + } + + private void resetNotificationsReceivedCount() { mNotificationsReceived = 0; } - public void assertNotificationsReceived(int expectedCount) { + private void assertNotificationsReceived(int expectedCount) { assertEquals(expectedCount, mNotificationsReceived); } } diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java index c18acd20e96a..7b1db953ef54 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java @@ -15,6 +15,8 @@ */ package com.android.server.timezonedetector.location; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING; +import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_NOT_APPLICABLE; import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK; import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE; @@ -42,6 +44,7 @@ import static com.android.server.timezonedetector.location.TestSupport.USER2_CON import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -51,6 +54,7 @@ import static java.util.Arrays.asList; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.service.timezone.TimeZoneProviderEvent; @@ -60,6 +64,7 @@ import android.util.IndentingPrintWriter; import com.android.server.timezonedetector.ConfigurationInternal; import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion; +import com.android.server.timezonedetector.LocationAlgorithmEvent; import com.android.server.timezonedetector.TestState; import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderMetricsLogger; import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.ProviderStateEnum; @@ -141,7 +146,7 @@ public class LocationTimeZoneProviderControllerTest { mTestPrimaryLocationTimeZoneProvider.setFailDuringInitialization(true); // Initialize. After initialization the providers must be initialized and one should be - // started. + // started. They should report their status change via the callback. controller.initialize(testEnvironment, mTestCallback); mTestPrimaryLocationTimeZoneProvider.assertInitialized(); @@ -154,7 +159,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertInitializationTimeoutSet(expectedInitTimeout); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -184,7 +190,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -211,7 +218,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING, STATE_FAILED); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -239,7 +247,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -262,7 +271,8 @@ public class LocationTimeZoneProviderControllerTest { mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_PROVIDERS_INITIALIZING, STATE_STOPPED); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -282,7 +292,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate time passing with no provider event being received from the primary. @@ -296,7 +307,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Simulate time passing with no provider event being received from either the primary or @@ -311,7 +322,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Finally, the uncertainty timeout should cause the controller to make an uncertain @@ -324,7 +335,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(STATE_UNCERTAIN); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); + mTestCallback.assertEventWithUncertainSuggestionReportedAndCommit(); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -345,7 +356,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate a location event being received from the primary provider. This should cause a @@ -358,7 +370,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -380,7 +392,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate time passing with no provider event being received from the primary. @@ -392,7 +405,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Simulate a location event being received from the primary provider. This should cause a @@ -405,7 +418,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -427,7 +440,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate time passing with no provider event being received from the primary. @@ -439,7 +453,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Simulate a location event being received from the secondary provider. This should cause a @@ -453,7 +467,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -475,7 +489,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate a location event being received from the primary provider. This should cause a @@ -488,7 +503,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -501,7 +516,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertFalse(controller.isUncertaintyTimeoutSet()); // And a third, different event should cause another suggestion. @@ -513,7 +528,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -535,7 +550,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate time passing with no provider event being received from the primary. @@ -547,7 +563,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Simulate a location event being received from the secondary provider. This should cause a @@ -561,7 +577,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -575,7 +591,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertFalse(controller.isUncertaintyTimeoutSet()); // And a third, different event should cause another suggestion. @@ -588,7 +604,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -610,7 +626,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate a location event being received from the primary provider. This should cause a @@ -623,7 +640,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -639,7 +656,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Simulate a location event being received from the secondary provider. This should cause a @@ -654,7 +671,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -670,7 +687,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Simulate time passing. This means the uncertainty timeout should fire and the uncertain @@ -683,7 +700,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(STATE_UNCERTAIN); - mTestCallback.assertUncertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithUncertainSuggestionReportedAndCommit( USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -705,7 +722,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate a location event being received from the primary provider. This should cause a @@ -718,7 +736,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -733,7 +751,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // And a success event from the primary provider should cause the controller to make another @@ -747,7 +765,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -767,7 +785,8 @@ public class LocationTimeZoneProviderControllerTest { mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_PROVIDERS_INITIALIZING, STATE_STOPPED); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Now signal a config change so that geo detection is enabled. @@ -778,7 +797,8 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Now signal a config change so that geo detection is disabled. @@ -788,7 +808,8 @@ public class LocationTimeZoneProviderControllerTest { mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -807,7 +828,8 @@ public class LocationTimeZoneProviderControllerTest { mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_PROVIDERS_INITIALIZING, STATE_STOPPED); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Now signal a config change so that geo detection is enabled. @@ -818,7 +840,8 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate a success event being received from the primary provider. @@ -830,7 +853,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -843,8 +866,9 @@ public class LocationTimeZoneProviderControllerTest { assertControllerState(controller, STATE_STOPPED); mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestMetricsLogger.assertStateChangesAndCommit(STATE_UNCERTAIN, STATE_STOPPED); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); + mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -865,7 +889,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate the primary provider suggesting a time zone. @@ -879,7 +904,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -897,9 +922,9 @@ public class LocationTimeZoneProviderControllerTest { mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfig( PROVIDER_STATE_STARTED_INITIALIZING, USER2_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); - mTestMetricsLogger.assertStateChangesAndCommit( - STATE_UNCERTAIN, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); + mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED, STATE_INITIALIZING); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -920,7 +945,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate a failure location event being received from the primary provider. This should @@ -933,7 +959,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate uncertainty from the secondary. @@ -945,7 +971,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // And a success event from the secondary provider should cause the controller to make @@ -958,7 +984,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -971,7 +997,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); } @@ -992,7 +1018,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate a failure location event being received from the primary provider. This should @@ -1005,7 +1032,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertFalse(controller.isUncertaintyTimeoutSet()); // Now signal a config change so that geo detection is disabled. @@ -1015,7 +1042,8 @@ public class LocationTimeZoneProviderControllerTest { mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Now signal a config change so that geo detection is enabled. @@ -1026,7 +1054,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -1047,7 +1076,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate an uncertain event from the primary. This will start the secondary, which will @@ -1062,7 +1092,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Simulate failure event from the secondary. This should just affect the secondary's state. @@ -1074,7 +1104,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // And a success event from the primary provider should cause the controller to make @@ -1087,7 +1117,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -1100,7 +1130,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); } @@ -1121,7 +1151,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate an uncertain event from the primary. This will start the secondary, which will @@ -1136,7 +1167,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Simulate failure event from the secondary. This should just affect the secondary's state. @@ -1148,7 +1179,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertUncertaintyTimeoutSet(testEnvironment, controller); // Now signal a config change so that geo detection is disabled. @@ -1158,7 +1189,8 @@ public class LocationTimeZoneProviderControllerTest { mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Now signal a config change so that geo detection is enabled. Only the primary can be @@ -1170,7 +1202,8 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -1191,7 +1224,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate a failure event from the primary. This will start the secondary. @@ -1203,7 +1237,7 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit( PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestMetricsLogger.assertStateChangesAndCommit(); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertNoEventReported(); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate failure event from the secondary. @@ -1214,7 +1248,8 @@ public class LocationTimeZoneProviderControllerTest { mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_FAILED); - mTestCallback.assertUncertainSuggestionMadeAndCommit(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); } @@ -1233,7 +1268,7 @@ public class LocationTimeZoneProviderControllerTest { { LocationTimeZoneManagerServiceState state = controller.getStateForTests(); assertEquals(STATE_INITIALIZING, state.getControllerState()); - assertNull(state.getLastSuggestion()); + assertNull(state.getLastEvent().getSuggestion()); assertControllerRecordedStates(state, STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); assertProviderStates(state.getPrimaryProviderStates(), @@ -1251,7 +1286,7 @@ public class LocationTimeZoneProviderControllerTest { { LocationTimeZoneManagerServiceState state = controller.getStateForTests(); assertEquals(STATE_INITIALIZING, state.getControllerState()); - assertNull(state.getLastSuggestion()); + assertNull(state.getLastEvent().getSuggestion()); assertControllerRecordedStates(state); assertProviderStates( state.getPrimaryProviderStates(), PROVIDER_STATE_STARTED_UNCERTAIN); @@ -1268,7 +1303,7 @@ public class LocationTimeZoneProviderControllerTest { LocationTimeZoneManagerServiceState state = controller.getStateForTests(); assertEquals(STATE_CERTAIN, state.getControllerState()); assertEquals(USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds(), - state.getLastSuggestion().getZoneIds()); + state.getLastEvent().getSuggestion().getZoneIds()); assertControllerRecordedStates(state, STATE_CERTAIN); assertProviderStates(state.getPrimaryProviderStates()); assertProviderStates( @@ -1280,7 +1315,7 @@ public class LocationTimeZoneProviderControllerTest { LocationTimeZoneManagerServiceState state = controller.getStateForTests(); assertEquals(STATE_CERTAIN, state.getControllerState()); assertEquals(USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds(), - state.getLastSuggestion().getZoneIds()); + state.getLastEvent().getSuggestion().getZoneIds()); assertControllerRecordedStates(state); assertProviderStates(state.getPrimaryProviderStates()); assertProviderStates(state.getSecondaryProviderStates()); @@ -1313,7 +1348,8 @@ public class LocationTimeZoneProviderControllerTest { mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit( STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING); - mTestCallback.assertNoSuggestionMade(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_RUNNING); assertFalse(controller.isUncertaintyTimeoutSet()); // Simulate the primary provider suggesting a time zone. @@ -1327,7 +1363,7 @@ public class LocationTimeZoneProviderControllerTest { PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED); mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit(); mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN); - mTestCallback.assertCertainSuggestionMadeFromEventAndCommit( + mTestCallback.assertEventWithCertainSuggestionReportedAndCommit( USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1); assertFalse(controller.isUncertaintyTimeoutSet()); @@ -1335,11 +1371,11 @@ public class LocationTimeZoneProviderControllerTest { controller.destroy(); assertControllerState(controller, STATE_DESTROYED); - mTestMetricsLogger.assertStateChangesAndCommit( - STATE_UNCERTAIN, STATE_STOPPED, STATE_DESTROYED); + mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED, STATE_DESTROYED); // Confirm that the previous suggestion was overridden. - mTestCallback.assertUncertainSuggestionMadeAndCommit(); + mTestCallback.assertEventWithNoSuggestionReportedAndCommit( + DETECTION_ALGORITHM_STATUS_NOT_RUNNING); mTestPrimaryLocationTimeZoneProvider.assertStateChangesAndCommit( PROVIDER_STATE_STOPPED, PROVIDER_STATE_DESTROYED); @@ -1517,63 +1553,101 @@ public class LocationTimeZoneProviderControllerTest { private static class TestCallback extends LocationTimeZoneProviderController.Callback { - private TestState<GeolocationTimeZoneSuggestion> mLatestSuggestion = new TestState<>(); + private TestState<LocationAlgorithmEvent> mLatestEvent = new TestState<>(); TestCallback(ThreadingDomain threadingDomain) { super(threadingDomain); } @Override - void suggest(GeolocationTimeZoneSuggestion suggestion) { - mLatestSuggestion.set(suggestion); + void sendEvent(LocationAlgorithmEvent event) { + mLatestEvent.set(event); + } + + void assertNoEventReported() { + mLatestEvent.assertHasNotBeenSet(); + } + + /** + * Asserts one or more events have been reported, and the most recent does not contain a + * suggestion. + */ + void assertEventWithNoSuggestionReportedAndCommit( + @DetectionAlgorithmStatus int expectedAlgorithmStatus) { + mLatestEvent.assertHasBeenSet(); + + LocationAlgorithmEvent latest = mLatestEvent.getLatest(); + assertEquals(expectedAlgorithmStatus, latest.getAlgorithmStatus().getStatus()); + assertNull(latest.getSuggestion()); + mLatestEvent.commitLatest(); } - void assertCertainSuggestionMadeFromEventAndCommit(TimeZoneProviderEvent event) { + void assertEventWithCertainSuggestionReportedAndCommit(TimeZoneProviderEvent event) { // Test coding error if this fails. assertEquals(TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION, event.getType()); + // By definition, the algorithm has to be running to report a suggestion. + @DetectionAlgorithmStatus int expectedAlgorithmStatus = + DETECTION_ALGORITHM_STATUS_RUNNING; TimeZoneProviderSuggestion suggestion = event.getSuggestion(); - assertSuggestionMadeAndCommit( + assertEventWithSuggestionReportedAndCommit( + expectedAlgorithmStatus, suggestion.getElapsedRealtimeMillis(), suggestion.getTimeZoneIds()); } - void assertNoSuggestionMade() { - mLatestSuggestion.assertHasNotBeenSet(); - } - - /** Asserts that an uncertain suggestion has been made from the supplied event. */ - void assertUncertainSuggestionMadeFromEventAndCommit(TimeZoneProviderEvent event) { + /** + * Asserts that one or more events have been reported, and the most recent contains an + * uncertain suggestion matching select details from the supplied provider event. + */ + void assertEventWithUncertainSuggestionReportedAndCommit(TimeZoneProviderEvent event) { // Test coding error if this fails. assertEquals(TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN, event.getType()); - assertSuggestionMadeAndCommit(event.getCreationElapsedMillis(), null); + // By definition, the algorithm has to be running to report a suggestion. + @DetectionAlgorithmStatus int expectedAlgorithmStatus = + DETECTION_ALGORITHM_STATUS_RUNNING; + assertEventWithSuggestionReportedAndCommit( + expectedAlgorithmStatus, event.getCreationElapsedMillis(), null); } /** - * Asserts that an uncertain suggestion has been made. - * Ignores the suggestion's effectiveFromElapsedMillis. + * Asserts that one or more events have been reported, and the most recent contains an + * uncertain suggestion. Ignores the suggestion's effectiveFromElapsedMillis. */ - void assertUncertainSuggestionMadeAndCommit() { + void assertEventWithUncertainSuggestionReportedAndCommit() { + // By definition, the algorithm has to be running to report a suggestion. + @DetectionAlgorithmStatus int expectedAlgorithmStatus = + DETECTION_ALGORITHM_STATUS_RUNNING; + // An "uncertain" suggestion has null time zone IDs. - assertSuggestionMadeAndCommit(null, null); + assertEventWithSuggestionReportedAndCommit(expectedAlgorithmStatus, null, null); } /** - * Asserts that a suggestion has been made and some properties of that suggestion. - * When expectedEffectiveFromElapsedMillis is null then its value isn't checked. + * Asserts that an event has been reported containing a suggestion and some properties of + * that suggestion. When expectedEffectiveFromElapsedMillis is null then its value isn't + * checked. */ - private void assertSuggestionMadeAndCommit( + private void assertEventWithSuggestionReportedAndCommit( + @DetectionAlgorithmStatus int expectedAlgorithmStatus, @Nullable @ElapsedRealtimeLong Long expectedEffectiveFromElapsedMillis, @Nullable List<String> expectedZoneIds) { - mLatestSuggestion.assertHasBeenSet(); + mLatestEvent.assertHasBeenSet(); + + LocationAlgorithmEvent latestEvent = mLatestEvent.getLatest(); + assertEquals(expectedAlgorithmStatus, latestEvent.getAlgorithmStatus().getStatus()); + + GeolocationTimeZoneSuggestion suggestion = latestEvent.getSuggestion(); + assertNotNull("Latest event doesn't contain a suggestion: event=" + latestEvent, + suggestion); + if (expectedEffectiveFromElapsedMillis != null) { - assertEquals( - expectedEffectiveFromElapsedMillis.longValue(), - mLatestSuggestion.getLatest().getEffectiveFromElapsedMillis()); + assertEquals(expectedEffectiveFromElapsedMillis.longValue(), + suggestion.getEffectiveFromElapsedMillis()); } - assertEquals(expectedZoneIds, mLatestSuggestion.getLatest().getZoneIds()); - mLatestSuggestion.commitLatest(); + assertEquals(expectedZoneIds, suggestion.getZoneIds()); + mLatestEvent.commitLatest(); } } @@ -1598,11 +1672,9 @@ public class LocationTimeZoneProviderControllerTest { } @Override - void onInitialize() { + boolean onInitialize() { mInitialized = true; - if (mFailDuringInitialization) { - throw new RuntimeException("Simulated initialization failure"); - } + return !mFailDuringInitialization; } @Override diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java index 2bee7e66c43f..1ae74c679b53 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java @@ -330,8 +330,9 @@ public class LocationTimeZoneProviderTest { } @Override - void onInitialize() { + boolean onInitialize() { mOnInitializeCalled = true; + return true; } @Override 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 d54d1fed1016..afec08505257 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -1101,6 +1101,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { new NotificationChannel("id", "name", IMPORTANCE_HIGH); mBinderService.updateNotificationChannelForPackage(PKG, mUid, updatedChannel); + // pretend only this following part is called by the app (system permissions are required to + // update the notification channel on behalf of the user above) + mService.isSystemUid = false; + // Recreating with a lower importance leaves channel unchanged. final NotificationChannel dupeChannel = new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_LOW); @@ -1126,6 +1130,46 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testCreateNotificationChannels_fromAppCannotSetFields() throws Exception { + // Confirm that when createNotificationChannels is called from the relevant app and not + // system, then it cannot set fields that can't be set by apps + mService.isSystemUid = false; + + final NotificationChannel channel = + new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + channel.setBypassDnd(true); + channel.setAllowBubbles(true); + + mBinderService.createNotificationChannels(PKG, + new ParceledListSlice(Arrays.asList(channel))); + + final NotificationChannel createdChannel = + mBinderService.getNotificationChannel(PKG, mContext.getUserId(), PKG, "id"); + assertFalse(createdChannel.canBypassDnd()); + assertFalse(createdChannel.canBubble()); + } + + @Test + public void testCreateNotificationChannels_fromSystemCanSetFields() throws Exception { + // Confirm that when createNotificationChannels is called from system, + // then it can set fields that can't be set by apps + mService.isSystemUid = true; + + final NotificationChannel channel = + new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + channel.setBypassDnd(true); + channel.setAllowBubbles(true); + + mBinderService.createNotificationChannels(PKG, + new ParceledListSlice(Arrays.asList(channel))); + + final NotificationChannel createdChannel = + mBinderService.getNotificationChannel(PKG, mContext.getUserId(), PKG, "id"); + assertTrue(createdChannel.canBypassDnd()); + assertTrue(createdChannel.canBubble()); + } + + @Test public void testBlockedNotifications_suspended() throws Exception { when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(true); @@ -3088,6 +3132,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testDeleteChannelGroupChecksForFgses() throws Exception { + // the setup for this test requires it to seem like it's coming from the app + mService.isSystemUid = false; when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) .thenReturn(singletonList(mock(AssociationInfo.class))); CountDownLatch latch = new CountDownLatch(2); @@ -3100,7 +3146,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { ParceledListSlice<NotificationChannel> pls = new ParceledListSlice(ImmutableList.of(notificationChannel)); try { - mBinderService.createNotificationChannelsForPackage(PKG, mUid, pls); + mBinderService.createNotificationChannels(PKG, pls); } catch (RemoteException e) { throw new RuntimeException(e); } @@ -3119,8 +3165,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { ParceledListSlice<NotificationChannel> pls = new ParceledListSlice(ImmutableList.of(notificationChannel)); try { - mBinderService.createNotificationChannelsForPackage(PKG, mUid, pls); - mBinderService.deleteNotificationChannelGroup(PKG, "group"); + // Because existing channels won't have their groups overwritten when the call + // is from the app, this call won't take the channel out of the group + mBinderService.createNotificationChannels(PKG, pls); + mBinderService.deleteNotificationChannelGroup(PKG, "group"); } catch (RemoteException e) { throw new RuntimeException(e); } @@ -8681,7 +8729,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertEquals("friend", friendChannel.getConversationId()); assertEquals(null, original.getConversationId()); assertEquals(original.canShowBadge(), friendChannel.canShowBadge()); - assertFalse(friendChannel.canBubble()); // can't be modified by app + assertEquals(original.canBubble(), friendChannel.canBubble()); // called by system assertFalse(original.getId().equals(friendChannel.getId())); assertNotNull(friendChannel.getId()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index bd8da4e713b7..079897bc099f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2806,7 +2806,7 @@ public class ActivityRecordTests extends WindowTestsBase { final Task task = activity.getTask(); final ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task).build(); topActivity.setVisible(false); - task.positionChildAt(topActivity, POSITION_TOP); + task.positionChildAt(POSITION_TOP, topActivity, false /* includeParents */); activity.addStartingWindow(mPackageName, android.R.style.Theme, null, true, true, false, true, false, false, false); waitUntilHandlersIdle(); diff --git a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java index 13ebc932fcef..0568b3893aa2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java @@ -25,6 +25,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -36,6 +37,8 @@ import android.view.DisplayInfo; import android.view.Surface; import android.view.SurfaceControl; +import com.android.server.wm.RefreshRatePolicy.FrameRateVote; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,12 +53,18 @@ import org.junit.runner.RunWith; @Presubmit @RunWith(WindowTestRunner.class) public class FrameRateSelectionPriorityTests extends WindowTestsBase { - private static final float FLOAT_TOLERANCE = 0.01f; private static final int LOW_MODE_ID = 3; private DisplayPolicy mDisplayPolicy = mock(DisplayPolicy.class); private RefreshRatePolicy mRefreshRatePolicy; private HighRefreshRateDenylist mDenylist = mock(HighRefreshRateDenylist.class); + private FrameRateVote mTempFrameRateVote = new FrameRateVote(); + + private static final FrameRateVote FRAME_RATE_VOTE_NONE = new FrameRateVote(); + private static final FrameRateVote FRAME_RATE_VOTE_60_EXACT = + new FrameRateVote(60, Surface.FRAME_RATE_COMPATIBILITY_EXACT); + private static final FrameRateVote FRAME_RATE_VOTE_60_PREFERRED = + new FrameRateVote(60, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT); WindowState createWindow(String name) { WindowState window = createWindow(null, TYPE_APPLICATION, name); @@ -85,12 +94,12 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { assertNotNull("Window state is created", appWindow); assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); appWindow.updateFrameRateSelectionPriorityIfNeeded(); // Priority doesn't change. assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); // Call the function a few times. appWindow.updateFrameRateSelectionPriorityIfNeeded(); @@ -109,16 +118,15 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); assertEquals(appWindow.getDisplayContent().getDisplayPolicy().getRefreshRatePolicy() .getPreferredModeId(appWindow), 0); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); - assertEquals(appWindow.getDisplayContent().getDisplayPolicy().getRefreshRatePolicy() - .getPreferredRefreshRate(appWindow), 0, FLOAT_TOLERANCE); - - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); + assertFalse(appWindow.getDisplayContent().getDisplayPolicy().getRefreshRatePolicy() + .updateFrameRateVote(appWindow)); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); appWindow.updateFrameRateSelectionPriorityIfNeeded(); // Priority stays MAX_VALUE. assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); verify(appWindow.getPendingTransaction(), never()).setFrameRateSelectionPriority( appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET); @@ -127,7 +135,7 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { appWindow.updateFrameRateSelectionPriorityIfNeeded(); // Priority changes to 1. assertEquals(appWindow.mFrameRateSelectionPriority, 1); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority( appWindow.getSurfaceControl(), 1); verify(appWindow.getPendingTransaction(), never()).setFrameRate( @@ -138,27 +146,27 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { public void testApplicationInFocusWithModeId() { final WindowState appWindow = createWindow("appWindow"); assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); // Application is in focus. appWindow.mToken.mDisplayContent.mCurrentFocus = appWindow; appWindow.updateFrameRateSelectionPriorityIfNeeded(); // Priority changes. assertEquals(appWindow.mFrameRateSelectionPriority, 1); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); // Update the mode ID to a requested number. appWindow.mAttrs.preferredDisplayModeId = 1; appWindow.updateFrameRateSelectionPriorityIfNeeded(); // Priority changes. assertEquals(appWindow.mFrameRateSelectionPriority, 0); - assertEquals(appWindow.mAppPreferredFrameRate, 60, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_60_EXACT); // Remove the mode ID request. appWindow.mAttrs.preferredDisplayModeId = 0; appWindow.updateFrameRateSelectionPriorityIfNeeded(); // Priority changes. assertEquals(appWindow.mFrameRateSelectionPriority, 1); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); // Verify we called actions on Transactions correctly. verify(appWindow.getPendingTransaction(), never()).setFrameRateSelectionPriority( @@ -175,7 +183,7 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { public void testApplicationNotInFocusWithModeId() { final WindowState appWindow = createWindow("appWindow"); assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); final WindowState inFocusWindow = createWindow("inFocus"); appWindow.mToken.mDisplayContent.mCurrentFocus = inFocusWindow; @@ -183,14 +191,14 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { appWindow.updateFrameRateSelectionPriorityIfNeeded(); // The window is not in focus. assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); // Update the mode ID to a requested number. appWindow.mAttrs.preferredDisplayModeId = 1; appWindow.updateFrameRateSelectionPriorityIfNeeded(); // Priority changes. assertEquals(appWindow.mFrameRateSelectionPriority, 2); - assertEquals(appWindow.mAppPreferredFrameRate, 60, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_60_EXACT); verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority( appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET); @@ -204,7 +212,7 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { public void testApplicationNotInFocusWithoutModeId() { final WindowState appWindow = createWindow("appWindow"); assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); final WindowState inFocusWindow = createWindow("inFocus"); appWindow.mToken.mDisplayContent.mCurrentFocus = inFocusWindow; @@ -212,14 +220,14 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { appWindow.updateFrameRateSelectionPriorityIfNeeded(); // The window is not in focus. assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); // Make sure that the mode ID is not set. appWindow.mAttrs.preferredDisplayModeId = 0; appWindow.updateFrameRateSelectionPriorityIfNeeded(); // Priority doesn't change. assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority( appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET); @@ -237,11 +245,10 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { when(mDenylist.isDenylisted("com.android.test")).thenReturn(true); assertEquals(0, mRefreshRatePolicy.getPreferredModeId(appWindow)); - assertEquals(60, mRefreshRatePolicy.getPreferredRefreshRate(appWindow), FLOAT_TOLERANCE); appWindow.updateFrameRateSelectionPriorityIfNeeded(); assertEquals(RefreshRatePolicy.LAYER_PRIORITY_UNSET, appWindow.mFrameRateSelectionPriority); - assertEquals(60, appWindow.mAppPreferredFrameRate, FLOAT_TOLERANCE); + assertEquals(FRAME_RATE_VOTE_60_EXACT, appWindow.mFrameRateVote); // Call the function a few times. appWindow.updateFrameRateSelectionPriorityIfNeeded(); @@ -262,19 +269,19 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { .thenReturn(DisplayManager.SWITCHING_TYPE_NONE); assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); // Update the mode ID to a requested number. appWindow.mAttrs.preferredDisplayModeId = 1; appWindow.updateFrameRateSelectionPriorityIfNeeded(); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); // Remove the mode ID request. appWindow.mAttrs.preferredDisplayModeId = 0; appWindow.updateFrameRateSelectionPriorityIfNeeded(); - assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE); + assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE); verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority( appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET); @@ -292,11 +299,10 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { appWindow.mAttrs.preferredRefreshRate = 60; assertEquals(0, mRefreshRatePolicy.getPreferredModeId(appWindow)); - assertEquals(60, mRefreshRatePolicy.getPreferredRefreshRate(appWindow), FLOAT_TOLERANCE); appWindow.updateFrameRateSelectionPriorityIfNeeded(); assertEquals(RefreshRatePolicy.LAYER_PRIORITY_UNSET, appWindow.mFrameRateSelectionPriority); - assertEquals(60, appWindow.mAppPreferredFrameRate, FLOAT_TOLERANCE); + assertEquals(FRAME_RATE_VOTE_60_PREFERRED, appWindow.mFrameRateVote); // Call the function a few times. appWindow.updateFrameRateSelectionPriorityIfNeeded(); @@ -307,6 +313,6 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { any(SurfaceControl.class), anyInt()); verify(appWindow.getPendingTransaction(), times(1)).setFrameRate( appWindow.getSurfaceControl(), 60, - Surface.FRAME_RATE_COMPATIBILITY_EXACT, Surface.CHANGE_FRAME_RATE_ALWAYS); + Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, Surface.CHANGE_FRAME_RATE_ALWAYS); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java index 9d2eb26f5f21..bcaf8860b072 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java @@ -16,22 +16,29 @@ package com.android.server.wm; +import static android.view.SurfaceControl.RefreshRateRange.FLOAT_TOLERANCE; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.hardware.display.DisplayManager; import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.view.Display.Mode; +import android.view.Surface; import android.view.WindowManager.LayoutParams; import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; +import com.android.server.wm.RefreshRatePolicy.FrameRateVote; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,7 +52,6 @@ import org.junit.runner.RunWith; @RunWith(WindowTestRunner.class) @FlakyTest public class RefreshRatePolicyTest extends WindowTestsBase { - private static final float FLOAT_TOLERANCE = 0.01f; private static final int HI_MODE_ID = 1; private static final float HI_REFRESH_RATE = 90; @@ -57,6 +63,19 @@ public class RefreshRatePolicyTest extends WindowTestsBase { private RefreshRatePolicy mPolicy; private HighRefreshRateDenylist mDenylist = mock(HighRefreshRateDenylist.class); + private FrameRateVote mTempFrameRateVote = new FrameRateVote(); + + private static final FrameRateVote FRAME_RATE_VOTE_NONE = new FrameRateVote(); + private static final FrameRateVote FRAME_RATE_VOTE_DENY_LIST = + new FrameRateVote(LOW_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_EXACT); + private static final FrameRateVote FRAME_RATE_VOTE_LOW_EXACT = + new FrameRateVote(LOW_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_EXACT); + private static final FrameRateVote FRAME_RATE_VOTE_HI_EXACT = + new FrameRateVote(HI_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_EXACT); + private static final FrameRateVote FRAME_RATE_VOTE_LOW_PREFERRED = + new FrameRateVote(LOW_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT); + private static final FrameRateVote FRAME_RATE_VOTE_HI_PREFERRED = + new FrameRateVote(HI_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT); // Parcel and Unparcel the LayoutParams in the window state to test the path the object // travels from the app's process to system server @@ -89,6 +108,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { WindowState createWindow(String name) { WindowState window = createWindow(null, TYPE_BASE_APPLICATION, name); when(window.getDisplayInfo()).thenReturn(mDisplayInfo); + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS); return window; } @@ -98,20 +119,23 @@ public class RefreshRatePolicyTest extends WindowTestsBase { cameraUsingWindow.mAttrs.packageName = "com.android.test"; parcelLayoutParams(cameraUsingWindow); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); mPolicy.addRefreshRateRangeForPackage("com.android.test", LOW_REFRESH_RATE, LOW_REFRESH_RATE); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); mPolicy.removeRefreshRateRangeForPackage("com.android.test"); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); } @@ -122,20 +146,23 @@ public class RefreshRatePolicyTest extends WindowTestsBase { cameraUsingWindow.mAttrs.packageName = "com.android.test"; parcelLayoutParams(cameraUsingWindow); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); mPolicy.addRefreshRateRangeForPackage("com.android.test", LOW_REFRESH_RATE, MID_REFRESH_RATE); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(MID_REFRESH_RATE, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); mPolicy.removeRefreshRateRangeForPackage("com.android.test"); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); } @@ -146,20 +173,23 @@ public class RefreshRatePolicyTest extends WindowTestsBase { cameraUsingWindow.mAttrs.packageName = "com.android.test"; parcelLayoutParams(cameraUsingWindow); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); mPolicy.addRefreshRateRangeForPackage("com.android.test", LOW_REFRESH_RATE - 10, HI_REFRESH_RATE + 10); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(HI_REFRESH_RATE, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); mPolicy.removeRefreshRateRangeForPackage("com.android.test"); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); } @@ -171,8 +201,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { parcelLayoutParams(denylistedWindow); when(mDenylist.isDenylisted("com.android.test")).thenReturn(true); assertEquals(0, mPolicy.getPreferredModeId(denylistedWindow)); - assertEquals(LOW_REFRESH_RATE, - mPolicy.getPreferredRefreshRate(denylistedWindow), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(denylistedWindow)); + assertEquals(FRAME_RATE_VOTE_DENY_LIST, denylistedWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(denylistedWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(denylistedWindow), FLOAT_TOLERANCE); } @@ -185,8 +215,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { parcelLayoutParams(overrideWindow); when(mDenylist.isDenylisted("com.android.test")).thenReturn(true); assertEquals(HI_MODE_ID, mPolicy.getPreferredModeId(overrideWindow)); - assertEquals(HI_REFRESH_RATE, - mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); + assertEquals(FRAME_RATE_VOTE_HI_EXACT, overrideWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE); } @@ -199,8 +229,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { parcelLayoutParams(overrideWindow); when(mDenylist.isDenylisted("com.android.test")).thenReturn(true); assertEquals(0, mPolicy.getPreferredModeId(overrideWindow)); - assertEquals(HI_REFRESH_RATE, - mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); + assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, overrideWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE); } @@ -214,8 +244,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { mPolicy.addRefreshRateRangeForPackage("com.android.test", LOW_REFRESH_RATE, LOW_REFRESH_RATE); assertEquals(HI_MODE_ID, mPolicy.getPreferredModeId(overrideWindow)); - assertEquals(HI_REFRESH_RATE, - mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); + assertEquals(FRAME_RATE_VOTE_HI_EXACT, overrideWindow.mFrameRateVote); assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(LOW_REFRESH_RATE, @@ -231,8 +261,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { mPolicy.addRefreshRateRangeForPackage("com.android.test", LOW_REFRESH_RATE, LOW_REFRESH_RATE); assertEquals(0, mPolicy.getPreferredModeId(overrideWindow)); - assertEquals(HI_REFRESH_RATE, - mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); + assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, overrideWindow.mFrameRateVote); assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(LOW_REFRESH_RATE, @@ -246,8 +276,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { overrideWindow.mAttrs.preferredDisplayModeId = LOW_MODE_ID; parcelLayoutParams(overrideWindow); assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow)); - assertEquals(LOW_REFRESH_RATE, - mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); + assertEquals(FRAME_RATE_VOTE_LOW_EXACT, overrideWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE); @@ -255,7 +285,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { overrideWindow.getPendingTransaction(), mock(AnimationAdapter.class), false /* hidden */, ANIMATION_TYPE_APP_TRANSITION); assertEquals(0, mPolicy.getPreferredModeId(overrideWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE); } @@ -267,8 +298,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { overrideWindow.mAttrs.preferredRefreshRate = LOW_REFRESH_RATE; parcelLayoutParams(overrideWindow); assertEquals(0, mPolicy.getPreferredModeId(overrideWindow)); - assertEquals(LOW_REFRESH_RATE, - mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); + assertEquals(FRAME_RATE_VOTE_LOW_PREFERRED, overrideWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE); @@ -276,7 +307,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { overrideWindow.getPendingTransaction(), mock(AnimationAdapter.class), false /* hidden */, ANIMATION_TYPE_APP_TRANSITION); assertEquals(0, mPolicy.getPreferredModeId(overrideWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(overrideWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE); } @@ -288,8 +320,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { parcelLayoutParams(window); when(mDenylist.isDenylisted("com.android.test")).thenReturn(true); assertEquals(0, mPolicy.getPreferredModeId(window)); - assertEquals(LOW_REFRESH_RATE, - mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_DENY_LIST, window.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE); @@ -297,7 +329,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { window.getPendingTransaction(), mock(AnimationAdapter.class), false /* hidden */, ANIMATION_TYPE_APP_TRANSITION); assertEquals(0, mPolicy.getPreferredModeId(window)); - assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE); } @@ -311,7 +344,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { mPolicy.addRefreshRateRangeForPackage("com.android.test", LOW_REFRESH_RATE, LOW_REFRESH_RATE); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(LOW_REFRESH_RATE, @@ -321,7 +355,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { cameraUsingWindow.getPendingTransaction(), mock(AnimationAdapter.class), false /* hidden */, ANIMATION_TYPE_APP_TRANSITION); assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow)); - assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow)); + assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE); } @@ -332,7 +367,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { window.mAttrs.preferredMaxDisplayRefreshRate = LOW_REFRESH_RATE; parcelLayoutParams(window); assertEquals(0, mPolicy.getPreferredModeId(window)); - assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE); assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE); @@ -340,7 +376,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { window.getPendingTransaction(), mock(AnimationAdapter.class), false /* hidden */, ANIMATION_TYPE_APP_TRANSITION); assertEquals(0, mPolicy.getPreferredModeId(window)); - assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE); } @@ -351,7 +388,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { window.mAttrs.preferredMinDisplayRefreshRate = LOW_REFRESH_RATE; parcelLayoutParams(window); assertEquals(0, mPolicy.getPreferredModeId(window)); - assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE); @@ -359,7 +397,8 @@ public class RefreshRatePolicyTest extends WindowTestsBase { window.getPendingTransaction(), mock(AnimationAdapter.class), false /* hidden */, ANIMATION_TYPE_APP_TRANSITION); assertEquals(0, mPolicy.getPreferredModeId(window)); - assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE); } @@ -370,8 +409,92 @@ public class RefreshRatePolicyTest extends WindowTestsBase { window.mAttrs.preferredRefreshRate = LOW_REFRESH_RATE; parcelLayoutParams(window); assertEquals(0, mPolicy.getPreferredModeId(window)); - assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE); + assertTrue(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_LOW_PREFERRED, window.mFrameRateVote); assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE); assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE); } + + @Test + public void testSwitchingTypeForExactVote() { + final WindowState window = createWindow("window"); + window.mAttrs.preferredDisplayModeId = HI_MODE_ID; + parcelLayoutParams(window); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_NONE); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS); + assertTrue(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_HI_EXACT, window.mFrameRateVote); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_HI_EXACT, window.mFrameRateVote); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY); + assertTrue(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); + } + + @Test + public void testSwitchingTypeForPreferredVote() { + final WindowState window = createWindow("window"); + window.mAttrs.preferredRefreshRate = HI_REFRESH_RATE; + parcelLayoutParams(window); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_NONE); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS); + assertTrue(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, window.mFrameRateVote); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, window.mFrameRateVote); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, window.mFrameRateVote); + } + + @Test + public void testSwitchingTypeForDenylist() { + when(mDenylist.isDenylisted("com.android.test")).thenReturn(true); + + final WindowState window = createWindow("window"); + window.mAttrs.packageName = "com.android.test"; + parcelLayoutParams(window); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_NONE); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS); + assertTrue(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_LOW_EXACT, window.mFrameRateVote); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS); + assertFalse(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_LOW_EXACT, window.mFrameRateVote); + + when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()) + .thenReturn(DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY); + assertTrue(mPolicy.updateFrameRateVote(window)); + assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java index 846a5066e036..5e1fae095db8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java @@ -192,16 +192,17 @@ public class SurfaceSyncGroupTest { } private static class SyncTarget implements SurfaceSyncGroup.SyncTarget { - private SurfaceSyncGroup.SyncBufferCallback mSyncBufferCallback; + private SurfaceSyncGroup.TransactionReadyCallback mTransactionReadyCallback; @Override - public void onReadyToSync(SurfaceSyncGroup.SyncBufferCallback syncBufferCallback) { - mSyncBufferCallback = syncBufferCallback; + public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, + SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) { + mTransactionReadyCallback = transactionReadyCallback; } void onBufferReady() { SurfaceControl.Transaction t = new StubTransaction(); - mSyncBufferCallback.onBufferReady(t); + mTransactionReadyCallback.onTransactionReady(t); } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 66bf78b6a13b..d52c34bdc9e1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -36,6 +36,7 @@ import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_90; import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; @@ -80,6 +81,7 @@ import android.util.DisplayMetrics; import android.util.Xml; import android.view.Display; import android.view.DisplayInfo; +import android.window.TaskFragmentOrganizer; import androidx.test.filters.MediumTest; @@ -433,6 +435,24 @@ public class TaskTests extends WindowTestsBase { } @Test + public void testPropagateFocusedStateToRootTask() { + final Task rootTask = createTask(mDefaultDisplay); + final Task leafTask = createTaskInRootTask(rootTask, 0 /* userId */); + + final ActivityRecord activity = createActivityRecord(leafTask); + + leafTask.getDisplayContent().setFocusedApp(activity); + + assertTrue(leafTask.getTaskInfo().isFocused); + assertTrue(rootTask.getTaskInfo().isFocused); + + leafTask.getDisplayContent().setFocusedApp(null); + + assertFalse(leafTask.getTaskInfo().isFocused); + assertFalse(rootTask.getTaskInfo().isFocused); + } + + @Test public void testReturnsToHomeRootTask() throws Exception { final Task task = createTask(1); spyOn(task); @@ -1471,6 +1491,26 @@ public class TaskTests extends WindowTestsBase { tf0, parentTask.getTaskFragment(TaskFragment::isOrganizedTaskFragment)); } + @Test + public void testReorderActivityToFront() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + doNothing().when(task).onActivityVisibleRequestedChanged(); + final ActivityRecord activity = task.getTopMostActivity(); + + final TaskFragment fragment = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord embeddedActivity = fragment.getTopMostActivity(); + task.moveActivityToFront(activity); + assertEquals("Activity must be moved to front", activity, task.getTopMostActivity()); + + doNothing().when(fragment).sendTaskFragmentInfoChanged(); + task.moveActivityToFront(embeddedActivity); + assertEquals("Activity must be moved to front", embeddedActivity, + task.getTopMostActivity()); + assertEquals("Activity must not be embedded", embeddedActivity, + task.getTopChild()); + } + private Task getTestTask() { final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); return task.getBottomMostTask(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 3b64c512ca8f..690c2aa1fe2b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -1176,7 +1176,7 @@ public class WindowOrganizerTests extends WindowTestsBase { assertTrue(rootTask2.isOrganized()); // Verify a back pressed does not call the organizer - mWm.mAtmService.mActivityClientController.onBackPressedOnTaskRoot(activity.token, + mWm.mAtmService.mActivityClientController.onBackPressed(activity.token, new IRequestFinishCallback.Default()); // Ensure events dispatch to organizer. mWm.mAtmService.mTaskOrganizerController.dispatchPendingEvents(); @@ -1187,7 +1187,7 @@ public class WindowOrganizerTests extends WindowTestsBase { rootTask.mRemoteToken.toWindowContainerToken(), true); // Verify now that the back press does call the organizer - mWm.mAtmService.mActivityClientController.onBackPressedOnTaskRoot(activity.token, + mWm.mAtmService.mActivityClientController.onBackPressed(activity.token, new IRequestFinishCallback.Default()); // Ensure events dispatch to organizer. mWm.mAtmService.mTaskOrganizerController.dispatchPendingEvents(); @@ -1198,7 +1198,7 @@ public class WindowOrganizerTests extends WindowTestsBase { rootTask.mRemoteToken.toWindowContainerToken(), false); // Verify now that the back press no longer calls the organizer - mWm.mAtmService.mActivityClientController.onBackPressedOnTaskRoot(activity.token, + mWm.mAtmService.mActivityClientController.onBackPressed(activity.token, new IRequestFinishCallback.Default()); // Ensure events dispatch to organizer. mWm.mAtmService.mTaskOrganizerController.dispatchPendingEvents(); @@ -1404,7 +1404,7 @@ public class WindowOrganizerTests extends WindowTestsBase { mWm.mWindowPlacerLocked.deferLayout(); rootTask.removeImmediately(); - mWm.mAtmService.mActivityClientController.onBackPressedOnTaskRoot(record.token, + mWm.mAtmService.mActivityClientController.onBackPressed(record.token, new IRequestFinishCallback.Default()); waitUntilHandlersIdle(); diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index ccec67e8687e..af37ed583438 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -1851,7 +1851,7 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - return service.getCallStateUsingPackage(mContext.getPackageName(), + return service.getCallStateUsingPackage(mContext.getOpPackageName(), mContext.getAttributionTag()); } catch (RemoteException e) { Log.d(TAG, "RemoteException calling getCallState().", e); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index f38b902f7531..ed96a9b2f996 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -7095,6 +7095,274 @@ public class CarrierConfigManager { public static final String KEY_REFRESH_GEOLOCATION_TIMEOUT_MILLIS_INT = KEY_PREFIX + "refresh_geolocation_timeout_millis_int"; + /** + * List of 3GPP access network technologies where e911 over IMS is supported + * in the home network and domestic 3rd-party networks. The order in the list represents + * the preference. The domain selection service shall scan the network type in the order + * of the preference. + * + * <p>Possible values are, + * {@link AccessNetworkConstants.AccessNetworkType#NGRAN} + * {@link AccessNetworkConstants.AccessNetworkType#EUTRAN} + * + * The default value for this key is + * {{@link AccessNetworkConstants.AccessNetworkType#EUTRAN}, + * @hide + */ + public static final String + KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY = KEY_PREFIX + + "emergency_over_ims_supported_3gpp_network_types_int_array"; + + /** + * List of 3GPP access network technologies where e911 over IMS is supported + * in the roaming network and non-domestic 3rd-party networks. The order in the list + * represents the preference. The domain selection service shall scan the network type + * in the order of the preference. + * + * <p>Possible values are, + * {@link AccessNetworkConstants.AccessNetworkType#NGRAN} + * {@link AccessNetworkConstants.AccessNetworkType#EUTRAN} + * + * The default value for this key is + * {{@link AccessNetworkConstants.AccessNetworkType#EUTRAN}, + * @hide + */ + public static final String + KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY = KEY_PREFIX + + "emergency_over_ims_roaming_supported_3gpp_network_types_int_array"; + + /** + * List of CS access network technologies where circuit-switched emergency calls are + * supported in the home network and domestic 3rd-party networks. The order in the list + * represents the preference. The domain selection service shall scan the network type + * in the order of the preference. + * + * <p>Possible values are, + * {@link AccessNetworkConstants.AccessNetworkType#GERAN} + * {@link AccessNetworkConstants.AccessNetworkType#UTRAN} + * {@link AccessNetworkConstants.AccessNetworkType#CDMA2000} + * + * The default value for this key is + * {{@link AccessNetworkConstants.AccessNetworkType#UTRAN}, + * {@link AccessNetworkConstants.AccessNetworkType#GERAN}}. + * @hide + */ + public static final String KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY = + KEY_PREFIX + "emergency_over_cs_supported_access_network_types_int_array"; + + /** + * List of CS access network technologies where circuit-switched emergency calls are + * supported in the roaming network and non-domestic 3rd-party networks. The order + * in the list represents the preference. The domain selection service shall scan + * the network type in the order of the preference. + * + * <p>Possible values are, + * {@link AccessNetworkConstants.AccessNetworkType#GERAN} + * {@link AccessNetworkConstants.AccessNetworkType#UTRAN} + * {@link AccessNetworkConstants.AccessNetworkType#CDMA2000} + * + * The default value for this key is + * {{@link AccessNetworkConstants.AccessNetworkType#UTRAN}, + * {@link AccessNetworkConstants.AccessNetworkType#GERAN}}. + * @hide + */ + public static final String + KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY = KEY_PREFIX + + "emergency_over_cs_roaming_supported_access_network_types_int_array"; + + /** @hide */ + @IntDef({ + DOMAIN_CS, + DOMAIN_PS_3GPP, + DOMAIN_PS_NON_3GPP + }) + public @interface EmergencyDomain {} + + /** + * Circuit switched domain. + * @hide + */ + public static final int DOMAIN_CS = 1; + + /** + * Packet switched domain over 3GPP networks. + * @hide + */ + public static final int DOMAIN_PS_3GPP = 2; + + /** + * Packet switched domain over non-3GPP networks such as Wi-Fi. + * @hide + */ + public static final int DOMAIN_PS_NON_3GPP = 3; + + /** + * Specifies the emergency call domain preference for the home network. + * The domain selection service shall choose the domain in the order + * for attempting the emergency call + * + * <p>Possible values are, + * {@link #DOMAIN_CS} + * {@link #DOMAIN_PS_3GPP} + * {@link #DOMAIN_PS_NON_3GPP}. + * + * The default value for this key is + * {{@link #DOMAIN_PS_3GPP}, + * {@link #DOMAIN_CS}, + * {@link #DOMAIN_PS_NON_3GPP}}. + * @hide + */ + public static final String KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY = + KEY_PREFIX + "emergency_domain_preference_int_array"; + + /** + * Specifies the emergency call domain preference for the roaming network. + * The domain selection service shall choose the domain in the order + * for attempting the emergency call. + * + * <p>Possible values are, + * {@link #DOMAIN_CS} + * {@link #DOMAIN_PS_3GPP} + * {@link #DOMAIN_PS_NON_3GPP}. + * + * The default value for this key is + * {{@link #DOMAIN_PS_3GPP}, + * {@link #DOMAIN_CS}, + * {@link #DOMAIN_PS_NON_3GPP}}. + * @hide + */ + public static final String KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY = + KEY_PREFIX + "emergency_domain_preference_roaming_int_array"; + + /** + * Specifies if emergency call shall be attempted on IMS, if PS is attached even though IMS + * is not registered and normal calls fallback to the CS networks. + * + * The default value for this key is {@code false}. + * @hide + */ + public static final String KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL = + KEY_PREFIX + "prefer_ims_emergency_when_voice_calls_on_cs_bool"; + + /** + * Specifies maximum number of emergency call retries over Wi-Fi. + * This is valid only when {@link #DOMAIN_PS_NON_3GPP} is included in + * {@link #KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY} or + * {@link #KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY}. + * + * The default value for this key is 1. + * @hide + */ + public static final String KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT = + KEY_PREFIX + "maximum_number_of_emergency_tries_over_vowifi_int"; + + /** + * Emergency scan timer to wait for scan results from radio before attempting the call + * over Wi-Fi. On timer expiry, if emergency call on Wi-Fi is allowed and possible, + * telephony shall cancel the scan and place the call on Wi-Fi. If emergency call on Wi-Fi + * is not possible, then domain seleciton continues to wait for the scan result from the + * radio. If an emergency scan result is received before the timer expires, the timer shall + * be stopped and no dialing over Wi-Fi will be tried. If this value is set to 0, then + * the timer is never started and domain selection waits for the scan result from the radio. + * + * The default value for the timer is 10 seconds. + * @hide + */ + public static final String KEY_EMERGENCY_SCAN_TIMER_SEC_INT = + KEY_PREFIX + "emergency_scan_timer_sec_int"; + + /** @hide */ + @IntDef(prefix = "SCAN_TYPE_", + value = { + SCAN_TYPE_NO_PREFERENCE, + SCAN_TYPE_FULL_SERVICE, + SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE}) + public @interface EmergencyScanType {} + + /** + * No specific preference given to the modem. Modem can return an emergency + * capable network either with limited service or full service. + * @hide + */ + public static final int SCAN_TYPE_NO_PREFERENCE = 0; + + /** + * Modem will attempt to camp on a network with full service only. + * @hide + */ + public static final int SCAN_TYPE_FULL_SERVICE = 1; + + /** + * Telephony shall attempt full service scan first. + * If a full service network is not found, telephony shall attempt a limited service scan. + * @hide + */ + public static final int SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE = 2; + + /** + * Specifies the preferred emergency network scanning type. + * + * <p>Possible values are, + * {@link #SCAN_TYPE_NO_PREFERENCE} + * {@link #SCAN_TYPE_FULL_SERVICE} + * {@link #SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE} + * + * The default value for this key is {@link #SCAN_TYPE_NO_PREFERENCE}. + * @hide + */ + public static final String KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT = + KEY_PREFIX + "emergency_network_scan_type_int"; + + /** + * Specifies the time by which a call should be set up on the current network + * once the call is routed on the network. If the call cannot be set up by timer expiry, + * call shall be re-dialed on the next available network. + * If this value is set to 0, the timer shall be disabled. + * + * The default value for this key is 0. + * @hide + */ + public static final String KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT = + KEY_PREFIX + "emergency_call_setup_timer_on_current_network_sec_int"; + + /** + * Specifies if emergency call shall be attempted on IMS only when IMS is registered. + * This is applicable only for the case PS is in service. + * + * The default value for this key is {@code false}. + * @hide + */ + public static final String KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL = + KEY_PREFIX + "emergency_requires_ims_registration_bool"; + + /** + * Specifies if LTE is preferred when re-scanning networks after the failure of dialing + * over NR. If not, CS will be preferred. + * + * The default value for this key is {@code false}. + * @hide + */ + public static final String KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL = + KEY_PREFIX + "emergency_lte_preferred_after_nr_failed_bool"; + + /** + * Specifies the numbers to be dialed over CDMA network in case of dialing over CS network. + * + * The default value for this key is an empty string array. + * @hide + */ + public static final String KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY = + KEY_PREFIX + "emergency_cdma_preferred_numbers_string_array"; + + /** + * Specifies if emergency call shall be attempted on IMS only when VoLTE is enabled. + * + * The default value for this key is {@code false}. + * @hide + */ + public static final String KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL = + KEY_PREFIX + "emergency_requires_volte_enabled_bool"; + private static PersistableBundle getDefaults() { PersistableBundle defaults = new PersistableBundle(); defaults.putBoolean(KEY_RETRY_EMERGENCY_ON_IMS_PDN_BOOL, false); @@ -7111,6 +7379,56 @@ public class CarrierConfigManager { defaults.putInt(KEY_EMERGENCY_REGISTRATION_TIMER_MILLIS_INT, 10000); defaults.putInt(KEY_REFRESH_GEOLOCATION_TIMEOUT_MILLIS_INT, 5000); + defaults.putIntArray( + KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY, + new int[] { + AccessNetworkType.EUTRAN, + }); + + defaults.putIntArray( + KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY, + new int[] { + AccessNetworkType.EUTRAN, + }); + + defaults.putIntArray( + KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY, + new int[] { + AccessNetworkType.UTRAN, + AccessNetworkType.GERAN, + }); + + defaults.putIntArray( + KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY, + new int[] { + AccessNetworkType.UTRAN, + AccessNetworkType.GERAN, + }); + + defaults.putIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY, + new int[] { + DOMAIN_PS_3GPP, + DOMAIN_CS, + DOMAIN_PS_NON_3GPP + }); + defaults.putIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY, + new int[] { + DOMAIN_PS_3GPP, + DOMAIN_CS, + DOMAIN_PS_NON_3GPP + }); + + defaults.putBoolean(KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL, false); + defaults.putInt(KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT, 1); + defaults.putInt(KEY_EMERGENCY_SCAN_TIMER_SEC_INT, 10); + defaults.putInt(KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT, SCAN_TYPE_NO_PREFERENCE); + defaults.putInt(KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT, 0); + defaults.putBoolean(KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL, false); + defaults.putBoolean(KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL, false); + defaults.putBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL, false); + defaults.putStringArray(KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY, + new String[] {}); + return defaults; } } @@ -7920,7 +8238,8 @@ public class CarrierConfigManager { KEY_XCAP_OVER_UT_SUPPORTED_RATS_INT_ARRAY, new int[] { AccessNetworkType.EUTRAN, - AccessNetworkType.IWLAN + AccessNetworkType.IWLAN, + AccessNetworkType.NGRAN }); defaults.putString(KEY_UT_AS_SERVER_FQDN_STRING, ""); defaults.putBoolean(KEY_TERMINAL_BASED_CALL_WAITING_DEFAULT_ENABLED_BOOL, true); diff --git a/telephony/java/android/telephony/CellIdentity.java b/telephony/java/android/telephony/CellIdentity.java index 06cfd6718664..6e3cfacf0208 100644 --- a/telephony/java/android/telephony/CellIdentity.java +++ b/telephony/java/android/telephony/CellIdentity.java @@ -107,7 +107,7 @@ public abstract class CellIdentity implements Parcelable { if ((mMccStr != null && mMncStr == null) || (mMccStr == null && mMncStr != null)) { AnomalyReporter.reportAnomaly( - UUID.fromString("a3ab0b9d-f2aa-4baf-911d-7096c0d4645a"), + UUID.fromString("e257ae06-ac0a-44c0-ba63-823b9f07b3e4"), "CellIdentity Missing Half of PLMN ID"); } diff --git a/telephony/java/android/telephony/DomainSelectionService.java b/telephony/java/android/telephony/DomainSelectionService.java index 383561ad01a0..c352f2bc3f83 100644 --- a/telephony/java/android/telephony/DomainSelectionService.java +++ b/telephony/java/android/telephony/DomainSelectionService.java @@ -28,7 +28,6 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; -import android.telephony.AccessNetworkConstants.RadioAccessNetworkType; import android.telephony.Annotation.DisconnectCauses; import android.telephony.Annotation.PreciseDisconnectCauses; import android.telephony.ims.ImsReasonInfo; @@ -703,9 +702,9 @@ public class DomainSelectionService extends Service { } @Override - public void onDomainSelected(@RadioAccessNetworkType int accessNetworkType) { + public void onDomainSelected(@NetworkRegistrationInfo.Domain int domain) { try { - mCallback.onDomainSelected(accessNetworkType); + mCallback.onDomainSelected(domain); } catch (Exception e) { Rlog.e(TAG, "onDomainSelected e=" + e); } @@ -835,7 +834,14 @@ public class DomainSelectionService extends Service { return Runnable::run; } - private @NonNull Executor getCachedExecutor() { + /** + * Gets the {@link Executor} which executes methods of this service. + * This method should be private when this service is implemented in a separated process + * other than telephony framework. + * @return {@link Executor} instance. + * @hide + */ + public @NonNull Executor getCachedExecutor() { synchronized (mExecutorLock) { if (mExecutor == null) { Executor e = getExecutor(); diff --git a/telephony/java/android/telephony/RadioAccessSpecifier.java b/telephony/java/android/telephony/RadioAccessSpecifier.java index a403095eeceb..9511db622a27 100644 --- a/telephony/java/android/telephony/RadioAccessSpecifier.java +++ b/telephony/java/android/telephony/RadioAccessSpecifier.java @@ -161,9 +161,17 @@ public final class RadioAccessSpecifier implements Parcelable { } @Override - public int hashCode () { + public int hashCode() { return ((mRadioAccessNetwork * 31) + (Arrays.hashCode(mBands) * 37) + (Arrays.hashCode(mChannels)) * 39); } + + @Override + public String toString() { + return "RadioAccessSpecifier[mRadioAccessNetwork=" + + AccessNetworkConstants.AccessNetworkType.toString(mRadioAccessNetwork) + + ", mBands=" + Arrays.toString(mBands) + + ", mChannels=" + Arrays.toString(mChannels) + "]"; + } } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 541573cad3ec..8c3ef675dabc 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -8298,16 +8298,23 @@ public class TelephonyManager { /** Authentication type for UICC challenge is EAP AKA. See RFC 4187 for details. */ public static final int AUTHTYPE_EAP_AKA = PhoneConstants.AUTH_CONTEXT_EAP_AKA; /** - * Authentication type for GBA Bootstrap Challenge is GBA_BOOTSTRAP. - * See 3GPP 33.220 Section 5.3.2. - * @hide + * Authentication type for GBA Bootstrap Challenge. + * Pass this authentication type into the {@link #getIccAuthentication} API to perform a GBA + * Bootstrap challenge (BSF), with {@code data} (generated according to the procedure defined in + * 3GPP 33.220 Section 5.3.2 step.4) in base64 encoding. + * This method will return the Bootstrapping response in base64 encoding when ICC authentication + * is completed. + * Ref 3GPP 33.220 Section 5.3.2. */ public static final int AUTHTYPE_GBA_BOOTSTRAP = PhoneConstants.AUTH_CONTEXT_GBA_BOOTSTRAP; /** - * Authentication type for GBA Network Application Functions (NAF) key - * External Challenge is AUTHTYPE_GBA_NAF_KEY_EXTERNAL. - * See 3GPP 33.220 Section 5.3.2. - * @hide + * Authentication type for GBA Network Application Functions (NAF) key External Challenge. + * Pass this authentication type into the {@link #getIccAuthentication} API to perform a GBA + * Network Applications Functions (NAF) key External challenge using the NAF_ID parameter + * as the {@code data} in base64 encoding. + * This method will return the Ks_Ext_Naf key in base64 encoding when ICC authentication + * is completed. + * Ref 3GPP 33.220 Section 5.3.2. */ public static final int AUTHTYPE_GBA_NAF_KEY_EXTERNAL = PhoneConstants.AUTHTYPE_GBA_NAF_KEY_EXTERNAL; @@ -8336,7 +8343,8 @@ public class TelephonyManager { * * @param appType the icc application type, like {@link #APPTYPE_USIM} * @param authType the authentication type, any one of {@link #AUTHTYPE_EAP_AKA} or - * {@link #AUTHTYPE_EAP_SIM} + * {@link #AUTHTYPE_EAP_SIM} or {@link #AUTHTYPE_GBA_BOOTSTRAP} or + * {@link #AUTHTYPE_GBA_NAF_KEY_EXTERNAL} * @param data authentication challenge data, base64 encoded. * See 3GPP TS 31.102 7.1.2 for more details. * @return the response of authentication. This value will be null in the following cases: @@ -8364,7 +8372,8 @@ public class TelephonyManager { * @param subId subscription ID used for authentication * @param appType the icc application type, like {@link #APPTYPE_USIM} * @param authType the authentication type, any one of {@link #AUTHTYPE_EAP_AKA} or - * {@link #AUTHTYPE_EAP_SIM} + * {@link #AUTHTYPE_EAP_SIM} or {@link #AUTHTYPE_GBA_BOOTSTRAP} or + * {@link #AUTHTYPE_GBA_NAF_KEY_EXTERNAL} * @param data authentication challenge data, base64 encoded. * See 3GPP TS 31.102 7.1.2 for more details. * @return the response of authentication. This value will be null in the following cases only @@ -9531,12 +9540,13 @@ public class TelephonyManager { /** * Set the allowed network types of the device and provide the reason triggering the allowed * network change. - * <p>Requires permission: android.Manifest.MODIFY_PHONE_STATE or + * <p>Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} or * that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}). * - * This can be called for following reasons + * This can be called for following reasons: * <ol> - * <li>Allowed network types control by USER {@link #ALLOWED_NETWORK_TYPES_REASON_USER} + * <li>Allowed network types control by USER + * {@link TelephonyManager#ALLOWED_NETWORK_TYPES_REASON_USER} * <li>Allowed network types control by carrier {@link #ALLOWED_NETWORK_TYPES_REASON_CARRIER} * </ol> * This API will result in allowing an intersection of allowed network types for all reasons, @@ -9546,7 +9556,13 @@ public class TelephonyManager { * @param allowedNetworkTypes The bitmask of allowed network type * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if invalid AllowedNetworkTypesReason is passed. - * @throws SecurityException if the caller does not have the required privileges + * @throws SecurityException if the caller does not have the required privileges or if the + * caller tries to use one of the following security-based reasons without + * {@link android.Manifest.permission#MODIFY_PHONE_STATE} permissions. + * <ol> + * <li>{@code TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G}</li> + * <li>{@code TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS}</li> + * </ol> */ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) @RequiresFeature( @@ -12530,7 +12546,7 @@ public class TelephonyManager { Log.e(TAG, "Error calling ITelephony#getServiceStateForSubscriber", e); } catch (NullPointerException e) { AnomalyReporter.reportAnomaly( - UUID.fromString("a3ab0b9d-f2aa-4baf-911d-7096c0d4645a"), + UUID.fromString("e2bed88e-def9-476e-bd71-3e572a8de6d1"), "getServiceStateForSubscriber " + subId + " NPE"); } return null; diff --git a/telephony/java/android/telephony/WwanSelectorCallback.java b/telephony/java/android/telephony/WwanSelectorCallback.java index 489a589d2b62..b3682caa4eb3 100644 --- a/telephony/java/android/telephony/WwanSelectorCallback.java +++ b/telephony/java/android/telephony/WwanSelectorCallback.java @@ -18,7 +18,6 @@ package android.telephony; import android.annotation.NonNull; import android.os.CancellationSignal; -import android.telephony.AccessNetworkConstants.RadioAccessNetworkType; import android.telephony.DomainSelectionService.EmergencyScanType; import java.util.List; @@ -46,7 +45,7 @@ public interface WwanSelectorCallback { * Notifies the FW that the domain has been selected. After this method is called, * this interface can be discarded. * - * @param accessNetworkType the selected network type. + * @param domain The selected domain. */ - void onDomainSelected(@RadioAccessNetworkType int accessNetworkType); + void onDomainSelected(@NetworkRegistrationInfo.Domain int domain); } diff --git a/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl b/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl index 65d994b9066b..339fbee91812 100644 --- a/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl +++ b/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl @@ -21,6 +21,6 @@ import com.android.internal.telephony.IWwanSelectorResultCallback; oneway interface IWwanSelectorCallback { void onRequestEmergencyNetworkScan(in int[] preferredNetworks, int scanType, in IWwanSelectorResultCallback cb); - void onDomainSelected(int accessNetworkType); + void onDomainSelected(int domain); void onCancel(); } diff --git a/telephony/java/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java index 9f612e6d7dd9..0c14dbaa5a3f 100644 --- a/telephony/java/com/android/internal/telephony/RILConstants.java +++ b/telephony/java/com/android/internal/telephony/RILConstants.java @@ -542,6 +542,10 @@ public interface RILConstants { int RIL_REQUEST_STOP_IMS_TRAFFIC = 236; int RIL_REQUEST_SEND_ANBR_QUERY = 237; int RIL_REQUEST_TRIGGER_EPS_FALLBACK = 238; + int RIL_REQUEST_SET_NULL_CIPHER_AND_INTEGRITY_ENABLED = 239; + int RIL_REQUEST_UPDATE_IMS_CALL_STATUS = 240; + int RIL_REQUEST_SET_N1_MODE_ENABLED = 241; + int RIL_REQUEST_IS_N1_MODE_ENABLED = 242; /* Responses begin */ int RIL_RESPONSE_ACKNOWLEDGEMENT = 800; diff --git a/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java b/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java new file mode 100644 index 000000000000..0f9663442740 --- /dev/null +++ b/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java @@ -0,0 +1,131 @@ +/* + * 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.internal.os; + +import android.content.ComponentName; +import android.content.Intent; +import android.platform.test.annotations.Presubmit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link TimeoutRecord}. */ +@SmallTest +@Presubmit +@RunWith(JUnit4.class) +public class TimeoutRecordTest { + + @Test + public void forBroadcastReceiver_returnsCorrectTimeoutRecord() { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(ComponentName.createRelative("com.example.app", "ExampleClass")); + + TimeoutRecord record = TimeoutRecord.forBroadcastReceiver(intent); + + assertNotNull(record); + assertEquals(record.mKind, TimeoutRecord.TimeoutKind.BROADCAST_RECEIVER); + assertEquals(record.mReason, + "Broadcast of Intent { act=android.intent.action.MAIN cmp=com.example" + + ".app/ExampleClass }"); + assertTrue(record.mEndTakenBeforeLocks); + } + + @Test + public void forBroadcastReceiver_withTimeoutDurationMs_returnsCorrectTimeoutRecord() { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(ComponentName.createRelative("com.example.app", "ExampleClass")); + + TimeoutRecord record = TimeoutRecord.forBroadcastReceiver(intent, 1000L); + + assertNotNull(record); + assertEquals(record.mKind, TimeoutRecord.TimeoutKind.BROADCAST_RECEIVER); + assertEquals(record.mReason, + "Broadcast of Intent { act=android.intent.action.MAIN cmp=com.example" + + ".app/ExampleClass }, waited 1000ms"); + assertTrue(record.mEndTakenBeforeLocks); + } + + @Test + public void forInputDispatchNoFocusedWindow_returnsCorrectTimeoutRecord() { + TimeoutRecord record = TimeoutRecord.forInputDispatchNoFocusedWindow("Test ANR reason"); + + assertNotNull(record); + assertEquals(record.mKind, TimeoutRecord.TimeoutKind.INPUT_DISPATCH_NO_FOCUSED_WINDOW); + assertEquals(record.mReason, + "Test ANR reason"); + assertTrue(record.mEndTakenBeforeLocks); + } + + @Test + public void forInputDispatchWindowUnresponsive_returnsCorrectTimeoutRecord() { + TimeoutRecord record = TimeoutRecord.forInputDispatchWindowUnresponsive("Test ANR reason"); + + assertNotNull(record); + assertEquals(record.mKind, TimeoutRecord.TimeoutKind.INPUT_DISPATCH_WINDOW_UNRESPONSIVE); + assertEquals(record.mReason, "Test ANR reason"); + assertTrue(record.mEndTakenBeforeLocks); + } + + @Test + public void forServiceExec_returnsCorrectTimeoutRecord() { + TimeoutRecord record = TimeoutRecord.forServiceExec("Test ANR reason"); + + assertNotNull(record); + assertEquals(record.mKind, TimeoutRecord.TimeoutKind.SERVICE_EXEC); + assertEquals(record.mReason, "Test ANR reason"); + assertTrue(record.mEndTakenBeforeLocks); + } + + @Test + public void forServiceStartWithEndTime_returnsCorrectTimeoutRecord() { + TimeoutRecord record = TimeoutRecord.forServiceStartWithEndTime("Test ANR reason", 1000L); + + assertNotNull(record); + assertEquals(record.mKind, TimeoutRecord.TimeoutKind.SERVICE_START); + assertEquals(record.mReason, "Test ANR reason"); + assertEquals(record.mEndUptimeMillis, 1000L); + assertTrue(record.mEndTakenBeforeLocks); + } + + @Test + public void forContentProvider_returnsCorrectTimeoutRecord() { + TimeoutRecord record = TimeoutRecord.forContentProvider("Test ANR reason"); + + assertNotNull(record); + assertEquals(record.mKind, TimeoutRecord.TimeoutKind.CONTENT_PROVIDER); + assertEquals(record.mReason, "Test ANR reason"); + assertFalse(record.mEndTakenBeforeLocks); + } + + @Test + public void forApp_returnsCorrectTimeoutRecord() { + TimeoutRecord record = TimeoutRecord.forApp("Test ANR reason"); + + assertNotNull(record); + assertEquals(record.mKind, TimeoutRecord.TimeoutKind.APP_REGISTERED); + assertEquals(record.mReason, "Test ANR reason"); + assertFalse(record.mEndTakenBeforeLocks); + } +} diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp index 47750fc11a6e..4e597fb3b30a 100644 --- a/tools/aapt/ResourceTable.cpp +++ b/tools/aapt/ResourceTable.cpp @@ -3743,15 +3743,15 @@ ssize_t ResourceTable::Entry::flatten(Bundle* /* bundle */, const sp<AaptFile>& size_t amt = 0; ResTable_entry header; memset(&header, 0, sizeof(header)); - header.size = htods(sizeof(header)); + header.full.size = htods(sizeof(header)); const type ty = mType; if (ty == TYPE_BAG) { - header.flags |= htods(header.FLAG_COMPLEX); + header.full.flags |= htods(header.FLAG_COMPLEX); } if (isPublic) { - header.flags |= htods(header.FLAG_PUBLIC); + header.full.flags |= htods(header.FLAG_PUBLIC); } - header.key.index = htodl(mNameIndex); + header.full.key.index = htodl(mNameIndex); if (ty != TYPE_BAG) { status_t err = data->writeData(&header, sizeof(header)); if (err != NO_ERROR) { diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index f9e52b491413..df878899fa28 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -687,32 +687,32 @@ class ChunkPrinter { continue; } - printer_->Print((entry->flags & ResTable_entry::FLAG_COMPLEX) ? "[ResTable_map_entry]" - : "[ResTable_entry]"); + if (entry->is_complex()) { + printer_->Print("[ResTable_map_entry]"); + } else if (entry->is_compact()) { + printer_->Print("[ResTable_entry_compact]"); + } else { + printer_->Print("[ResTable_entry]"); + } + printer_->Print(StringPrintf(" id: 0x%04x", it.index())); printer_->Print(StringPrintf( - " name: %s", - android::util::GetString(key_pool_, android::util::DeviceToHost32(entry->key.index)) - .c_str())); - printer_->Print( - StringPrintf(" keyIndex: %u", android::util::DeviceToHost32(entry->key.index))); - printer_->Print(StringPrintf(" size: %u", android::util::DeviceToHost32(entry->size))); - printer_->Print(StringPrintf(" flags: 0x%04x", android::util::DeviceToHost32(entry->flags))); + " name: %s", android::util::GetString(key_pool_, entry->key()).c_str())); + printer_->Print(StringPrintf(" keyIndex: %u", entry->key())); + printer_->Print(StringPrintf(" size: %zu", entry->size())); + printer_->Print(StringPrintf(" flags: 0x%04x", entry->flags())); printer_->Indent(); - if (entry->flags & ResTable_entry::FLAG_COMPLEX) { - auto map_entry = (const ResTable_map_entry*)entry; - printer_->Print( - StringPrintf(" count: 0x%04x", android::util::DeviceToHost32(map_entry->count))); + if (auto map_entry = entry->map_entry()) { + uint32_t map_entry_count = android::util::DeviceToHost32(map_entry->count); + printer_->Print(StringPrintf(" count: 0x%04x", map_entry_count)); printer_->Print(StringPrintf(" parent: 0x%08x\n", android::util::DeviceToHost32(map_entry->parent.ident))); // Print the name and value mappings - auto maps = (const ResTable_map*)((const uint8_t*)entry + - android::util::DeviceToHost32(entry->size)); - for (size_t i = 0, count = android::util::DeviceToHost32(map_entry->count); i < count; - i++) { + auto maps = (const ResTable_map*)((const uint8_t*)entry + entry->size()); + for (size_t i = 0; i < map_entry_count; i++) { PrintResValue(&(maps[i].value), config, type); printer_->Print(StringPrintf( @@ -725,9 +725,8 @@ class ChunkPrinter { printer_->Print("\n"); // Print the value of the entry - auto value = - (const Res_value*)((const uint8_t*)entry + android::util::DeviceToHost32(entry->size)); - PrintResValue(value, config, type); + Res_value value = entry->value(); + PrintResValue(&value, config, type); } printer_->Undent(); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index cffcdf2f1710..5fdfb66bdf4e 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -159,6 +159,9 @@ class LinkCommand : public Command { AddOptionalSwitch("--enable-sparse-encoding", "This decreases APK size at the cost of resource retrieval performance.", &options_.use_sparse_encoding); + AddOptionalSwitch("--enable-compact-entries", + "This decreases APK size by using compact resource entries for simple data types.", + &options_.table_flattener_options.use_compact_entries); AddOptionalSwitch("-x", "Legacy flag that specifies to use the package identifier 0x01.", &legacy_x_flag_); AddOptionalSwitch("-z", "Require localization of strings marked 'suggested'.", diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp index d9e379db84b7..82918629f1f4 100644 --- a/tools/aapt2/format/binary/BinaryResourceParser.cpp +++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp @@ -384,21 +384,16 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package, continue; } - const ResourceName name( - package->name, *parsed_type, - android::util::GetString(key_pool_, android::util::DeviceToHost32(entry->key.index))); + const ResourceName name(package->name, *parsed_type, + android::util::GetString(key_pool_, entry->key())); const ResourceId res_id(package_id, type->id, static_cast<uint16_t>(it.index())); std::unique_ptr<Value> resource_value; - if (entry->flags & ResTable_entry::FLAG_COMPLEX) { - const ResTable_map_entry* mapEntry = static_cast<const ResTable_map_entry*>(entry); - + if (auto mapEntry = entry->map_entry()) { // TODO(adamlesinski): Check that the entry count is valid. resource_value = ParseMapEntry(name, config, mapEntry); } else { - const Res_value* value = - (const Res_value*)((const uint8_t*)entry + android::util::DeviceToHost32(entry->size)); - resource_value = ParseValue(name, config, *value); + resource_value = ParseValue(name, config, entry->value()); } if (!resource_value) { @@ -419,7 +414,7 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package, .SetId(res_id, OnIdConflict::CREATE_ENTRY) .SetAllowMangled(true); - if (entry->flags & ResTable_entry::FLAG_PUBLIC) { + if (entry->flags() & ResTable_entry::FLAG_PUBLIC) { Visibility visibility{Visibility::Level::kPublic}; auto spec_flags = entry_type_spec_flags_.find(res_id); diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp index 8832c24842ee..9dc205f4c1ba 100644 --- a/tools/aapt2/format/binary/ResEntryWriter.cpp +++ b/tools/aapt2/format/binary/ResEntryWriter.cpp @@ -24,11 +24,6 @@ namespace aapt { -using android::BigBuffer; -using android::Res_value; -using android::ResTable_entry; -using android::ResTable_map; - struct less_style_entries { bool operator()(const Style::Entry* a, const Style::Entry* b) const { if (a->key.id) { @@ -189,26 +184,40 @@ class MapFlattenVisitor : public ConstValueVisitor { }; template <typename T> -void WriteEntry(const FlatEntry* entry, T* out_result) { +void WriteEntry(const FlatEntry* entry, T* out_result, bool compact = false) { static_assert(std::is_same_v<ResTable_entry, T> || std::is_same_v<ResTable_entry_ext, T>, "T must be ResTable_entry or ResTable_entry_ext"); ResTable_entry* out_entry = (ResTable_entry*)out_result; + uint16_t flags = 0; + if (entry->entry->visibility.level == Visibility::Level::kPublic) { - out_entry->flags |= ResTable_entry::FLAG_PUBLIC; + flags |= ResTable_entry::FLAG_PUBLIC; } if (entry->value->IsWeak()) { - out_entry->flags |= ResTable_entry::FLAG_WEAK; + flags |= ResTable_entry::FLAG_WEAK; } if constexpr (std::is_same_v<ResTable_entry_ext, T>) { - out_entry->flags |= ResTable_entry::FLAG_COMPLEX; + flags |= ResTable_entry::FLAG_COMPLEX; } - out_entry->flags = android::util::HostToDevice16(out_entry->flags); - out_entry->key.index = android::util::HostToDevice32(entry->entry_key); - out_entry->size = android::util::HostToDevice16(sizeof(T)); + if (!compact) { + out_entry->full.flags = android::util::HostToDevice16(flags); + out_entry->full.key.index = android::util::HostToDevice32(entry->entry_key); + out_entry->full.size = android::util::HostToDevice16(sizeof(T)); + } else { + Res_value value; + CHECK(entry->entry_key < 0xffffu) << "cannot encode key in 16-bit"; + CHECK(compact && (std::is_same_v<ResTable_entry, T>)) << "cannot encode complex entry"; + CHECK(ValueCast<Item>(entry->value)->Flatten(&value)) << "flatten failed"; + + flags |= ResTable_entry::FLAG_COMPACT | (value.dataType << 8); + out_entry->compact.flags = android::util::HostToDevice16(flags); + out_entry->compact.key = android::util::HostToDevice16(entry->entry_key); + out_entry->compact.data = value.data; + } } int32_t WriteMapToBuffer(const FlatEntry* map_entry, BigBuffer* buffer) { @@ -222,57 +231,26 @@ int32_t WriteMapToBuffer(const FlatEntry* map_entry, BigBuffer* buffer) { return offset; } -void WriteItemToPair(const FlatEntry* item_entry, ResEntryValuePair* out_pair) { - static_assert(sizeof(ResEntryValuePair) == sizeof(ResTable_entry) + sizeof(Res_value), - "ResEntryValuePair must not have padding between entry and value."); - - WriteEntry<ResTable_entry>(item_entry, &out_pair->entry); - - CHECK(ValueCast<Item>(item_entry->value)->Flatten(&out_pair->value)) << "flatten failed"; - out_pair->value.size = android::util::HostToDevice16(sizeof(out_pair->value)); -} - -int32_t SequentialResEntryWriter::WriteMap(const FlatEntry* entry) { - return WriteMapToBuffer(entry, entries_buffer_); -} - -int32_t SequentialResEntryWriter::WriteItem(const FlatEntry* entry) { - int32_t offset = entries_buffer_->size(); - auto* out_pair = entries_buffer_->NextBlock<ResEntryValuePair>(); - WriteItemToPair(entry, out_pair); - return offset; -} - -std::size_t ResEntryValuePairContentHasher::operator()(const ResEntryValuePairRef& ref) const { - return android::JenkinsHashMixBytes(0, ref.ptr, sizeof(ResEntryValuePair)); -} - -bool ResEntryValuePairContentEqualTo::operator()(const ResEntryValuePairRef& a, - const ResEntryValuePairRef& b) const { - return std::memcmp(a.ptr, b.ptr, sizeof(ResEntryValuePair)) == 0; -} +template <bool compact_entry, typename T> +std::pair<int32_t, T*> WriteItemToBuffer(const FlatEntry* item_entry, BigBuffer* buffer) { + int32_t offset = buffer->size(); + T* out_entry = buffer->NextBlock<T>(); -int32_t DeduplicateItemsResEntryWriter::WriteMap(const FlatEntry* entry) { - return WriteMapToBuffer(entry, entries_buffer_); + if constexpr (compact_entry) { + WriteEntry(item_entry, out_entry, true); + } else { + WriteEntry(item_entry, &out_entry->entry); + CHECK(ValueCast<Item>(item_entry->value)->Flatten(&out_entry->value)) << "flatten failed"; + out_entry->value.size = android::util::HostToDevice16(sizeof(out_entry->value)); + } + return {offset, out_entry}; } -int32_t DeduplicateItemsResEntryWriter::WriteItem(const FlatEntry* entry) { - int32_t initial_offset = entries_buffer_->size(); - - auto* out_pair = entries_buffer_->NextBlock<ResEntryValuePair>(); - WriteItemToPair(entry, out_pair); +// explicitly specialize both versions +template std::pair<int32_t, ResEntryValue<false>*> WriteItemToBuffer<false>( + const FlatEntry* item_entry, BigBuffer* buffer); - auto ref = ResEntryValuePairRef{*out_pair}; - auto [it, inserted] = entry_offsets.insert({ref, initial_offset}); - if (inserted) { - // If inserted just return a new offset as this is a first time we store - // this entry. - return initial_offset; - } - // If not inserted this means that this is a duplicate, backup allocated block to the buffer - // and return offset of previously stored entry. - entries_buffer_->BackUp(sizeof(ResEntryValuePair)); - return it->second; -} +template std::pair<int32_t, ResEntryValue<true>*> WriteItemToBuffer<true>( + const FlatEntry* item_entry, BigBuffer* buffer); -} // namespace aapt
\ No newline at end of file +} // namespace aapt diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h index a36ceec2613b..c11598ec12f7 100644 --- a/tools/aapt2/format/binary/ResEntryWriter.h +++ b/tools/aapt2/format/binary/ResEntryWriter.h @@ -27,6 +27,11 @@ namespace aapt { +using android::BigBuffer; +using android::Res_value; +using android::ResTable_entry; +using android::ResTable_map; + struct FlatEntry { const ResourceTableEntryView* entry; const Value* value; @@ -39,28 +44,42 @@ struct FlatEntry { // We introduce this structure for ResEntryWriter to a have single allocation using // BigBuffer::NextBlock which allows to return it back with BigBuffer::Backup. struct ResEntryValuePair { - android::ResTable_entry entry; - android::Res_value value; + ResTable_entry entry; + Res_value value; }; -// References ResEntryValuePair object stored in BigBuffer used as a key in std::unordered_map. -// Allows access to memory address where ResEntryValuePair is stored. -union ResEntryValuePairRef { - const std::reference_wrapper<const ResEntryValuePair> pair; +static_assert(sizeof(ResEntryValuePair) == sizeof(ResTable_entry) + sizeof(Res_value), + "ResEntryValuePair must not have padding between entry and value."); + +template <bool compact> +using ResEntryValue = std::conditional_t<compact, ResTable_entry, ResEntryValuePair>; + +// References ResEntryValue object stored in BigBuffer used as a key in std::unordered_map. +// Allows access to memory address where ResEntryValue is stored. +template <bool compact> +union ResEntryValueRef { + using T = ResEntryValue<compact>; + const std::reference_wrapper<const T> ref; const u_char* ptr; - explicit ResEntryValuePairRef(const ResEntryValuePair& ref) : pair(ref) { + explicit ResEntryValueRef(const T& rev) : ref(rev) { } }; -// Hasher which computes hash of ResEntryValuePair using its bytes representation in memory. -struct ResEntryValuePairContentHasher { - std::size_t operator()(const ResEntryValuePairRef& ref) const; +// Hasher which computes hash of ResEntryValue using its bytes representation in memory. +struct ResEntryValueContentHasher { + template <typename R> + std::size_t operator()(const R& ref) const { + return android::JenkinsHashMixBytes(0, ref.ptr, sizeof(typename R::T)); + } }; // Equaler which compares ResEntryValuePairs using theirs bytes representation in memory. -struct ResEntryValuePairContentEqualTo { - bool operator()(const ResEntryValuePairRef& a, const ResEntryValuePairRef& b) const; +struct ResEntryValueContentEqualTo { + template <typename R> + bool operator()(const R& a, const R& b) const { + return std::memcmp(a.ptr, b.ptr, sizeof(typename R::T)) == 0; + } }; // Base class that allows to write FlatEntries into entries_buffer. @@ -79,9 +98,9 @@ class ResEntryWriter { } protected: - ResEntryWriter(android::BigBuffer* entries_buffer) : entries_buffer_(entries_buffer) { + ResEntryWriter(BigBuffer* entries_buffer) : entries_buffer_(entries_buffer) { } - android::BigBuffer* entries_buffer_; + BigBuffer* entries_buffer_; virtual int32_t WriteItem(const FlatEntry* entry) = 0; @@ -91,18 +110,29 @@ class ResEntryWriter { DISALLOW_COPY_AND_ASSIGN(ResEntryWriter); }; +int32_t WriteMapToBuffer(const FlatEntry* map_entry, BigBuffer* buffer); + +template <bool compact_entry, typename T=ResEntryValue<compact_entry>> +std::pair<int32_t, T*> WriteItemToBuffer(const FlatEntry* item_entry, BigBuffer* buffer); + // ResEntryWriter which writes FlatEntries sequentially into entries_buffer. // Next entry is always written right after previous one in the buffer. +template <bool compact_entry = false> class SequentialResEntryWriter : public ResEntryWriter { public: - explicit SequentialResEntryWriter(android::BigBuffer* entries_buffer) + explicit SequentialResEntryWriter(BigBuffer* entries_buffer) : ResEntryWriter(entries_buffer) { } ~SequentialResEntryWriter() override = default; - int32_t WriteItem(const FlatEntry* entry) override; + int32_t WriteItem(const FlatEntry* entry) override { + auto result = WriteItemToBuffer<compact_entry>(entry, entries_buffer_); + return result.first; + } - int32_t WriteMap(const FlatEntry* entry) override; + int32_t WriteMap(const FlatEntry* entry) override { + return WriteMapToBuffer(entry, entries_buffer_); + } private: DISALLOW_COPY_AND_ASSIGN(SequentialResEntryWriter); @@ -111,25 +141,44 @@ class SequentialResEntryWriter : public ResEntryWriter { // ResEntryWriter that writes only unique entry and value pairs into entries_buffer. // Next entry is written into buffer only if there is no entry with the same bytes representation // in memory written before. Otherwise returns offset of already written entry. +template <bool compact_entry = false> class DeduplicateItemsResEntryWriter : public ResEntryWriter { public: - explicit DeduplicateItemsResEntryWriter(android::BigBuffer* entries_buffer) + explicit DeduplicateItemsResEntryWriter(BigBuffer* entries_buffer) : ResEntryWriter(entries_buffer) { } ~DeduplicateItemsResEntryWriter() override = default; - int32_t WriteItem(const FlatEntry* entry) override; + int32_t WriteItem(const FlatEntry* entry) override { + const auto& [offset, out_entry] = WriteItemToBuffer<compact_entry>(entry, entries_buffer_); + + auto [it, inserted] = entry_offsets.insert({Ref{*out_entry}, offset}); + if (inserted) { + // If inserted just return a new offset as this is a first time we store + // this entry + return offset; + } - int32_t WriteMap(const FlatEntry* entry) override; + // If not inserted this means that this is a duplicate, backup allocated block to the buffer + // and return offset of previously stored entry + entries_buffer_->BackUp(sizeof(*out_entry)); + return it->second; + } + + int32_t WriteMap(const FlatEntry* entry) override { + return WriteMapToBuffer(entry, entries_buffer_); + } private: DISALLOW_COPY_AND_ASSIGN(DeduplicateItemsResEntryWriter); - std::unordered_map<ResEntryValuePairRef, int32_t, ResEntryValuePairContentHasher, - ResEntryValuePairContentEqualTo> - entry_offsets; + using Ref = ResEntryValueRef<compact_entry>; + using Map = std::unordered_map<Ref, int32_t, + ResEntryValueContentHasher, + ResEntryValueContentEqualTo>; + Map entry_offsets; }; } // namespace aapt -#endif
\ No newline at end of file +#endif diff --git a/tools/aapt2/format/binary/ResEntryWriter_test.cpp b/tools/aapt2/format/binary/ResEntryWriter_test.cpp index 56ca1332ec5d..4cb17c33e64a 100644 --- a/tools/aapt2/format/binary/ResEntryWriter_test.cpp +++ b/tools/aapt2/format/binary/ResEntryWriter_test.cpp @@ -56,14 +56,28 @@ TEST_F(SequentialResEntryWriterTest, WriteEntriesOneByOne) { .AddSimple("com.app.test:id/id3", ResourceId(0x7f010002)) .Build(); - BigBuffer out(512); - SequentialResEntryWriter writer(&out); - auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); - - std::vector<int32_t> expected_offsets{0, sizeof(ResEntryValuePair), - 2 * sizeof(ResEntryValuePair)}; - EXPECT_EQ(out.size(), 3 * sizeof(ResEntryValuePair)); - EXPECT_EQ(offsets, expected_offsets); + { + BigBuffer out(512); + SequentialResEntryWriter<false> writer(&out); + auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); + + std::vector<int32_t> expected_offsets{0, sizeof(ResEntryValuePair), + 2 * sizeof(ResEntryValuePair)}; + EXPECT_EQ(out.size(), 3 * sizeof(ResEntryValuePair)); + EXPECT_EQ(offsets, expected_offsets); + } + + { + /* expect a compact entry to only take sizeof(ResTable_entry) */ + BigBuffer out(512); + SequentialResEntryWriter<true> writer(&out); + auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); + + std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry), + 2 * sizeof(ResTable_entry)}; + EXPECT_EQ(out.size(), 3 * sizeof(ResTable_entry)); + EXPECT_EQ(offsets, expected_offsets); + } }; TEST_F(SequentialResEntryWriterTest, WriteMapEntriesOneByOne) { @@ -83,13 +97,26 @@ TEST_F(SequentialResEntryWriterTest, WriteMapEntriesOneByOne) { .AddValue("com.app.test:array/arr2", std::move(array2)) .Build(); - BigBuffer out(512); - SequentialResEntryWriter writer(&out); - auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); + { + BigBuffer out(512); + SequentialResEntryWriter<false> writer(&out); + auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); - std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)}; - EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map))); - EXPECT_EQ(offsets, expected_offsets); + std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)}; + EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map))); + EXPECT_EQ(offsets, expected_offsets); + } + + { + /* compact_entry should have no impact to map items */ + BigBuffer out(512); + SequentialResEntryWriter<true> writer(&out); + auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); + + std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)}; + EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map))); + EXPECT_EQ(offsets, expected_offsets); + } }; TEST_F(DeduplicateItemsResEntryWriterTest, DeduplicateItemEntries) { @@ -100,13 +127,26 @@ TEST_F(DeduplicateItemsResEntryWriterTest, DeduplicateItemEntries) { .AddSimple("com.app.test:id/id3", ResourceId(0x7f010002)) .Build(); - BigBuffer out(512); - DeduplicateItemsResEntryWriter writer(&out); - auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); + { + BigBuffer out(512); + DeduplicateItemsResEntryWriter<false> writer(&out); + auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); - std::vector<int32_t> expected_offsets{0, 0, 0}; - EXPECT_EQ(out.size(), sizeof(ResEntryValuePair)); - EXPECT_EQ(offsets, expected_offsets); + std::vector<int32_t> expected_offsets{0, 0, 0}; + EXPECT_EQ(out.size(), sizeof(ResEntryValuePair)); + EXPECT_EQ(offsets, expected_offsets); + } + + { + /* expect a compact entry to only take sizeof(ResTable_entry) */ + BigBuffer out(512); + DeduplicateItemsResEntryWriter<true> writer(&out); + auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); + + std::vector<int32_t> expected_offsets{0, 0, 0}; + EXPECT_EQ(out.size(), sizeof(ResTable_entry)); + EXPECT_EQ(offsets, expected_offsets); + } }; TEST_F(DeduplicateItemsResEntryWriterTest, WriteMapEntriesOneByOne) { @@ -126,13 +166,26 @@ TEST_F(DeduplicateItemsResEntryWriterTest, WriteMapEntriesOneByOne) { .AddValue("com.app.test:array/arr2", std::move(array2)) .Build(); - BigBuffer out(512); - DeduplicateItemsResEntryWriter writer(&out); - auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); + { + BigBuffer out(512); + DeduplicateItemsResEntryWriter<false> writer(&out); + auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); - std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)}; - EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map))); - EXPECT_EQ(offsets, expected_offsets); -}; + std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)}; + EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map))); + EXPECT_EQ(offsets, expected_offsets); + } + + { + /* compact_entry should have no impact to map items */ + BigBuffer out(512); + DeduplicateItemsResEntryWriter<true> writer(&out); + auto offsets = WriteAllEntries(table->GetPartitionedView(), writer); + + std::vector<int32_t> expected_offsets{0, sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map)}; + EXPECT_EQ(out.size(), 2 * (sizeof(ResTable_entry_ext) + 2 * sizeof(ResTable_map))); + EXPECT_EQ(offsets, expected_offsets); + } + }; } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 318b8b6dcd31..f19223411232 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -16,6 +16,7 @@ #include "format/binary/TableFlattener.h" +#include <limits> #include <sstream> #include <type_traits> #include <variant> @@ -67,7 +68,9 @@ class PackageFlattener { public: PackageFlattener(IAaptContext* context, const ResourceTablePackageView& package, const std::map<size_t, std::string>* shared_libs, - SparseEntriesMode sparse_entries, bool collapse_key_stringpool, + SparseEntriesMode sparse_entries, + bool compact_entries, + bool collapse_key_stringpool, const std::set<ResourceName>& name_collapse_exemptions, bool deduplicate_entry_values) : context_(context), @@ -75,6 +78,7 @@ class PackageFlattener { package_(package), shared_libs_(shared_libs), sparse_entries_(sparse_entries), + compact_entries_(compact_entries), collapse_key_stringpool_(collapse_key_stringpool), name_collapse_exemptions_(name_collapse_exemptions), deduplicate_entry_values_(deduplicate_entry_values) { @@ -135,6 +139,33 @@ class PackageFlattener { private: DISALLOW_COPY_AND_ASSIGN(PackageFlattener); + // Use compact entries only if + // 1) it is enabled, and that + // 2) the entries will be accessed on platforms U+, and + // 3) all entry keys can be encoded in 16 bits + bool UseCompactEntries(const ConfigDescription& config, std::vector<FlatEntry>* entries) const { + return compact_entries_ && + (context_->GetMinSdkVersion() > SDK_TIRAMISU || config.sdkVersion > SDK_TIRAMISU) && + std::none_of(entries->cbegin(), entries->cend(), + [](const auto& e) { return e.entry_key >= std::numeric_limits<uint16_t>::max(); }); + } + + std::unique_ptr<ResEntryWriter> GetResEntryWriter(bool dedup, bool compact, BigBuffer* buffer) { + if (dedup) { + if (compact) { + return std::make_unique<DeduplicateItemsResEntryWriter<true>>(buffer); + } else { + return std::make_unique<DeduplicateItemsResEntryWriter<false>>(buffer); + } + } else { + if (compact) { + return std::make_unique<SequentialResEntryWriter<true>>(buffer); + } else { + return std::make_unique<SequentialResEntryWriter<false>>(buffer); + } + } + } + bool FlattenConfig(const ResourceTableTypeView& type, const ConfigDescription& config, const size_t num_total_entries, std::vector<FlatEntry>* entries, BigBuffer* buffer) { @@ -150,21 +181,20 @@ class PackageFlattener { std::vector<uint32_t> offsets; offsets.resize(num_total_entries, 0xffffffffu); + bool compact_entry = UseCompactEntries(config, entries); + android::BigBuffer values_buffer(512); - std::variant<std::monostate, DeduplicateItemsResEntryWriter, SequentialResEntryWriter> - writer_variant; - ResEntryWriter* res_entry_writer; - if (deduplicate_entry_values_) { - res_entry_writer = &writer_variant.emplace<DeduplicateItemsResEntryWriter>(&values_buffer); - } else { - res_entry_writer = &writer_variant.emplace<SequentialResEntryWriter>(&values_buffer); - } + auto res_entry_writer = GetResEntryWriter(deduplicate_entry_values_, + compact_entry, &values_buffer); for (FlatEntry& flat_entry : *entries) { CHECK(static_cast<size_t>(flat_entry.entry->id.value()) < num_total_entries); offsets[flat_entry.entry->id.value()] = res_entry_writer->Write(&flat_entry); } + // whether the offsets can be represented in 2 bytes + bool short_offsets = (values_buffer.size() / 4u) < std::numeric_limits<uint16_t>::max(); + bool sparse_encode = sparse_entries_ == SparseEntriesMode::Enabled || sparse_entries_ == SparseEntriesMode::Forced; @@ -177,8 +207,7 @@ class PackageFlattener { } // Only sparse encode if the offsets are representable in 2 bytes. - sparse_encode = - sparse_encode && (values_buffer.size() / 4u) <= std::numeric_limits<uint16_t>::max(); + sparse_encode = sparse_encode && short_offsets; // Only sparse encode if the ratio of populated entries to total entries is below some // threshold. @@ -200,12 +229,22 @@ class PackageFlattener { } } else { type_header->entryCount = android::util::HostToDevice32(num_total_entries); - uint32_t* indices = type_writer.NextBlock<uint32_t>(num_total_entries); - for (size_t i = 0; i < num_total_entries; i++) { - indices[i] = android::util::HostToDevice32(offsets[i]); + if (compact_entry && short_offsets) { + // use 16-bit offset only when compact_entry is true + type_header->flags |= ResTable_type::FLAG_OFFSET16; + uint16_t* indices = type_writer.NextBlock<uint16_t>(num_total_entries); + for (size_t i = 0; i < num_total_entries; i++) { + indices[i] = android::util::HostToDevice16(offsets[i] / 4u); + } + } else { + uint32_t* indices = type_writer.NextBlock<uint32_t>(num_total_entries); + for (size_t i = 0; i < num_total_entries; i++) { + indices[i] = android::util::HostToDevice32(offsets[i]); + } } } + type_writer.buffer()->Align4(); type_header->entriesStart = android::util::HostToDevice32(type_writer.size()); type_writer.buffer()->AppendBuffer(std::move(values_buffer)); type_writer.Finish(); @@ -512,6 +551,7 @@ class PackageFlattener { const ResourceTablePackageView package_; const std::map<size_t, std::string>* shared_libs_; SparseEntriesMode sparse_entries_; + bool compact_entries_; android::StringPool type_pool_; android::StringPool key_pool_; bool collapse_key_stringpool_; @@ -568,7 +608,9 @@ bool TableFlattener::Consume(IAaptContext* context, ResourceTable* table) { } PackageFlattener flattener(context, package, &table->included_packages_, - options_.sparse_entries, options_.collapse_key_stringpool, + options_.sparse_entries, + options_.use_compact_entries, + options_.collapse_key_stringpool, options_.name_collapse_exemptions, options_.deduplicate_entry_values); if (!flattener.FlattenPackage(&package_buffer)) { diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 6151b7e33b3f..35254ba24e53 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -44,6 +44,9 @@ struct TableFlattenerOptions { // as a sparse map of entry ID and offset to actual data. SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled; + // When true, use compact entries for simple data + bool use_compact_entries = false; + // When true, the key string pool in the final ResTable // is collapsed to a single entry. All resource entries // have name indices that point to this single value diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt index 4d69d26e46db..741655b402f2 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt @@ -20,6 +20,7 @@ import com.android.tools.lint.client.api.IssueRegistry import com.android.tools.lint.client.api.Vendor import com.android.tools.lint.detector.api.CURRENT_API import com.google.android.lint.aidl.EnforcePermissionDetector +import com.google.android.lint.aidl.EnforcePermissionHelperDetector import com.google.android.lint.aidl.ManualPermissionCheckDetector import com.google.android.lint.parcel.SaferParcelChecker import com.google.auto.service.AutoService @@ -38,6 +39,7 @@ class AndroidFrameworkIssueRegistry : IssueRegistry() { CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED, EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION, + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER, ManualPermissionCheckDetector.ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION, SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS, diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt new file mode 100644 index 000000000000..3c2ea1db0ad6 --- /dev/null +++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt @@ -0,0 +1,130 @@ +/* + * 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.google.android.lint.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiElement +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UDeclarationsExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod + +class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List<Class<out UElement?>> = + listOf(UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) + + private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return + + val targetExpression = "super.${node.name}$HELPER_SUFFIX()" + + val body = node.uastBody as? UBlockExpression + if (body == null) { + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + "Method must start with $targetExpression", + ) + return + } + + val firstExpression = body.expressions.firstOrNull() + if (firstExpression == null) { + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + "Method must start with $targetExpression", + ) + return + } + + val firstExpressionSource = firstExpression.asSourceString() + .filterNot(Char::isWhitespace) + + if (firstExpressionSource != targetExpression) { + val locationTarget = getLocationTarget(firstExpression) + val expressionLocation = context.getLocation(locationTarget) + val indent = " ".repeat(expressionLocation.start?.column ?: 0) + + val fix = fix() + .replace() + .range(expressionLocation) + .beginning() + .with("$targetExpression;\n\n$indent") + .reformat(true) + .autoFix() + .build() + + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + "Method must start with $targetExpression", + fix + ) + } + } + } + + companion object { + private const val HELPER_SUFFIX = "_enforcePermission" + + private const val EXPLANATION = """ + When @EnforcePermission is applied, the AIDL compiler generates a Stub method to do the + permission check called yourMethodName$HELPER_SUFFIX. + + You must call this method as the first expression in your implementation. + """ + + val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create( + id = "MissingEnforcePermissionHelper", + briefDescription = """Missing permission-enforcing method call in AIDL method + |annotated with @EnforcePermission""".trimMargin(), + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionHelperDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + /** + * handles an edge case with UDeclarationsExpression, where sourcePsi is null, + * resulting in an incorrect Location if used directly + */ + private fun getLocationTarget(firstExpression: UExpression): PsiElement? { + if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi + if (firstExpression is UDeclarationsExpression) { + return firstExpression.declarations.firstOrNull()?.sourcePsi + } + return null + } + } +} diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt new file mode 100644 index 000000000000..31e484628a04 --- /dev/null +++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt @@ -0,0 +1,159 @@ +/* +* 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.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask + +class EnforcePermissionHelperDetectorTest : LintDetectorTest() { + override fun getDetector() = EnforcePermissionHelperDetector() + override fun getIssues() = listOf( + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk() + + fun testFirstExpressionIsFunctionCall() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + Binder.getCallingUid(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Autofix for src/Foo.java line 5: Replace with super.test_enforcePermission();...: + @@ -8 +8 + + super.test_enforcePermission(); + + + """ + ) + } + + fun testFirstExpressionIsVariableDeclaration() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + String foo = "bar"; + Binder.getCallingUid(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Autofix for src/Foo.java line 5: Replace with super.test_enforcePermission();...: + @@ -8 +8 + + super.test_enforcePermission(); + + + """ + ) + } + + fun testMethodIsEmpty() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException {} + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testOkay() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + companion object { + val stubs = arrayOf(aidlStub, contextStub, binderStub) + } +} + + + diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt index a968f5e6cb1c..d4a34979ed9f 100644 --- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt +++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt @@ -17,7 +17,6 @@ package com.google.android.lint.aidl import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.checks.infrastructure.TestMode import com.android.tools.lint.detector.api.Detector @@ -361,82 +360,6 @@ class ManualPermissionCheckDetectorTest : LintDetectorTest() { companion object { - private val aidlStub: TestFile = java( - """ - package android.test; - public interface ITest extends android.os.IInterface { - public static abstract class Stub extends android.os.Binder implements android.test.ITest {} - public void test() throws android.os.RemoteException; - } - """ - ).indented() - - private val contextStub: TestFile = java( - """ - package android.content; - public class Context { - @android.content.pm.PermissionMethod - public void enforceCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {} - } - """ - ).indented() - - private val binderStub: TestFile = java( - """ - package android.os; - public class Binder { - public static int getCallingUid() {} - } - """ - ).indented() - - private val permissionMethodStub: TestFile = java( - """ - package android.content.pm; - - import static java.lang.annotation.ElementType.METHOD; - import static java.lang.annotation.RetentionPolicy.CLASS; - - import java.lang.annotation.Retention; - import java.lang.annotation.Target; - - @Retention(CLASS) - @Target({METHOD}) - public @interface PermissionMethod {} - """ - ).indented() - - private val permissionNameStub: TestFile = java( - """ - package android.content.pm; - - import static java.lang.annotation.ElementType.FIELD; - import static java.lang.annotation.ElementType.LOCAL_VARIABLE; - import static java.lang.annotation.ElementType.METHOD; - import static java.lang.annotation.ElementType.PARAMETER; - import static java.lang.annotation.RetentionPolicy.CLASS; - - import java.lang.annotation.Retention; - import java.lang.annotation.Target; - - @Retention(CLASS) - @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) - public @interface PermissionName {} - """ - ).indented() - - private val manifestStub: TestFile = java( - """ - package android; - - public final class Manifest { - public static final class permission { - public static final String READ_CONTACTS="android.permission.READ_CONTACTS"; - } - } - """.trimIndent() - ) - val stubs = arrayOf( aidlStub, contextStub, diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt new file mode 100644 index 000000000000..bd6b1952847c --- /dev/null +++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt @@ -0,0 +1,80 @@ +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import com.android.tools.lint.checks.infrastructure.TestFile + +val aidlStub: TestFile = java( + """ + package android.test; + public interface ITest extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements android.test.ITest {} + public void test() throws android.os.RemoteException; + } + """ +).indented() + +val contextStub: TestFile = java( + """ + package android.content; + public class Context { + @android.content.pm.PermissionMethod + public void enforceCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {} + } + """ +).indented() + +val binderStub: TestFile = java( + """ + package android.os; + public class Binder { + public static int getCallingUid() {} + } + """ +).indented() + +val permissionMethodStub: TestFile = java( +""" + package android.content.pm; + + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({METHOD}) + public @interface PermissionMethod {} + """ +).indented() + +val permissionNameStub: TestFile = java( +""" + package android.content.pm; + + import static java.lang.annotation.ElementType.FIELD; + import static java.lang.annotation.ElementType.LOCAL_VARIABLE; + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.ElementType.PARAMETER; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) + public @interface PermissionName {} + """ +).indented() + +val manifestStub: TestFile = java( + """ + package android; + + public final class Manifest { + public static final class permission { + public static final String READ_CONTACTS="android.permission.READ_CONTACTS"; + } + } + """.trimIndent() +)
\ No newline at end of file |