diff options
97 files changed, 2962 insertions, 702 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 7e142a5ddd7b..83db4cbb7e43 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1433,10 +1433,10 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.d(TAG, "Removing jobs for pkg " + pkgName + " at uid " + pkgUid); } synchronized (mLock) { - // Exclude jobs scheduled on behalf of this app for now because SyncManager + // Exclude jobs scheduled on behalf of this app because SyncManager // and other job proxy agents may not know to reschedule the job properly // after force stop. - // TODO(209852664): determine how to best handle syncs & other proxied jobs + // Proxied jobs will not be allowed to run if the source app is stopped. cancelJobsForPackageAndUidLocked(pkgName, pkgUid, /* includeSchedulingApp */ true, /* includeSourceApp */ false, JobParameters.STOP_REASON_USER, @@ -1448,7 +1448,9 @@ public class JobSchedulerService extends com.android.server.SystemService } }; - private String getPackageName(Intent intent) { + /** Returns the package name stored in the intent's data. */ + @Nullable + public static String getPackageName(Intent intent) { Uri uri = intent.getData(); String pkg = uri != null ? uri.getSchemeSpecificPart() : null; return pkg; diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java index cd3ba6b9e13e..4aadc903ba23 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java @@ -17,18 +17,26 @@ package com.android.server.job.controllers; import static com.android.server.job.JobSchedulerService.NEVER_INDEX; +import static com.android.server.job.JobSchedulerService.getPackageName; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManagerInternal; import android.os.SystemClock; import android.os.UserHandle; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; +import android.util.SparseArrayMap; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.GuardedBy; import com.android.server.AppStateTracker; import com.android.server.AppStateTrackerImpl; import com.android.server.AppStateTrackerImpl.Listener; @@ -50,6 +58,8 @@ import java.util.function.Predicate; * * - the uid-active boolean state expressed by the AppStateTracker. Jobs in 'active' * uids are inherently eligible to run jobs regardless of the uid's standby bucket. + * + * - the app's stopped state */ public final class BackgroundJobsController extends StateController { private static final String TAG = "JobScheduler.Background"; @@ -63,9 +73,48 @@ public final class BackgroundJobsController extends StateController { private final ActivityManagerInternal mActivityManagerInternal; private final AppStateTrackerImpl mAppStateTracker; + private final PackageManagerInternal mPackageManagerInternal; + + @GuardedBy("mLock") + private final SparseArrayMap<String, Boolean> mPackageStoppedState = new SparseArrayMap<>(); private final UpdateJobFunctor mUpdateJobFunctor = new UpdateJobFunctor(); + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String pkgName = getPackageName(intent); + final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); + final String action = intent.getAction(); + if (pkgUid == -1) { + Slog.e(TAG, "Didn't get package UID in intent (" + action + ")"); + return; + } + + if (DEBUG) { + Slog.d(TAG, "Got " + action + " for " + pkgUid + "/" + pkgName); + } + + switch (action) { + case Intent.ACTION_PACKAGE_RESTARTED: { + synchronized (mLock) { + mPackageStoppedState.add(pkgUid, pkgName, Boolean.TRUE); + updateJobRestrictionsForUidLocked(pkgUid, false); + } + } + break; + + case Intent.ACTION_PACKAGE_UNSTOPPED: { + synchronized (mLock) { + mPackageStoppedState.add(pkgUid, pkgName, Boolean.FALSE); + updateJobRestrictionsLocked(pkgUid, UNKNOWN); + } + } + break; + } + } + }; + public BackgroundJobsController(JobSchedulerService service) { super(service); @@ -73,11 +122,18 @@ public final class BackgroundJobsController extends StateController { LocalServices.getService(ActivityManagerInternal.class)); mAppStateTracker = (AppStateTrackerImpl) Objects.requireNonNull( LocalServices.getService(AppStateTracker.class)); + mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); } @Override public void startTrackingLocked() { mAppStateTracker.addListener(mForceAppStandbyListener); + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + filter.addAction(Intent.ACTION_PACKAGE_UNSTOPPED); + filter.addDataScheme("package"); + mContext.registerReceiverAsUser( + mBroadcastReceiver, UserHandle.ALL, filter, null, null); } @Override @@ -99,11 +155,45 @@ public final class BackgroundJobsController extends StateController { } @Override + public void onAppRemovedLocked(String packageName, int uid) { + mPackageStoppedState.delete(uid, packageName); + } + + @Override + public void onUserRemovedLocked(int userId) { + for (int u = mPackageStoppedState.numMaps() - 1; u >= 0; --u) { + final int uid = mPackageStoppedState.keyAt(u); + if (UserHandle.getUserId(uid) == userId) { + mPackageStoppedState.deleteAt(u); + } + } + } + + @Override public void dumpControllerStateLocked(final IndentingPrintWriter pw, final Predicate<JobStatus> predicate) { + pw.println("Aconfig flags:"); + pw.increaseIndent(); + pw.print(android.content.pm.Flags.FLAG_STAY_STOPPED, + android.content.pm.Flags.stayStopped()); + pw.println(); + pw.decreaseIndent(); + pw.println(); + mAppStateTracker.dump(pw); pw.println(); + pw.println("Stopped packages:"); + pw.increaseIndent(); + mPackageStoppedState.forEach((uid, pkgName, isStopped) -> { + pw.print(uid); + pw.print(":"); + pw.print(pkgName); + pw.print("="); + pw.println(isStopped); + }); + pw.println(); + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { final int uid = jobStatus.getSourceUid(); final String sourcePkg = jobStatus.getSourcePackageName(); @@ -205,14 +295,34 @@ public final class BackgroundJobsController extends StateController { } } + private boolean isPackageStopped(String packageName, int uid) { + if (mPackageStoppedState.contains(uid, packageName)) { + return mPackageStoppedState.get(uid, packageName); + } + final boolean isStopped = mPackageManagerInternal.isPackageStopped(packageName, uid); + mPackageStoppedState.add(uid, packageName, isStopped); + return isStopped; + } + boolean updateSingleJobRestrictionLocked(JobStatus jobStatus, final long nowElapsed, int activeState) { final int uid = jobStatus.getSourceUid(); final String packageName = jobStatus.getSourcePackageName(); - final boolean isUserBgRestricted = - !mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled() - && !mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(uid, packageName); + final boolean isSourcePkgStopped = + isPackageStopped(jobStatus.getSourcePackageName(), jobStatus.getSourceUid()); + final boolean isCallingPkgStopped; + if (!jobStatus.isProxyJob()) { + isCallingPkgStopped = isSourcePkgStopped; + } else { + isCallingPkgStopped = + isPackageStopped(jobStatus.getCallingPackageName(), jobStatus.getUid()); + } + final boolean isStopped = android.content.pm.Flags.stayStopped() + && (isCallingPkgStopped || isSourcePkgStopped); + final boolean isUserBgRestricted = isStopped + || (!mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled() + && !mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(uid, packageName)); // If a job started with the foreground flag, it'll cause the UID to stay active // and thus cause areJobsRestricted() to always return false, so if // areJobsRestricted() returns false and the app is BG restricted and not TOP, @@ -233,7 +343,8 @@ public final class BackgroundJobsController extends StateController { && isUserBgRestricted && mService.getUidProcState(uid) > ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE; - final boolean canRun = !shouldStopImmediately + // Don't let jobs (including proxied jobs) run if the app is in the stopped state. + final boolean canRun = !isStopped && !shouldStopImmediately && !mAppStateTracker.areJobsRestricted( uid, packageName, jobStatus.canRunInBatterySaver()); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index b74806494a60..d1f575ef40c8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -1102,6 +1102,12 @@ public final class JobStatus { return job.getService(); } + /** Return the package name of the app that scheduled the job. */ + public String getCallingPackageName() { + return job.getService().getPackageName(); + } + + /** Return the package name of the app on whose behalf the job was scheduled. */ public String getSourcePackageName() { return sourcePackageName; } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 076fddf234b9..98a78cfaa38c 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1162,6 +1162,13 @@ package android.content.pm { field public static final int SHOW_IN_LAUNCHER_WITH_PARENT = 0; // 0x0 } + public static final class UserProperties.Builder { + ctor public UserProperties.Builder(); + method @NonNull public android.content.pm.UserProperties build(); + method @NonNull public android.content.pm.UserProperties.Builder setShowInQuietMode(int); + method @NonNull public android.content.pm.UserProperties.Builder setShowInSharingSurfaces(int); + } + } package android.content.res { diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 5c6c49a18caa..23a5d4d20a2e 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -2805,7 +2805,7 @@ public class Intent implements Parcelable, Cloneable { * and the package in the stopped state cannot self-start for any reason unless there's an * explicit request to start a component in the package. The {@link #ACTION_PACKAGE_UNSTOPPED} * broadcast is sent when such an explicit process start occurs and the package is taken - * out of the stopped state. + * out of the stopped state. The data contains the name of the package. * </p> * <ul> * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java index 445ca0c98416..56e8291f25e9 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -1076,6 +1076,8 @@ public final class UserProperties implements Parcelable { * Intended for building default values (and so all properties are present in the built object). * @hide */ + @TestApi + @SuppressLint("UnflaggedApi") // b/306636213 public static final class Builder { // UserProperties fields and their default values. private @ShowInLauncher int mShowInLauncher = SHOW_IN_LAUNCHER_WITH_PARENT; @@ -1099,54 +1101,82 @@ public final class UserProperties implements Parcelable { private boolean mDeleteAppWithParent = false; private boolean mAlwaysVisible = false; + /** + * @hide + */ + @SuppressLint("UnflaggedApi") // b/306636213 + @TestApi + public Builder() {} + + /** @hide */ public Builder setShowInLauncher(@ShowInLauncher int showInLauncher) { mShowInLauncher = showInLauncher; return this; } + /** @hide */ public Builder setStartWithParent(boolean startWithParent) { mStartWithParent = startWithParent; return this; } - /** Sets the value for {@link #mShowInSettings} */ + /** Sets the value for {@link #mShowInSettings} + * @hide + */ public Builder setShowInSettings(@ShowInSettings int showInSettings) { mShowInSettings = showInSettings; return this; } - /** Sets the value for {@link #mShowInQuietMode} */ + /** Sets the value for {@link #mShowInQuietMode} + * @hide + */ + @TestApi + @SuppressLint("UnflaggedApi") // b/306636213 + @NonNull public Builder setShowInQuietMode(@ShowInQuietMode int showInQuietMode) { mShowInQuietMode = showInQuietMode; return this; } - /** Sets the value for {@link #mShowInSharingSurfaces}. */ + /** Sets the value for {@link #mShowInSharingSurfaces}. + * @hide + */ + @TestApi + @SuppressLint("UnflaggedApi") // b/306636213 + @NonNull public Builder setShowInSharingSurfaces(@ShowInSharingSurfaces int showInSharingSurfaces) { mShowInSharingSurfaces = showInSharingSurfaces; return this; } - /** Sets the value for {@link #mInheritDevicePolicy}*/ + /** Sets the value for {@link #mInheritDevicePolicy} + * @hide + */ public Builder setInheritDevicePolicy( @InheritDevicePolicy int inheritRestrictionsDevicePolicy) { mInheritDevicePolicy = inheritRestrictionsDevicePolicy; return this; } + /** @hide */ public Builder setUseParentsContacts(boolean useParentsContacts) { mUseParentsContacts = useParentsContacts; return this; } - /** Sets the value for {@link #mUpdateCrossProfileIntentFiltersOnOTA} */ + /** Sets the value for {@link #mUpdateCrossProfileIntentFiltersOnOTA} + * @hide + */ public Builder setUpdateCrossProfileIntentFiltersOnOTA(boolean updateCrossProfileIntentFiltersOnOTA) { mUpdateCrossProfileIntentFiltersOnOTA = updateCrossProfileIntentFiltersOnOTA; return this; } - /** Sets the value for {@link #mCrossProfileIntentFilterAccessControl} */ + /** Sets the value for {@link #mCrossProfileIntentFilterAccessControl} + * @hide + */ public Builder setCrossProfileIntentFilterAccessControl( @CrossProfileIntentFilterAccessControlLevel int crossProfileIntentFilterAccessControl) { @@ -1154,24 +1184,30 @@ public final class UserProperties implements Parcelable { return this; } - /** Sets the value for {@link #mCrossProfileIntentResolutionStrategy} */ + /** Sets the value for {@link #mCrossProfileIntentResolutionStrategy} + * @hide + */ public Builder setCrossProfileIntentResolutionStrategy(@CrossProfileIntentResolutionStrategy int crossProfileIntentResolutionStrategy) { mCrossProfileIntentResolutionStrategy = crossProfileIntentResolutionStrategy; return this; } + /** @hide */ public Builder setMediaSharedWithParent(boolean mediaSharedWithParent) { mMediaSharedWithParent = mediaSharedWithParent; return this; } + /** @hide */ public Builder setCredentialShareableWithParent(boolean credentialShareableWithParent) { mCredentialShareableWithParent = credentialShareableWithParent; return this; } - /** Sets the value for {@link #mAuthAlwaysRequiredToDisableQuietMode} */ + /** Sets the value for {@link #mAuthAlwaysRequiredToDisableQuietMode} + * @hide + */ public Builder setAuthAlwaysRequiredToDisableQuietMode( boolean authAlwaysRequiredToDisableQuietMode) { mAuthAlwaysRequiredToDisableQuietMode = @@ -1179,19 +1215,28 @@ public final class UserProperties implements Parcelable { return this; } - /** Sets the value for {@link #mDeleteAppWithParent}*/ + /** Sets the value for {@link #mDeleteAppWithParent} + * @hide + */ public Builder setDeleteAppWithParent(boolean deleteAppWithParent) { mDeleteAppWithParent = deleteAppWithParent; return this; } - /** Sets the value for {@link #mAlwaysVisible}*/ + /** Sets the value for {@link #mAlwaysVisible} + * @hide + */ public Builder setAlwaysVisible(boolean alwaysVisible) { mAlwaysVisible = alwaysVisible; return this; } - /** Builds a UserProperties object with *all* values populated. */ + /** Builds a UserProperties object with *all* values populated. + * @hide + */ + @TestApi + @SuppressLint("UnflaggedApi") // b/306636213 + @NonNull public UserProperties build() { return new UserProperties( mShowInLauncher, diff --git a/core/java/android/security/FileIntegrityManager.java b/core/java/android/security/FileIntegrityManager.java index dae3202b2043..025aac962fb9 100644 --- a/core/java/android/security/FileIntegrityManager.java +++ b/core/java/android/security/FileIntegrityManager.java @@ -53,10 +53,10 @@ public final class FileIntegrityManager { * verification, although the app APIs are only made available to apps in a later SDK version. * Only when this method returns true, the other fs-verity APIs in the same class can succeed. * - * <p>The app may not need this method and just call the other APIs (i.e. {@link - * #setupFsVerity(File)} and {@link #getFsVerityDigest(File)}) normally and handle any failure. - * If some app feature really depends on fs-verity (e.g. protecting integrity of a large file - * download), an early check of support status may avoid any cost if it is to fail late. + * <p>The app may not need this method and just call the other APIs normally and handle any + * failure. If some app feature really depends on fs-verity (e.g. protecting integrity of a + * large file download), an early check of support status may avoid any cost if it is to fail + * late. * * <p>Note: for historical reasons this is named {@code isApkVeritySupported()} instead of * {@code isFsVeritySupported()}. It has also been available since API level 30, predating the diff --git a/core/java/android/view/HdrRenderState.java b/core/java/android/view/HdrRenderState.java new file mode 100644 index 000000000000..2fbbf48dff77 --- /dev/null +++ b/core/java/android/view/HdrRenderState.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.os.SystemClock; + +import com.android.graphics.hwui.flags.Flags; + +import java.util.function.Consumer; + +/** @hide */ +class HdrRenderState implements Consumer<Display> { + // Targeting an animation from 1x to 5x over 400ms means we need to increase by 0.01/ms + private static final float TRANSITION_PER_MS = 0.01f; + + private static final boolean FLAG_ANIMATE_ENABLED = Flags.animateHdrTransitions(); + + private final ViewRootImpl mViewRoot; + + private boolean mIsListenerRegistered = false; + private boolean mUpdateHdrSdrRatioInfo = false; + private float mDesiredHdrSdrRatio = 1f; + private float mTargetHdrSdrRatio = 1f; + private float mRenderHdrSdrRatio = 1f; + private float mPreviousRenderRatio = 1f; + private long mLastUpdateMillis = -1; + + HdrRenderState(ViewRootImpl viewRoot) { + mViewRoot = viewRoot; + } + + @Override + public void accept(Display display) { + forceUpdateHdrSdrRatio(); + mViewRoot.invalidate(); + } + + boolean isHdrEnabled() { + return mDesiredHdrSdrRatio >= 1.01f; + } + + void stopListening() { + if (mIsListenerRegistered) { + mViewRoot.mDisplay.unregisterHdrSdrRatioChangedListener(this); + mIsListenerRegistered = false; + } + } + + void startListening() { + if (isHdrEnabled() && !mIsListenerRegistered && mViewRoot.mDisplay != null) { + mViewRoot.mDisplay.registerHdrSdrRatioChangedListener(mViewRoot.mExecutor, this); + } + } + + /** @return true if something changed, else false */ + boolean updateForFrame(long frameTimeMillis) { + boolean hasUpdate = mUpdateHdrSdrRatioInfo; + mUpdateHdrSdrRatioInfo = false; + mRenderHdrSdrRatio = mTargetHdrSdrRatio; + long timeDelta = Math.max(Math.min(32, frameTimeMillis - mLastUpdateMillis), 8); + final float maxStep = timeDelta * TRANSITION_PER_MS; + mLastUpdateMillis = frameTimeMillis; + if (hasUpdate && FLAG_ANIMATE_ENABLED) { + if (mTargetHdrSdrRatio == 1.0f) { + mPreviousRenderRatio = mTargetHdrSdrRatio; + } else { + float delta = mTargetHdrSdrRatio - mPreviousRenderRatio; + if (delta > maxStep) { + mRenderHdrSdrRatio = mPreviousRenderRatio + maxStep; + mUpdateHdrSdrRatioInfo = true; + mViewRoot.invalidate(); + } + mPreviousRenderRatio = mRenderHdrSdrRatio; + } + } + return hasUpdate; + } + + float getDesiredHdrSdrRatio() { + return mDesiredHdrSdrRatio; + } + + float getRenderHdrSdrRatio() { + return mRenderHdrSdrRatio; + } + + void forceUpdateHdrSdrRatio() { + mTargetHdrSdrRatio = Math.min(mDesiredHdrSdrRatio, mViewRoot.mDisplay.getHdrSdrRatio()); + mUpdateHdrSdrRatioInfo = true; + } + + void setDesiredHdrSdrRatio(float desiredRatio) { + mLastUpdateMillis = SystemClock.uptimeMillis(); + // TODO: When decreasing the desired ratio we need to animate it downwards + if (desiredRatio != mDesiredHdrSdrRatio) { + mDesiredHdrSdrRatio = desiredRatio; + forceUpdateHdrSdrRatio(); + mViewRoot.invalidate(); + + if (isHdrEnabled()) { + startListening(); + } else { + stopListening(); + } + } + } +} diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 870ec4bb6969..1530aa78d73d 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -735,10 +735,7 @@ public final class ViewRootImpl implements ViewParent, private BLASTBufferQueue mBlastBufferQueue; - private boolean mUpdateHdrSdrRatioInfo = false; - private float mDesiredHdrSdrRatio = 1f; - private float mRenderHdrSdrRatio = 1f; - private Consumer<Display> mHdrSdrRatioChangedListener = null; + private final HdrRenderState mHdrRenderState = new HdrRenderState(this); /** * Child container layer of {@code mSurface} with the same bounds as its parent, and cropped to @@ -1813,7 +1810,7 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mThreadedRenderer = renderer; renderer.setSurfaceControl(mSurfaceControl, mBlastBufferQueue); updateColorModeIfNeeded(attrs.getColorMode(), attrs.getDesiredHdrHeadroom()); - updateRenderHdrSdrRatio(); + mHdrRenderState.forceUpdateHdrSdrRatio(); updateForceDarkMode(); mAttachInfo.mHardwareAccelerated = true; mAttachInfo.mHardwareAccelerationRequested = true; @@ -2156,9 +2153,7 @@ public final class ViewRootImpl implements ViewParent, private void updateInternalDisplay(int displayId, Resources resources) { final Display preferredDisplay = ResourcesManager.getInstance().getAdjustedDisplay(displayId, resources); - if (mHdrSdrRatioChangedListener != null && mDisplay != null) { - mDisplay.unregisterHdrSdrRatioChangedListener(mHdrSdrRatioChangedListener); - } + mHdrRenderState.stopListening(); if (preferredDisplay == null) { // Fallback to use default display. Slog.w(TAG, "Cannot get desired display with Id: " + displayId); @@ -2167,9 +2162,7 @@ public final class ViewRootImpl implements ViewParent, } else { mDisplay = preferredDisplay; } - if (mHdrSdrRatioChangedListener != null && mDisplay != null) { - mDisplay.registerHdrSdrRatioChangedListener(mExecutor, mHdrSdrRatioChangedListener); - } + mHdrRenderState.startListening(); mContext.updateDisplay(mDisplay.getDisplayId()); } @@ -5154,11 +5147,12 @@ public final class ViewRootImpl implements ViewParent, useAsyncReport = true; - if (mUpdateHdrSdrRatioInfo) { - mUpdateHdrSdrRatioInfo = false; + if (mHdrRenderState.updateForFrame(mAttachInfo.mDrawingTime)) { + final float renderRatio = mHdrRenderState.getRenderHdrSdrRatio(); applyTransactionOnDraw(mTransaction.setExtendedRangeBrightness( - getSurfaceControl(), mRenderHdrSdrRatio, mDesiredHdrSdrRatio)); - mAttachInfo.mThreadedRenderer.setTargetHdrSdrRatio(mRenderHdrSdrRatio); + getSurfaceControl(), renderRatio, + mHdrRenderState.getDesiredHdrSdrRatio())); + mAttachInfo.mThreadedRenderer.setTargetHdrSdrRatio(renderRatio); } if (activeSyncGroup != null) { @@ -5769,11 +5763,6 @@ public final class ViewRootImpl implements ViewParent, } } - private void updateRenderHdrSdrRatio() { - mRenderHdrSdrRatio = Math.min(mDesiredHdrSdrRatio, mDisplay.getHdrSdrRatio()); - mUpdateHdrSdrRatioInfo = true; - } - private void updateColorModeIfNeeded(@ActivityInfo.ColorMode int colorMode, float desiredRatio) { if (mAttachInfo.mThreadedRenderer == null) { @@ -5793,22 +5782,8 @@ public final class ViewRootImpl implements ViewParent, if (desiredRatio == 0 || desiredRatio > automaticRatio) { desiredRatio = automaticRatio; } - if (desiredRatio != mDesiredHdrSdrRatio) { - mDesiredHdrSdrRatio = desiredRatio; - updateRenderHdrSdrRatio(); - invalidate(); - if (mDesiredHdrSdrRatio < 1.01f) { - mDisplay.unregisterHdrSdrRatioChangedListener(mHdrSdrRatioChangedListener); - mHdrSdrRatioChangedListener = null; - } else { - mHdrSdrRatioChangedListener = display -> { - updateRenderHdrSdrRatio(); - invalidate(); - }; - mDisplay.registerHdrSdrRatioChangedListener(mExecutor, mHdrSdrRatioChangedListener); - } - } + mHdrRenderState.setDesiredHdrSdrRatio(desiredRatio); } @Override @@ -6428,7 +6403,7 @@ public final class ViewRootImpl implements ViewParent, } final ViewRootHandler mHandler = new ViewRootHandler(); - private final Executor mExecutor = (Runnable r) -> { + final Executor mExecutor = (Runnable r) -> { mHandler.post(r); }; @@ -8764,7 +8739,7 @@ public final class ViewRootImpl implements ViewParent, if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl, mBlastBufferQueue); } - updateRenderHdrSdrRatio(); + mHdrRenderState.forceUpdateHdrSdrRatio(); if (mPreviousTransformHint != transformHint) { mPreviousTransformHint = transformHint; dispatchTransformHintChanged(transformHint); @@ -9312,9 +9287,7 @@ public final class ViewRootImpl implements ViewParent, private void destroyHardwareRenderer() { ThreadedRenderer hardwareRenderer = mAttachInfo.mThreadedRenderer; - if (mHdrSdrRatioChangedListener != null) { - mDisplay.unregisterHdrSdrRatioChangedListener(mHdrSdrRatioChangedListener); - } + mHdrRenderState.stopListening(); if (hardwareRenderer != null) { if (mHardwareRendererObserver != null) { diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 3dbe65ef4180..a38092a21178 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -185,16 +185,31 @@ public final class AccessibilityManager { /** * Annotations for the shortcut type. + * <p>Note: Keep in sync with {@link #SHORTCUT_TYPES}.</p> * @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = { + // LINT.IfChange(shortcut_type_intdef) ACCESSIBILITY_BUTTON, ACCESSIBILITY_SHORTCUT_KEY + // LINT.ThenChange(:shortcut_type_array) }) public @interface ShortcutType {} /** + * Used for iterating through {@link ShortcutType}. + * <p>Note: Keep in sync with {@link ShortcutType}.</p> + * @hide + */ + public static final int[] SHORTCUT_TYPES = { + // LINT.IfChange(shortcut_type_array) + ACCESSIBILITY_BUTTON, + ACCESSIBILITY_SHORTCUT_KEY, + // LINT.ThenChange(:shortcut_type_intdef) + }; + + /** * Annotations for content flag of UI. * @hide */ @@ -914,6 +929,28 @@ public final class AccessibilityManager { } /** + * Returns whether the user must be shown the AccessibilityService warning dialog + * before the AccessibilityService (or any shortcut for the service) can be enabled. + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY) + public boolean isAccessibilityServiceWarningRequired(@NonNull AccessibilityServiceInfo info) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return true; + } + } + try { + return service.isAccessibilityServiceWarningRequired(info); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while checking isAccessibilityServiceWarningRequired: ", re); + return true; + } + } + + /** * Registers an {@link AccessibilityStateChangeListener} for changes in * the global accessibility state of the system. Equivalent to calling * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)} diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index f741080c57a8..9c04c27d189a 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -128,6 +128,9 @@ interface IAccessibilityManager { boolean isAccessibilityTargetAllowed(String packageName, int uid, int userId); boolean sendRestrictedDialogIntent(String packageName, int uid, int userId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)") + boolean isAccessibilityServiceWarningRequired(in AccessibilityServiceInfo info); + parcelable WindowTransformationSpec { float[] transformationMatrix; MagnificationSpec magnificationSpec; diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index b29967888312..75ea08e5450b 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -17,17 +17,17 @@ flag { } flag { + name: "cleanup_accessibility_warning_dialog" namespace: "accessibility" - name: "collection_info_item_counts" - description: "Fields for total items and the number of important for accessibility items in a collection" - bug: "302376158" + description: "Cleans up duplicated or broken logic surrounding the accessibility warning dialog." + bug: "303511250" } flag { - name: "deduplicate_accessibility_warning_dialog" namespace: "accessibility" - description: "Removes duplicate definition of the accessibility warning dialog." - bug: "303511250" + name: "collection_info_item_counts" + description: "Fields for total items and the number of important for accessibility items in a collection" + bug: "302376158" } flag { diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index 14ec14bf7cfc..966161fd642a 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -31,6 +31,7 @@ import static android.view.contentcapture.ContentCaptureHelper.getSanitizedStrin import static android.view.contentcapture.ContentCaptureHelper.sDebug; import static android.view.contentcapture.ContentCaptureHelper.sVerbose; import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE; +import static android.view.contentcapture.flags.Flags.runOnBackgroundThreadEnabled; import android.annotation.NonNull; import android.annotation.Nullable; @@ -209,14 +210,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession { binder = resultData.getBinder(EXTRA_BINDER); if (binder == null) { Log.wtf(TAG, "No " + EXTRA_BINDER + " extra result"); - mainSession.mHandler.post(() -> mainSession.resetSession( + mainSession.runOnContentCaptureThread(() -> mainSession.resetSession( STATE_DISABLED | STATE_INTERNAL_ERROR)); return; } } else { binder = null; } - mainSession.mHandler.post(() -> + mainSession.runOnContentCaptureThread(() -> mainSession.onSessionStarted(resultCode, binder)); } } @@ -256,7 +257,13 @@ public final class MainContentCaptureSession extends ContentCaptureSession { */ void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags) { - runOnContentCaptureThread(() -> startImpl(token, shareableActivityToken, component, flags)); + if (runOnBackgroundThreadEnabled()) { + runOnContentCaptureThread( + () -> startImpl(token, shareableActivityToken, component, flags)); + } else { + // Preserve the control arm behaviour. + startImpl(token, shareableActivityToken, component, flags); + } } private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, @@ -613,7 +620,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession { @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @Override public void flush(@FlushReason int reason) { - runOnContentCaptureThread(() -> flushImpl(reason)); + if (runOnBackgroundThreadEnabled()) { + runOnContentCaptureThread(() -> flushImpl(reason)); + } else { + // Preserve the control arm behaviour. + flushImpl(reason); + } } private void flushImpl(@FlushReason int reason) { @@ -904,7 +916,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** public because is also used by ViewRootImpl */ public void notifyContentCaptureEvents( @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) { - runOnContentCaptureThread(() -> notifyContentCaptureEventsImpl(contentCaptureEvents)); + if (runOnBackgroundThreadEnabled()) { + runOnContentCaptureThread(() -> notifyContentCaptureEventsImpl(contentCaptureEvents)); + } else { + // Preserve the control arm behaviour. + notifyContentCaptureEventsImpl(contentCaptureEvents); + } } private void notifyContentCaptureEventsImpl( @@ -1076,19 +1093,30 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * </p> */ private void runOnContentCaptureThread(@NonNull Runnable r) { - if (!mHandler.getLooper().isCurrentThread()) { - mHandler.post(r); + if (runOnBackgroundThreadEnabled()) { + if (!mHandler.getLooper().isCurrentThread()) { + mHandler.post(r); + } else { + r.run(); + } } else { - r.run(); + // Preserve the control arm behaviour to always post to the handler. + mHandler.post(r); } } private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) { - if (!mHandler.getLooper().isCurrentThread()) { + if (runOnBackgroundThreadEnabled()) { + if (!mHandler.getLooper().isCurrentThread()) { + mHandler.removeMessages(what); + mHandler.post(r); + } else { + r.run(); + } + } else { + // Preserve the control arm behaviour to always post to the handler. mHandler.removeMessages(what); mHandler.post(r); - } else { - r.run(); } } } diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java index d4eccd458e35..7d06e3f5a7bc 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java @@ -37,6 +37,7 @@ import android.os.Bundle; import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.Flags; import android.widget.AdapterView; @@ -114,18 +115,39 @@ public class AccessibilityShortcutChooserActivity extends Activity { private void onTargetChecked(AdapterView<?> parent, View view, int position, long id) { final AccessibilityTarget target = mTargets.get(position); - if (!target.isShortcutEnabled()) { - if (target instanceof AccessibilityServiceTarget - || target instanceof AccessibilityActivityTarget) { + if (Flags.cleanupAccessibilityWarningDialog()) { + if (target instanceof AccessibilityServiceTarget serviceTarget) { if (sendRestrictedDialogIntentIfNeeded(target)) { return; } + final AccessibilityManager am = getSystemService(AccessibilityManager.class); + if (am.isAccessibilityServiceWarningRequired( + serviceTarget.getAccessibilityServiceInfo())) { + showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, + position, mTargetAdapter); + return; + } + } + if (target instanceof AccessibilityActivityTarget activityTarget) { + if (!activityTarget.isShortcutEnabled() + && sendRestrictedDialogIntentIfNeeded(activityTarget)) { + return; + } } + } else { + if (!target.isShortcutEnabled()) { + if (target instanceof AccessibilityServiceTarget + || target instanceof AccessibilityActivityTarget) { + if (sendRestrictedDialogIntentIfNeeded(target)) { + return; + } + } - if (target instanceof AccessibilityServiceTarget) { - showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, - position, mTargetAdapter); - return; + if (target instanceof AccessibilityServiceTarget) { + showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, + position, mTargetAdapter); + return; + } } } @@ -156,7 +178,7 @@ public class AccessibilityShortcutChooserActivity extends Activity { return; } - if (Flags.deduplicateAccessibilityWarningDialog()) { + if (Flags.cleanupAccessibilityWarningDialog()) { mPermissionDialog = AccessibilityServiceWarning .createAccessibilityServiceWarningDialog(context, serviceTarget.getAccessibilityServiceInfo(), diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java index 088b57feb200..6374e5df3307 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java @@ -18,7 +18,6 @@ package com.android.internal.accessibility; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.action.ViewActions.doubleClick; import static androidx.test.espresso.action.ViewActions.scrollTo; import static androidx.test.espresso.action.ViewActions.swipeUp; import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; @@ -85,10 +84,13 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; /** * Tests for {@link AccessibilityShortcutChooserActivity}. @@ -151,6 +153,8 @@ public class AccessibilityShortcutChooserActivityTest { when(mAccessibilityManagerService.getInstalledAccessibilityServiceList( anyInt())).thenReturn(new ParceledListSlice<>( Collections.singletonList(mAccessibilityServiceInfo))); + when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) + .thenReturn(true); when(mAccessibilityManagerService.isAccessibilityTargetAllowed( anyString(), anyInt(), anyInt())).thenReturn(true); when(mKeyguardManager.isKeyguardLocked()).thenReturn(false); @@ -169,7 +173,7 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG) + @RequiresFlagsDisabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_oldPermissionDialog_deny_dialogIsHidden() { launchActivity(); openShortcutsList(); @@ -183,7 +187,7 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG) + @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_allow_rowChecked() { launchActivity(); openShortcutsList(); @@ -197,7 +201,7 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG) + @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_deny_rowNotChecked() { launchActivity(); openShortcutsList(); @@ -211,7 +215,7 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG) + @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved() { launchActivity(); openShortcutsList(); @@ -227,6 +231,59 @@ public class AccessibilityShortcutChooserActivityTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) + public void selectTestService_permissionDialog_notShownWhenNotRequired() throws Exception { + when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) + .thenReturn(false); + launchActivity(); + openShortcutsList(); + + // Clicking the test service should not show a permission dialog window, + assertThat(mDevice.findObject(By.text(TEST_LABEL)).clickAndWait( + Until.newWindow(), UI_TIMEOUT_MS)).isFalse(); + // and should become checked. + assertThat(mDevice.findObject(By.checked(true))).isNotNull(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) + public void selectTestService_notPermittedByAdmin_blockedEvenIfNoWarningRequired() + throws Exception { + when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) + .thenReturn(false); + when(mAccessibilityManagerService.isAccessibilityTargetAllowed( + eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt())).thenReturn(false); + // This test class mocks AccessibilityManagerService, so the restricted dialog window + // will not actually appear and therefore cannot be used for a wait Until.newWindow(). + // To still allow smart waiting in this test we can instead set up the mocked method + // to update an atomic boolean and wait for that to be set. + final Object waitObject = new Object(); + final AtomicBoolean calledSendRestrictedDialogIntent = new AtomicBoolean(false); + Mockito.doAnswer((Answer<Void>) invocation -> { + synchronized (waitObject) { + calledSendRestrictedDialogIntent.set(true); + waitObject.notify(); + } + return null; + }).when(mAccessibilityManagerService).sendRestrictedDialogIntent( + eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt()); + launchActivity(); + openShortcutsList(); + + mDevice.findObject(By.text(TEST_LABEL)).click(); + final long timeout = System.currentTimeMillis() + UI_TIMEOUT_MS; + synchronized (waitObject) { + while (!calledSendRestrictedDialogIntent.get() && + (System.currentTimeMillis() < timeout)) { + waitObject.wait(timeout - System.currentTimeMillis()); + } + } + + assertThat(calledSendRestrictedDialogIntent.get()).isTrue(); + assertThat(mDevice.findObject(By.checked(true))).isNull(); + } + + @Test public void clickServiceTarget_notPermittedByAdmin_sendRestrictedDialogIntent() throws Exception { when(mAccessibilityManagerService.isAccessibilityTargetAllowed( @@ -329,7 +386,7 @@ public class AccessibilityShortcutChooserActivityTest { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (Flags.deduplicateAccessibilityWarningDialog()) { + if (Flags.cleanupAccessibilityWarningDialog()) { // Setting the Theme is necessary here for the dialog to use the proper style // resources as designated in its layout XML. setTheme(R.style.Theme_DeviceDefault_DayNight); diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java index b76dd51d3f2b..24aab6192c50 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java @@ -58,7 +58,7 @@ import java.util.concurrent.atomic.AtomicBoolean; @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @RequiresFlagsEnabled( - android.view.accessibility.Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG) + android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public class AccessibilityServiceWarningTest { private static final String A11Y_SERVICE_PACKAGE_LABEL = "TestA11yService"; private static final String A11Y_SERVICE_SUMMARY = "TestA11yService summary"; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index d5fab441cd46..fe4980a9eb16 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -554,6 +554,11 @@ public class PipTransition extends PipTransitionController { } } } + // if overlay is present remove it immediately, as exit transition came before it faded out + if (mPipOrganizer.mSwipePipToHomeOverlay != null) { + startTransaction.remove(mPipOrganizer.mSwipePipToHomeOverlay); + clearSwipePipToHomeOverlay(); + } if (pipChange == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: No window of exiting PIP is found. Can't play expand animation", TAG); @@ -1007,7 +1012,6 @@ public class PipTransition extends PipTransitionController { // the overlay to the final PIP task. startTransaction.reparent(swipePipToHomeOverlay, leash) .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE); - mPipOrganizer.mSwipePipToHomeOverlay = null; } final Rect sourceBounds = pipTaskInfo.configuration.windowConfiguration.getBounds(); @@ -1029,7 +1033,7 @@ public class PipTransition extends PipTransitionController { sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); if (swipePipToHomeOverlay != null) { mPipOrganizer.fadeOutAndRemoveOverlay(swipePipToHomeOverlay, - null /* callback */, false /* withStartDelay */); + this::clearSwipePipToHomeOverlay /* callback */, false /* withStartDelay */); } mPipTransitionState.setInSwipePipToHomeTransition(false); } @@ -1173,6 +1177,10 @@ public class PipTransition extends PipTransitionController { mPipMenuController.updateMenuBounds(destinationBounds); } + private void clearSwipePipToHomeOverlay() { + mPipOrganizer.mSwipePipToHomeOverlay = null; + } + @Override public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt index d7b306c3be23..03170a326890 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt @@ -57,10 +57,13 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) + + tapl.enableBlockTimeout(true) } @Test open fun enterSplitScreenByDragFromAllApps() { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .openAllApps() .getAppIcon(secondaryApp.appName) @@ -72,5 +75,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun teardown() { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) + tapl.enableBlockTimeout(false) } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt index 8134fddd40e5..479d01ddaeb9 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt @@ -59,10 +59,13 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) + + tapl.enableBlockTimeout(true) } @Test open fun enterSplitScreenByDragFromShortcut() { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .getAppIcon(secondaryApp.appName) .openDeepShortcutMenu() @@ -83,6 +86,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun teardwon() { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) + tapl.enableBlockTimeout(false) } companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt index 3417744f13a5..625c56bc4a4c 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt @@ -54,6 +54,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) + tapl.enableBlockTimeout(true) + tapl.goHome() SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) primaryApp.launchViaIntent(wmHelper) @@ -61,6 +63,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @Test open fun enterSplitScreenByDragFromTaskbar() { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .getAppIcon(secondaryApp.appName) .dragToSplitscreen(secondaryApp.packageName, primaryApp.packageName) @@ -71,6 +74,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun teardown() { primaryApp.exit(wmHelper) secondaryApp.exit(wmHelper) + tapl.enableBlockTimeout(false) } companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt index 394864ad9d4d..5c43cbdb3832 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt @@ -23,6 +23,7 @@ import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -42,8 +43,10 @@ abstract class EnterSplitScreenByDragFromAllAppsBenchmark(override val flicker: setup { tapl.goHome() primaryApp.launchViaIntent(wmHelper) + tapl.enableBlockTimeout(true) } transitions { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .openAllApps() .getAppIcon(secondaryApp.appName) @@ -57,6 +60,11 @@ abstract class EnterSplitScreenByDragFromAllAppsBenchmark(override val flicker: Assume.assumeTrue(tapl.isTablet) } + @After + fun after() { + tapl.enableBlockTimeout(false) + } + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt index 3b3be84f9841..15ad0c12c49a 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt @@ -23,6 +23,7 @@ import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -42,13 +43,20 @@ abstract class EnterSplitScreenByDragFromShortcutBenchmark( Assume.assumeTrue(tapl.isTablet) } + @After + fun after() { + tapl.enableBlockTimeout(false) + } + protected val thisTransition: FlickerBuilder.() -> Unit = { setup { tapl.goHome() SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName) primaryApp.launchViaIntent(wmHelper) + tapl.enableBlockTimeout(true) } transitions { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .getAppIcon(secondaryApp.appName) .openDeepShortcutMenu() diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt index eff355987cc0..ca8adb1fcb38 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt @@ -23,6 +23,7 @@ import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After import org.junit.Assume import org.junit.Before import org.junit.FixMethodOrder @@ -44,6 +45,7 @@ abstract class EnterSplitScreenByDragFromTaskbarBenchmark(override val flicker: primaryApp.launchViaIntent(wmHelper) } transitions { + tapl.showTaskbarIfHidden() tapl.launchedAppState.taskbar .getAppIcon(secondaryApp.appName) .dragToSplitscreen(secondaryApp.packageName, primaryApp.packageName) @@ -54,6 +56,12 @@ abstract class EnterSplitScreenByDragFromTaskbarBenchmark(override val flicker: @Before fun before() { Assume.assumeTrue(tapl.isTablet) + tapl.enableBlockTimeout(true) + } + + @After + fun after() { + tapl.enableBlockTimeout(false) } companion object { diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 78a64795967a..ca119757e816 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -1,6 +1,13 @@ package: "com.android.graphics.hwui.flags" flag { + name: "matrix_44" + namespace: "core_graphics" + description: "API for 4x4 matrix and related canvas functions" + bug: "280116960" +} + +flag { name: "limited_hdr" namespace: "core_graphics" description: "API to enable apps to restrict the amount of HDR headroom that is used" @@ -41,3 +48,10 @@ flag { description: "Enable r_8, r_16_uint, rg_1616_uint, and rgba_10101010 in the SDK" bug: "292545615" } + +flag { + name: "animate_hdr_transitions" + namespace: "core_graphics" + description: "Automatically animate all changes in HDR headroom" + bug: "314810174" +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt index a5998faa68ad..db69b8bdf42b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt @@ -60,14 +60,15 @@ fun ModalBottomSheet( sheetContent = sheetContent, sheetShape = EntryShape.TopRoundedCorner, ) {} - LaunchedEffect(state.currentValue) { + LaunchedEffect(state.currentValue, state.targetValue) { if (state.currentValue == ModalBottomSheetValue.Hidden) { if (isInitialRender) { onInitialRenderComplete() scope.launch { state.show() } - } else { + } else if (state.targetValue == ModalBottomSheetValue.Hidden) { + // Only dismiss ui when the motion is downwards onDismiss() } } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt index 658b45f68c92..2986504b7dbc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt @@ -17,11 +17,13 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import com.android.compose.nestedscroll.PriorityNestedScrollConnection /** * Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled. @@ -32,8 +34,9 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll */ enum class NestedScrollBehavior(val canStartOnPostFling: Boolean) { /** - * During scene transitions, scroll events are consumed by the [SceneTransitionLayout] instead - * of the scrollable component. + * During scene transitions, if we are within + * [SceneTransitionLayoutImpl.transitionInterceptionThreshold], the [SceneTransitionLayout] + * consumes scroll events instead of the scrollable component. */ DuringTransitionBetweenScenes(canStartOnPostFling = false), @@ -72,21 +75,101 @@ internal fun Modifier.nestedScrollToScene( orientation: Orientation, startBehavior: NestedScrollBehavior, endBehavior: NestedScrollBehavior, -): Modifier = composed { - val connection = - remember(layoutImpl, orientation, startBehavior, endBehavior) { +) = + this then + NestedScrollToSceneElement( + layoutImpl = layoutImpl, + orientation = orientation, + startBehavior = startBehavior, + endBehavior = endBehavior, + ) + +private data class NestedScrollToSceneElement( + private val layoutImpl: SceneTransitionLayoutImpl, + private val orientation: Orientation, + private val startBehavior: NestedScrollBehavior, + private val endBehavior: NestedScrollBehavior, +) : ModifierNodeElement<NestedScrollToSceneNode>() { + override fun create() = + NestedScrollToSceneNode( + layoutImpl = layoutImpl, + orientation = orientation, + startBehavior = startBehavior, + endBehavior = endBehavior, + ) + + override fun update(node: NestedScrollToSceneNode) { + node.update( + layoutImpl = layoutImpl, + orientation = orientation, + startBehavior = startBehavior, + endBehavior = endBehavior, + ) + } + + override fun InspectorInfo.inspectableProperties() { + name = "nestedScrollToScene" + properties["layoutImpl"] = layoutImpl + properties["orientation"] = orientation + properties["startBehavior"] = startBehavior + properties["endBehavior"] = endBehavior + } +} + +private class NestedScrollToSceneNode( + layoutImpl: SceneTransitionLayoutImpl, + orientation: Orientation, + startBehavior: NestedScrollBehavior, + endBehavior: NestedScrollBehavior, +) : DelegatingNode() { + private var priorityNestedScrollConnection: PriorityNestedScrollConnection = + scenePriorityNestedScrollConnection( + layoutImpl = layoutImpl, + orientation = orientation, + startBehavior = startBehavior, + endBehavior = endBehavior, + ) + + private var nestedScrollNode: DelegatableNode = + nestedScrollModifierNode( + connection = priorityNestedScrollConnection, + dispatcher = null, + ) + + override fun onAttach() { + delegate(nestedScrollNode) + } + + override fun onDetach() { + // Make sure we reset the scroll connection when this modifier is removed from composition + priorityNestedScrollConnection.reset() + } + + fun update( + layoutImpl: SceneTransitionLayoutImpl, + orientation: Orientation, + startBehavior: NestedScrollBehavior, + endBehavior: NestedScrollBehavior, + ) { + // Clean up the old nested scroll connection + priorityNestedScrollConnection.reset() + undelegate(nestedScrollNode) + + // Create a new nested scroll connection + priorityNestedScrollConnection = scenePriorityNestedScrollConnection( layoutImpl = layoutImpl, orientation = orientation, startBehavior = startBehavior, - endBehavior = endBehavior + endBehavior = endBehavior, ) - } - - // Make sure we reset the scroll connection when this modifier is removed from composition - DisposableEffect(connection) { onDispose { connection.reset() } } - - nestedScroll(connection = connection) + nestedScrollNode = + nestedScrollModifierNode( + connection = priorityNestedScrollConnection, + dispatcher = null, + ) + delegate(nestedScrollNode) + } } private fun scenePriorityNestedScrollConnection( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index ea3006f0b502..8896e6e64bd9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -39,6 +39,7 @@ import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import javax.inject.Provider import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before @@ -83,7 +84,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { underTest = CommunalEditModeViewModel( withDeps.communalInteractor, - shadeViewController, + Provider { shadeViewController }, powerManager, mediaHost, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 9bd083501780..7fbcae0d8986 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -39,6 +39,7 @@ import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import javax.inject.Provider import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before @@ -84,7 +85,7 @@ class CommunalViewModelTest : SysuiTestCase() { CommunalViewModel( withDeps.communalInteractor, withDeps.tutorialInteractor, - shadeViewController, + Provider { shadeViewController }, powerManager, mediaHost, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt new file mode 100644 index 000000000000..7497ebdc2978 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.uimodenight.domain + +import android.app.UiModeManager +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.os.UserHandle +import android.testing.LeakCheck +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileDataInteractor +import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel +import com.android.systemui.statusbar.phone.ConfigurationControllerImpl +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.DateFormatUtil +import com.android.systemui.utils.leaks.FakeBatteryController +import com.android.systemui.utils.leaks.FakeLocationController +import com.google.common.truth.Truth.assertThat +import java.time.LocalTime +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class UiModeNightTileDataInteractorTest : SysuiTestCase() { + private val configurationController: ConfigurationController = + ConfigurationControllerImpl(context) + private val batteryController = FakeBatteryController(LeakCheck()) + private val locationController = FakeLocationController(LeakCheck()) + + private lateinit var underTest: UiModeNightTileDataInteractor + + @Mock private lateinit var uiModeManager: UiModeManager + @Mock private lateinit var dateFormatUtil: DateFormatUtil + + @Before + fun setup() { + uiModeManager = mock<UiModeManager>() + dateFormatUtil = mock<DateFormatUtil>() + + whenever(uiModeManager.customNightModeStart).thenReturn(LocalTime.MIN) + whenever(uiModeManager.customNightModeEnd).thenReturn(LocalTime.MAX) + + underTest = + UiModeNightTileDataInteractor( + context, + configurationController, + uiModeManager, + batteryController, + locationController, + dateFormatUtil + ) + } + + @Test + fun collectTileDataReadsUiModeManagerNightMode() = runTest { + val expectedNightMode = Configuration.UI_MODE_NIGHT_UNDEFINED + whenever(uiModeManager.nightMode).thenReturn(expectedNightMode) + + val model by + collectLastValue( + underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)) + ) + runCurrent() + + assertThat(model).isNotNull() + val actualNightMode = model?.uiMode + assertThat(actualNightMode).isEqualTo(expectedNightMode) + } + + @Test + fun collectTileDataReadsUiModeManagerNightModeCustomTypeAndTimes() = runTest { + collectLastValue(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))) + + runCurrent() + + verify(uiModeManager).nightMode + verify(uiModeManager).nightModeCustomType + verify(uiModeManager).customNightModeStart + verify(uiModeManager).customNightModeEnd + } + + /** Here, available refers to the tile showing up, not the tile being clickable. */ + @Test + fun isAvailableRegardlessOfPowerSaveModeOn() = runTest { + batteryController.setPowerSaveMode(true) + + runCurrent() + val availability by collectLastValue(underTest.availability(TEST_USER)) + + assertThat(availability).isTrue() + } + + @Test + fun dataMatchesConfigurationController() = runTest { + setUiMode(UI_MODE_NIGHT_NO) + val flowValues: List<UiModeNightTileModel> by + collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))) + + runCurrent() + setUiMode(UI_MODE_NIGHT_YES) + runCurrent() + setUiMode(UI_MODE_NIGHT_NO) + runCurrent() + + assertThat(flowValues.size).isEqualTo(3) + assertThat(flowValues.map { it.isNightMode }).containsExactly(false, true, false).inOrder() + } + + @Test + fun dataMatchesBatteryController() = runTest { + batteryController.setPowerSaveMode(false) + val flowValues: List<UiModeNightTileModel> by + collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))) + + runCurrent() + batteryController.setPowerSaveMode(true) + runCurrent() + batteryController.setPowerSaveMode(false) + runCurrent() + + assertThat(flowValues.size).isEqualTo(3) + assertThat(flowValues.map { it.isPowerSave }).containsExactly(false, true, false).inOrder() + } + + @Test + fun dataMatchesLocationController() = runTest { + locationController.setLocationEnabled(false) + val flowValues: List<UiModeNightTileModel> by + collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))) + + runCurrent() + locationController.setLocationEnabled(true) + runCurrent() + locationController.setLocationEnabled(false) + runCurrent() + + assertThat(flowValues.size).isEqualTo(3) + assertThat(flowValues.map { it.isLocationEnabled }) + .containsExactly(false, true, false) + .inOrder() + } + + @Test + fun collectTileDataReads24HourFormatFromDateTimeUtil() = runTest { + collectLastValue(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))) + runCurrent() + + verify(dateFormatUtil).is24HourFormat + } + + /** + * Use this method to trigger [ConfigurationController.ConfigurationListener.onUiModeChanged] + */ + private fun setUiMode(uiMode: Int) { + val config = context.resources.configuration + val newConfig = Configuration(config) + newConfig.uiMode = uiMode + + /** [underTest] will see this config the next time it creates a model */ + context.orCreateTestableResources.overrideConfiguration(newConfig) + + /** Trigger updateUiMode callbacks */ + configurationController.onConfigurationChanged(newConfig) + } + + private companion object { + val TEST_USER = UserHandle.of(1)!! + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt new file mode 100644 index 000000000000..87f50090e58b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt @@ -0,0 +1,481 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.uimodenight.domain + +import android.app.UiModeManager +import android.text.TextUtils +import android.view.View +import android.widget.Switch +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject +import com.android.systemui.qs.tiles.impl.uimodenight.UiModeNightTileModelHelper.createModel +import com.android.systemui.qs.tiles.impl.uimodenight.qsUiModeNightTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import kotlin.reflect.KClass +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class UiModeNightTileMapperTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val qsTileConfig = kosmos.qsUiModeNightTileConfig + + private val mapper by lazy { + UiModeNightTileMapper(context.orCreateTestableResources.resources) + } + + private fun createUiNightModeTileState( + iconRes: Int = R.drawable.qs_light_dark_theme_icon_off, + label: CharSequence = context.getString(R.string.quick_settings_ui_mode_night_label), + activationState: QSTileState.ActivationState = QSTileState.ActivationState.INACTIVE, + secondaryLabel: CharSequence? = null, + supportedActions: Set<QSTileState.UserAction> = + if (activationState == QSTileState.ActivationState.UNAVAILABLE) + setOf(QSTileState.UserAction.LONG_CLICK) + else setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), + contentDescription: CharSequence? = null, + stateDescription: CharSequence? = null, + sideViewIcon: QSTileState.SideViewIcon = QSTileState.SideViewIcon.None, + enabledState: QSTileState.EnabledState = QSTileState.EnabledState.ENABLED, + expandedAccessibilityClass: KClass<out View>? = Switch::class, + ): QSTileState { + return QSTileState( + { Icon.Resource(iconRes, null) }, + label, + activationState, + secondaryLabel, + supportedActions, + contentDescription, + stateDescription, + sideViewIcon, + enabledState, + expandedAccessibilityClass?.qualifiedName + ) + } + + @Test + fun mapsEnabledDataToUnavailableStateWhenOnPowerSave() { + val inputModel = createModel(nightMode = true, powerSave = true) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedSecondaryLabel = + context.getString(R.string.quick_settings_dark_mode_secondary_label_battery_saver) + val expectedContentDescription = + TextUtils.concat(expectedLabel, ", ", expectedSecondaryLabel) + val expectedState = + createUiNightModeTileState( + activationState = QSTileState.ActivationState.UNAVAILABLE, + secondaryLabel = expectedSecondaryLabel, + contentDescription = expectedContentDescription + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun mapsDisabledDataToUnavailableStateWhenOnPowerSave() { + val inputModel = createModel(nightMode = false, powerSave = true) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedSecondaryLabel = + context.getString(R.string.quick_settings_dark_mode_secondary_label_battery_saver) + val expectedContentDescription = + TextUtils.concat(expectedLabel, ", ", expectedSecondaryLabel) + val expectedState = + createUiNightModeTileState( + activationState = QSTileState.ActivationState.UNAVAILABLE, + secondaryLabel = expectedSecondaryLabel, + contentDescription = expectedContentDescription + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun mapsDisabledDataToInactiveState() { + val inputModel = createModel(nightMode = false, powerSave = false) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[1] + val expectedState = + createUiNightModeTileState( + activationState = QSTileState.ActivationState.INACTIVE, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + contentDescription = expectedLabel + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun mapsEnabledDataToActiveState() { + val inputModel = createModel(true, false) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[2] + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_on, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.ACTIVE, + contentDescription = expectedLabel + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun mapsEnabledDataToOnIconState() { + val inputModel = createModel(nightMode = true, powerSave = false) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[2] + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_on, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.ACTIVE, + contentDescription = expectedLabel + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun mapsDisabledDataToOffIconState() { + val inputModel = createModel(nightMode = false, powerSave = false) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[1] + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.INACTIVE, + contentDescription = expectedLabel + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun supportsClickAndLongClickActionsWhenNotInPowerSaveInNightMode() { + val inputModel = createModel(nightMode = true, powerSave = false) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[2] + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_on, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.ACTIVE, + contentDescription = expectedLabel, + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun supportsOnlyLongClickActionWhenUnavailableInPowerSaveInNightMode() { + val inputModel = createModel(nightMode = true, powerSave = true) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = + context.getString(R.string.quick_settings_dark_mode_secondary_label_battery_saver) + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedContentDescription = + TextUtils.concat(expectedLabel, ", ", expectedSecondaryLabel) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.UNAVAILABLE, + contentDescription = expectedContentDescription, + supportedActions = setOf(QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun supportsClickAndLongClickActionsWhenNotInPowerSaveNotInNightMode() { + val inputModel = createModel(nightMode = false, powerSave = false) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[1] + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.INACTIVE, + contentDescription = expectedLabel, + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun supportsOnlyClickActionWhenUnavailableInPowerSaveNotInNightMode() { + val inputModel = createModel(nightMode = false, powerSave = true) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = + context.getString(R.string.quick_settings_dark_mode_secondary_label_battery_saver) + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.UNAVAILABLE, + contentDescription = TextUtils.concat(expectedLabel, ", ", expectedSecondaryLabel), + supportedActions = setOf(QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun secondaryLabelCorrectWhenInPowerSaveMode() { + val inputModel = createModel(powerSave = true) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = + context.getString(R.string.quick_settings_dark_mode_secondary_label_battery_saver) + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.UNAVAILABLE, + contentDescription = TextUtils.concat(expectedLabel, ", ", expectedSecondaryLabel), + supportedActions = setOf(QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun secondaryLabelCorrectWhenInNightModeNotInPowerSaveModeLocationEnabledUiModeIsNightAuto() { + val inputModel = + createModel( + nightMode = true, + powerSave = false, + isLocationEnabled = true, + uiMode = UiModeManager.MODE_NIGHT_AUTO + ) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = + context.getString(R.string.quick_settings_dark_mode_secondary_label_until_sunrise) + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_on, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.ACTIVE, + contentDescription = TextUtils.concat(expectedLabel, ", ", expectedSecondaryLabel), + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun secondaryLabelCorrectWhenNotInNightModeNotInPowerSaveModeLocationEnableUiModeIsNightAuto() { + val inputModel = + createModel( + nightMode = false, + powerSave = false, + isLocationEnabled = true, + uiMode = UiModeManager.MODE_NIGHT_AUTO + ) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = + context.getString(R.string.quick_settings_dark_mode_secondary_label_on_at_sunset) + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.INACTIVE, + contentDescription = TextUtils.concat(expectedLabel, ", ", expectedSecondaryLabel), + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun secondaryLabelCorrectWhenNotInPowerSaveAndUiModeIsNightYesInNightMode() { + val inputModel = + createModel(nightMode = true, powerSave = false, uiMode = UiModeManager.MODE_NIGHT_YES) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[2] + + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_on, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.ACTIVE, + contentDescription = expectedLabel, + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun secondaryLabelCorrectWhenNotInPowerSaveAndUiModeIsNightNoNotInNightMode() { + val inputModel = + createModel(nightMode = false, powerSave = false, uiMode = UiModeManager.MODE_NIGHT_NO) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[1] + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.INACTIVE, + contentDescription = expectedLabel, + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun secondaryLabelCorrectWhenNotInPowerSaveAndUiModeIsUnknownCustomNotInNightMode() { + val inputModel = + createModel( + nightMode = false, + powerSave = false, + uiMode = UiModeManager.MODE_NIGHT_CUSTOM, + nighModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN + ) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[1] + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.INACTIVE, + contentDescription = expectedLabel, + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun secondaryLabelCorrectWhenNotInPowerSaveAndUiModeIsUnknownCustomInNightMode() { + val inputModel = + createModel( + nightMode = true, + powerSave = false, + uiMode = UiModeManager.MODE_NIGHT_CUSTOM, + nighModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN + ) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = context.resources.getStringArray(R.array.tile_states_dark)[2] + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_on, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.ACTIVE, + contentDescription = expectedLabel, + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun secondaryLabelCorrectWhenInPowerSaveAndUiModeIsUnknownCustomNotInNightMode() { + val inputModel = + createModel( + nightMode = false, + powerSave = true, + uiMode = UiModeManager.MODE_NIGHT_CUSTOM, + nighModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN + ) + + val actualState: QSTileState = mapper.map(qsTileConfig, inputModel) + + val expectedSecondaryLabel = + context.getString(R.string.quick_settings_dark_mode_secondary_label_battery_saver) + val expectedLabel = context.getString(R.string.quick_settings_ui_mode_night_label) + val expectedContentDescription = + TextUtils.concat(expectedLabel, ", ", expectedSecondaryLabel) + val expectedState = + createUiNightModeTileState( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + label = expectedLabel, + secondaryLabel = expectedSecondaryLabel, + activationState = QSTileState.ActivationState.UNAVAILABLE, + contentDescription = expectedContentDescription, + supportedActions = setOf(QSTileState.UserAction.LONG_CLICK) + ) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileUserActionInteractorTest.kt new file mode 100644 index 000000000000..004ec6250e7e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileUserActionInteractorTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.uimodenight.domain + +import android.app.UiModeManager +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.actions.intentInputs +import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx +import com.android.systemui.qs.tiles.impl.uimodenight.UiModeNightTileModelHelper.createModel +import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileUserActionInteractor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class UiModeNightTileUserActionInteractorTest : SysuiTestCase() { + + private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler() + + private lateinit var underTest: UiModeNightTileUserActionInteractor + + @Mock private lateinit var uiModeManager: UiModeManager + + @Before + fun setup() { + uiModeManager = mock<UiModeManager>() + underTest = + UiModeNightTileUserActionInteractor( + EmptyCoroutineContext, + uiModeManager, + qsTileIntentUserActionHandler + ) + } + + @Test + fun handleClickToEnable() = runTest { + val stateBeforeClick = false + + underTest.handleInput(QSTileInputTestKtx.click(createModel(stateBeforeClick))) + + verify(uiModeManager).setNightModeActivated(!stateBeforeClick) + } + + @Test + fun handleClickToDisable() = runTest { + val stateBeforeClick = true + + underTest.handleInput(QSTileInputTestKtx.click(createModel(stateBeforeClick))) + + verify(uiModeManager).setNightModeActivated(!stateBeforeClick) + } + + @Test + fun clickToEnableDoesNothingWhenInPowerSaveInNightMode() = runTest { + val isNightMode = true + val isPowerSave = true + + underTest.handleInput(QSTileInputTestKtx.click(createModel(isNightMode, isPowerSave))) + + verify(uiModeManager, never()).setNightModeActivated(any()) + } + + @Test + fun clickToEnableDoesNothingWhenInPowerSaveNotInNightMode() = runTest { + val isNightMode = false + val isPowerSave = true + + underTest.handleInput(QSTileInputTestKtx.click(createModel(isNightMode, isPowerSave))) + + verify(uiModeManager, never()).setNightModeActivated(any()) + } + + @Test + fun handleLongClickNightModeEnabled() = runTest { + val isNightMode = true + + underTest.handleInput(QSTileInputTestKtx.longClick(createModel(isNightMode))) + + Truth.assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1) + val intentInput = qsTileIntentUserActionHandler.intentInputs.last() + val actualIntentAction = intentInput.intent.action + val expectedIntentAction = Settings.ACTION_DARK_THEME_SETTINGS + Truth.assertThat(actualIntentAction).isEqualTo(expectedIntentAction) + } + + @Test + fun handleLongClickNightModeDisabled() = runTest { + val isNightMode = false + + underTest.handleInput(QSTileInputTestKtx.longClick(createModel(isNightMode))) + + Truth.assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1) + val intentInput = qsTileIntentUserActionHandler.intentInputs.last() + val actualIntentAction = intentInput.intent.action + val expectedIntentAction = Settings.ACTION_DARK_THEME_SETTINGS + Truth.assertThat(actualIntentAction).isEqualTo(expectedIntentAction) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index c110de969b57..70be03146e17 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -63,6 +63,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import kotlin.test.Ignore import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -395,6 +396,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } @Test + @Ignore("b/315130482") fun deviceGoesToSleep_wakeUp_unlock() = testScope.runTest { unlockDevice() diff --git a/packages/SystemUI/res/layout/screen_record_options.xml b/packages/SystemUI/res/layout/screen_record_options.xml index 8916e427a1d5..fa345c929c25 100644 --- a/packages/SystemUI/res/layout/screen_record_options.xml +++ b/packages/SystemUI/res/layout/screen_record_options.xml @@ -40,16 +40,22 @@ android:popupBackground="@drawable/screenrecord_spinner_background" android:dropDownWidth="274dp" android:importantForAccessibility="yes"/> - <Switch + <FrameLayout + android:id="@+id/screenrecord_audio_switch_container" android:layout_width="wrap_content" - android:minWidth="48dp" - android:layout_height="48dp" - android:layout_weight="0" - android:layout_gravity="end" - android:id="@+id/screenrecord_audio_switch" - android:contentDescription="@string/screenrecord_audio_label" - style="@style/ScreenRecord.Switch" - android:importantForAccessibility="yes"/> + android:layout_height="wrap_content"> + <Switch + android:layout_width="wrap_content" + android:minWidth="48dp" + android:layout_height="48dp" + android:layout_gravity="end" + android:focusable="false" + android:clickable="false" + android:id="@+id/screenrecord_audio_switch" + android:contentDescription="@string/screenrecord_audio_label" + style="@style/ScreenRecord.Switch" + android:importantForAccessibility="yes"/> + </FrameLayout> </LinearLayout> <LinearLayout android:id="@+id/show_taps" @@ -75,13 +81,20 @@ android:fontFamily="@*android:string/config_bodyFontFamily" android:textColor="?android:attr/textColorPrimary" android:contentDescription="@string/screenrecord_taps_label"/> - <Switch + <FrameLayout + android:id="@+id/screenrecord_taps_switch_container" android:layout_width="wrap_content" - android:minWidth="48dp" - android:layout_height="48dp" - android:id="@+id/screenrecord_taps_switch" - android:contentDescription="@string/screenrecord_taps_label" - style="@style/ScreenRecord.Switch" - android:importantForAccessibility="yes"/> + android:layout_height="wrap_content"> + <Switch + android:layout_width="wrap_content" + android:minWidth="48dp" + android:layout_height="48dp" + android:focusable="false" + android:clickable="false" + android:id="@+id/screenrecord_taps_switch" + android:contentDescription="@string/screenrecord_taps_label" + style="@style/ScreenRecord.Switch" + android:importantForAccessibility="yes"/> + </FrameLayout> </LinearLayout> </LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index bed42833a1d4..333fc194b288 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -24,13 +24,14 @@ import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.shade.ShadeViewController +import javax.inject.Provider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** The base view model for the communal hub. */ abstract class BaseCommunalViewModel( private val communalInteractor: CommunalInteractor, - private val shadeViewController: ShadeViewController, + private val shadeViewController: Provider<ShadeViewController>, private val powerManager: PowerManager, val mediaHost: MediaHost, ) { @@ -48,7 +49,7 @@ abstract class BaseCommunalViewModel( fun onOuterTouch(motionEvent: MotionEvent) { // Forward the touch to the shade so that basic gestures like swipe up/down for // shade/bouncer work. - shadeViewController.handleExternalTouch(motionEvent) + shadeViewController.get().handleExternalTouch(motionEvent) } // TODO(b/308813166): remove once CommunalContainer is moved lower in z-order and doesn't block diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index b6843c529180..c82e00038b34 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -25,6 +25,7 @@ import com.android.systemui.media.dagger.MediaModule import com.android.systemui.shade.ShadeViewController import javax.inject.Inject import javax.inject.Named +import javax.inject.Provider import kotlinx.coroutines.flow.Flow /** The view model for communal hub in edit mode. */ @@ -33,7 +34,7 @@ class CommunalEditModeViewModel @Inject constructor( private val communalInteractor: CommunalInteractor, - shadeViewController: ShadeViewController, + shadeViewController: Provider<ShadeViewController>, powerManager: PowerManager, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, ) : BaseCommunalViewModel(communalInteractor, shadeViewController, powerManager, mediaHost) { diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index d7dcdb9ea4f0..abf198637911 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -26,6 +26,7 @@ import com.android.systemui.media.dagger.MediaModule import com.android.systemui.shade.ShadeViewController import javax.inject.Inject import javax.inject.Named +import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -39,7 +40,7 @@ class CommunalViewModel constructor( private val communalInteractor: CommunalInteractor, tutorialInteractor: CommunalTutorialInteractor, - shadeViewController: ShadeViewController, + shadeViewController: Provider<ShadeViewController>, powerManager: PowerManager, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, ) : BaseCommunalViewModel(communalInteractor, shadeViewController, powerManager, mediaHost) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 017dac200431..20da00ee3daf 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -43,6 +43,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel +import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -81,6 +82,7 @@ constructor( private val interactionJankMonitor: InteractionJankMonitor, private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor, private val vibratorHelper: VibratorHelper, + private val falsingManager: FalsingManager, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -155,6 +157,7 @@ constructor( interactionJankMonitor, deviceEntryHapticsInteractor, vibratorHelper, + falsingManager, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 2d6c0e1c13b2..b51edab6dfe8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -157,6 +157,9 @@ interface KeyguardRepository { val lastDozeTapToWakePosition: StateFlow<Point?> + /** Last point that [KeyguardRootView] was tapped */ + val lastRootViewTapPosition: MutableStateFlow<Point?> + /** Observable for the [StatusBarState] */ val statusBarState: StateFlow<StatusBarState> @@ -418,6 +421,8 @@ constructor( _lastDozeTapToWakePosition.value = position } + override val lastRootViewTapPosition: MutableStateFlow<Point?> = MutableStateFlow(null) + override val isDreamingWithOverlay: Flow<Boolean> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index b8c392591494..702386d3b498 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -171,6 +171,9 @@ constructor( /** Whether the keyguard is going away. */ val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway + /** Last point that [KeyguardRootView] view was tapped */ + val lastRootViewTapPosition: Flow<Point?> = repository.lastRootViewTapPosition.asStateFlow() + /** Whether the primary bouncer is showing or not. */ val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow @@ -304,6 +307,10 @@ constructor( repository.setClockShouldBeCentered(shouldBeCentered) } + fun setLastRootViewTapPosition(point: Point?) { + repository.lastRootViewTapPosition.value = point + } + companion object { private const val TAG = "KeyguardInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 02c6f33ffd01..e603ead463f2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -19,6 +19,8 @@ package com.android.systemui.keyguard.ui.binder import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.DrawableRes +import android.annotation.SuppressLint +import android.graphics.Point import android.view.HapticFeedbackConstants import android.view.View import android.view.View.OnLayoutChangeListener @@ -47,6 +49,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -73,6 +76,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) object KeyguardRootViewBinder { + @SuppressLint("ClickableViewAccessibility") @JvmStatic fun bind( view: ViewGroup, @@ -87,6 +91,7 @@ object KeyguardRootViewBinder { interactionJankMonitor: InteractionJankMonitor?, deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, vibratorHelper: VibratorHelper?, + falsingManager: FalsingManager?, ): DisposableHandle { var onLayoutChangeListener: OnLayoutChange? = null val childViews = mutableMapOf<Int, View>() @@ -94,6 +99,16 @@ object KeyguardRootViewBinder { val burnInLayerId = R.id.burn_in_layer val aodNotificationIconContainerId = R.id.aod_notification_icon_container val largeClockId = R.id.lockscreen_clock_view_large + + if (keyguardBottomAreaRefactor()) { + view.setOnTouchListener { _, event -> + if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) { + viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt())) + } + false + } + } + val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt index 8514225fda90..11e63e76c289 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.binder +import android.graphics.Rect import android.view.View import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle @@ -25,6 +26,8 @@ import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.view.LaunchableLinearLayout import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.common.ui.binder.TextViewBinder +import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils.LAUNCH_SOURCE_KEYGUARD @@ -35,12 +38,15 @@ import com.android.systemui.statusbar.VibratorHelper import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch object KeyguardSettingsViewBinder { fun bind( parentView: View, viewModel: KeyguardSettingsMenuViewModel, + longPressViewModel: KeyguardLongPressViewModel, + rootViewModel: KeyguardRootViewModel, vibratorHelper: VibratorHelper, activityStarter: ActivityStarter ): DisposableHandle { @@ -88,6 +94,18 @@ object KeyguardSettingsViewBinder { } } } + + launch { + rootViewModel.lastRootViewTapPosition.filterNotNull().collect { point -> + if (view.isVisible) { + val hitRect = Rect() + view.getHitRect(hitRect) + if (!hitRect.contains(point.x, point.y)) { + longPressViewModel.onTouchedOutside() + } + } + } + } } } return disposableHandle diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 4eecfdefa663..03e45fdbe75f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -376,6 +376,7 @@ constructor( null, // jank monitor not required for preview mode null, // device entry haptics not required preview mode null, // device entry haptics not required for preview mode + null, // falsing manager not required for preview mode ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt index 9a33f08386a3..4bc2d86e6b54 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt @@ -29,15 +29,15 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import androidx.core.view.isVisible import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.res.R import com.android.systemui.animation.view.LaunchableLinearLayout import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder +import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import javax.inject.Inject import kotlinx.coroutines.DisposableHandle @@ -47,6 +47,8 @@ class DefaultSettingsPopupMenuSection constructor( @Main private val resources: Resources, private val keyguardSettingsMenuViewModel: KeyguardSettingsMenuViewModel, + private val keyguardLongPressViewModel: KeyguardLongPressViewModel, + private val keyguardRootViewModel: KeyguardRootViewModel, private val vibratorHelper: VibratorHelper, private val activityStarter: ActivityStarter, ) : KeyguardSection() { @@ -73,6 +75,8 @@ constructor( KeyguardSettingsViewBinder.bind( constraintLayout.requireViewById<View>(R.id.keyguard_settings_button), keyguardSettingsMenuViewModel, + keyguardLongPressViewModel, + keyguardRootViewModel, vibratorHelper, activityStarter, ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 97ddbb033648..4588e02df10e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.graphics.Point import android.util.MathUtils import android.view.View.VISIBLE import com.android.app.animation.Interpolators @@ -101,6 +102,9 @@ constructor( val goneToAodTransition = keyguardTransitionInteractor.transition(from = GONE, to = AOD) + /** Last point that the root view was tapped */ + val lastRootViewTapPosition: Flow<Point?> = keyguardInteractor.lastRootViewTapPosition + /** the shared notification container bounds *on the lockscreen* */ val notificationBounds: StateFlow<NotificationContainerBounds> = keyguardInteractor.notificationContainerBounds @@ -262,4 +266,8 @@ constructor( } .toAnimatedValueFlow() } + + fun setRootViewLastTapPosition(point: Point) { + keyguardInteractor.setLastRootViewTapPosition(point) + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt new file mode 100644 index 000000000000..3f30c75a6b6a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.uimodenight.domain + +import android.app.UiModeManager +import android.content.res.Resources +import android.text.TextUtils +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +/** Maps [UiModeNightTileModel] to [QSTileState]. */ +class UiModeNightTileMapper @Inject constructor(@Main private val resources: Resources) : + QSTileDataToStateMapper<UiModeNightTileModel> { + companion object { + val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a") + val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm") + } + override fun map(config: QSTileConfig, data: UiModeNightTileModel): QSTileState = + with(data) { + QSTileState.build(resources, config.uiConfig) { + var shouldSetSecondaryLabel = false + + if (isPowerSave) { + secondaryLabel = + resources.getString( + R.string.quick_settings_dark_mode_secondary_label_battery_saver + ) + } else if (uiMode == UiModeManager.MODE_NIGHT_AUTO && isLocationEnabled) { + secondaryLabel = + resources.getString( + if (isNightMode) + R.string.quick_settings_dark_mode_secondary_label_until_sunrise + else R.string.quick_settings_dark_mode_secondary_label_on_at_sunset + ) + } else if (uiMode == UiModeManager.MODE_NIGHT_CUSTOM) { + if (nightModeCustomType == UiModeManager.MODE_NIGHT_CUSTOM_TYPE_SCHEDULE) { + val time: LocalTime = + if (isNightMode) { + customNightModeEnd + } else { + customNightModeStart + } + + val formatter: DateTimeFormatter = + if (is24HourFormat) formatter24Hour else formatter12Hour + + secondaryLabel = + resources.getString( + if (isNightMode) + R.string.quick_settings_dark_mode_secondary_label_until + else R.string.quick_settings_dark_mode_secondary_label_on_at, + formatter.format(time) + ) + } else if ( + nightModeCustomType == UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME + ) { + secondaryLabel = + resources.getString( + if (isNightMode) + R.string + .quick_settings_dark_mode_secondary_label_until_bedtime_ends + else R.string.quick_settings_dark_mode_secondary_label_on_at_bedtime + ) + } else { + secondaryLabel = null // undefined type of nightModeCustomType + shouldSetSecondaryLabel = true + } + } else { + secondaryLabel = null + shouldSetSecondaryLabel = true + } + + contentDescription = + if (TextUtils.isEmpty(secondaryLabel)) label + else TextUtils.concat(label, ", ", secondaryLabel) + if (isPowerSave) { + activationState = QSTileState.ActivationState.UNAVAILABLE + if (shouldSetSecondaryLabel) + secondaryLabel = resources.getStringArray(R.array.tile_states_dark)[0] + } else { + activationState = + if (isNightMode) QSTileState.ActivationState.ACTIVE + else QSTileState.ActivationState.INACTIVE + + if (shouldSetSecondaryLabel) { + secondaryLabel = + if (activationState == QSTileState.ActivationState.INACTIVE) + resources.getStringArray(R.array.tile_states_dark)[1] + else resources.getStringArray(R.array.tile_states_dark)[2] + } + } + + val iconRes = + if (activationState == QSTileState.ActivationState.ACTIVE) + R.drawable.qs_light_dark_theme_icon_on + else R.drawable.qs_light_dark_theme_icon_off + val iconResource = Icon.Resource(iconRes, null) + icon = { iconResource } + + supportedActions = + if (activationState == QSTileState.ActivationState.UNAVAILABLE) + setOf(QSTileState.UserAction.LONG_CLICK) + else setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileDataInteractor.kt new file mode 100644 index 000000000000..c928e8af17fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileDataInteractor.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor + +import android.app.UiModeManager +import android.content.Context +import android.content.res.Configuration +import android.os.UserHandle +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.LocationController +import com.android.systemui.util.time.DateFormatUtil +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** Observes ui mode night state changes providing the [UiModeNightTileModel]. */ +class UiModeNightTileDataInteractor +@Inject +constructor( + @Application private val context: Context, + private val configurationController: ConfigurationController, + private val uiModeManager: UiModeManager, + private val batteryController: BatteryController, + private val locationController: LocationController, + private val dateFormatUtil: DateFormatUtil, +) : QSTileDataInteractor<UiModeNightTileModel> { + + override fun tileData( + user: UserHandle, + triggers: Flow<DataUpdateTrigger> + ): Flow<UiModeNightTileModel> = + ConflatedCallbackFlow.conflatedCallbackFlow { + // send initial state + trySend(createModel()) + + val configurationCallback = + object : ConfigurationController.ConfigurationListener { + override fun onUiModeChanged() { + trySend(createModel()) + } + } + configurationController.addCallback(configurationCallback) + + val batteryCallback = + object : BatteryController.BatteryStateChangeCallback { + override fun onPowerSaveChanged(isPowerSave: Boolean) { + trySend(createModel()) + } + } + batteryController.addCallback(batteryCallback) + + val locationCallback = + object : LocationController.LocationChangeCallback { + override fun onLocationSettingsChanged(locationEnabled: Boolean) { + trySend(createModel()) + } + } + locationController.addCallback(locationCallback) + + awaitClose { + configurationController.removeCallback(configurationCallback) + batteryController.removeCallback(batteryCallback) + locationController.removeCallback(locationCallback) + } + } + + private fun createModel(): UiModeNightTileModel { + val uiMode = uiModeManager.nightMode + val nightMode = + (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == + Configuration.UI_MODE_NIGHT_YES + val powerSave = batteryController.isPowerSave + val locationEnabled = locationController.isLocationEnabled + val nightModeCustomType = uiModeManager.nightModeCustomType + val use24HourFormat = dateFormatUtil.is24HourFormat + val customNightModeEnd = uiModeManager.customNightModeEnd + val customNightModeStart = uiModeManager.customNightModeStart + + return UiModeNightTileModel( + uiMode, + nightMode, + powerSave, + locationEnabled, + nightModeCustomType, + use24HourFormat, + customNightModeEnd, + customNightModeStart + ) + } + + override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt new file mode 100644 index 000000000000..00d7a629f5be --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor + +import android.app.UiModeManager +import android.content.Intent +import android.provider.Settings +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.interactor.QSTileInput +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext + +/** Handles ui mode night tile clicks. */ +class UiModeNightTileUserActionInteractor +@Inject +constructor( + @Background private val backgroundContext: CoroutineContext, + private val uiModeManager: UiModeManager, + private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, +) : QSTileUserActionInteractor<UiModeNightTileModel> { + + override suspend fun handleInput(input: QSTileInput<UiModeNightTileModel>) = + with(input) { + when (action) { + is QSTileUserAction.Click -> { + if (!input.data.isPowerSave) { + withContext(backgroundContext) { + uiModeManager.setNightModeActivated(!input.data.isNightMode) + } + } + } + is QSTileUserAction.LongClick -> { + qsTileIntentUserActionHandler.handle( + action.view, + Intent(Settings.ACTION_DARK_THEME_SETTINGS) + ) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/model/UiModeNightTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/model/UiModeNightTileModel.kt new file mode 100644 index 000000000000..4fa1306d988d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/model/UiModeNightTileModel.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.uimodenight.domain.model + +import java.time.LocalTime + +/** + * UiModeNight tile model. Quick Settings tile for: Night Mode / Dark Theme / Dark Mode. + * + * @param isNightMode is true when the NightMode is enabled; + */ +data class UiModeNightTileModel( + val uiMode: Int, + val isNightMode: Boolean, + val isPowerSave: Boolean, + val isLocationEnabled: Boolean, + val nightModeCustomType: Int, + val is24HourFormat: Boolean, + val customNightModeEnd: LocalTime, + val customNightModeStart: LocalTime +) diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt index 10d51a59e44c..3eb26f498921 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt @@ -28,6 +28,7 @@ import android.view.MotionEvent.ACTION_MOVE import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import android.view.ViewGroup import android.view.accessibility.AccessibilityNodeInfo import android.widget.AdapterView import android.widget.ArrayAdapter @@ -64,10 +65,13 @@ class ScreenRecordPermissionDialogDelegate( mediaProjectionMetricsLogger, R.drawable.ic_screenrecord, R.color.screenrecord_icon_color - ), SystemUIDialog.Delegate { + ), + SystemUIDialog.Delegate { private lateinit var tapsSwitch: Switch + private lateinit var tapsSwitchContainer: ViewGroup private lateinit var tapsView: View private lateinit var audioSwitch: Switch + private lateinit var audioSwitchContainer: ViewGroup private lateinit var options: Spinner override fun createDialog(): SystemUIDialog { @@ -114,12 +118,17 @@ class ScreenRecordPermissionDialogDelegate( private fun initRecordOptionsView() { audioSwitch = dialog.requireViewById(R.id.screenrecord_audio_switch) tapsSwitch = dialog.requireViewById(R.id.screenrecord_taps_switch) + audioSwitchContainer = dialog.requireViewById(R.id.screenrecord_audio_switch_container) + tapsSwitchContainer = dialog.requireViewById(R.id.screenrecord_taps_switch_container) // Add these listeners so that the switch only responds to movement // within its target region, to meet accessibility requirements audioSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE } tapsSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE } + audioSwitchContainer.setOnClickListener { audioSwitch.toggle() } + tapsSwitchContainer.setOnClickListener { tapsSwitch.toggle() } + tapsView = dialog.requireViewById(R.id.show_taps) updateTapsViewVisibility() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 93bc96022292..af6da3fb6e51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -146,7 +146,7 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh * When you just need a dialog, call this. */ public SystemUIDialog create() { - return create(new DialogDelegate<>(){}); + return create(new DialogDelegate<>(){}, mContext); } /** @@ -155,13 +155,18 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh * * When you need to customize the dialog, pass it a delegate. */ + public SystemUIDialog create(Delegate delegate, Context context) { + return create((DialogDelegate<SystemUIDialog>) delegate, context); + } + public SystemUIDialog create(Delegate delegate) { - return create((DialogDelegate<SystemUIDialog>) delegate); + return create(delegate, mContext); } - private SystemUIDialog create(DialogDelegate<SystemUIDialog> dialogDelegate) { + private SystemUIDialog create(DialogDelegate<SystemUIDialog> dialogDelegate, + Context context) { return new SystemUIDialog( - mContext, + context, DEFAULT_THEME, DEFAULT_DISMISS_ON_DEVICE_LOCK, mFeatureFlags, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt index 0f2da2d09633..087e100e9b33 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt @@ -38,6 +38,10 @@ import com.android.systemui.qs.tiles.impl.location.domain.LocationTileMapper import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileDataInteractor import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileUserActionInteractor import com.android.systemui.qs.tiles.impl.location.domain.model.LocationTileModel +import com.android.systemui.qs.tiles.impl.uimodenight.domain.UiModeNightTileMapper +import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileDataInteractor +import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel @@ -64,6 +68,7 @@ interface PolicyModule { const val FLASHLIGHT_TILE_SPEC = "flashlight" const val LOCATION_TILE_SPEC = "location" const val ALARM_TILE_SPEC = "alarm" + const val UIMODENIGHT_TILE_SPEC = "dark" /** Inject flashlight config */ @Provides @@ -160,6 +165,38 @@ interface PolicyModule { stateInteractor, mapper, ) + + /** Inject uimodenight config */ + @Provides + @IntoMap + @StringKey(UIMODENIGHT_TILE_SPEC) + fun provideUiModeNightTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(UIMODENIGHT_TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_light_dark_theme_icon_off, + labelRes = R.string.quick_settings_ui_mode_night_label, + ), + instanceId = uiEventLogger.getNewInstanceId(), + ) + + /** Inject uimodenight into tileViewModelMap in QSModule */ + @Provides + @IntoMap + @StringKey(UIMODENIGHT_TILE_SPEC) + fun provideUiModeNightTileViewModel( + factory: QSTileViewModelFactory.Static<UiModeNightTileModel>, + mapper: UiModeNightTileMapper, + stateInteractor: UiModeNightTileDataInteractor, + userActionInteractor: UiModeNightTileUserActionInteractor + ): QSTileViewModel = + factory.create( + TileSpec.create(UIMODENIGHT_TILE_SPEC), + userActionInteractor, + stateInteractor, + mapper, + ) } /** Inject FlashlightTile into tileMap in QSModule */ diff --git a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java index d6e6f3fc56b1..bd698ab8ad5c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java +++ b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java @@ -157,6 +157,7 @@ public class CsdWarningDialog extends SystemUIDialog if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REPEATED_5X) { // only show a notification in case we reached 500% of dose show5XNotification(); + dismissCsdDialog(); return; } super.show(); @@ -217,6 +218,10 @@ public class CsdWarningDialog extends SystemUIDialog @Override public void onDismiss(DialogInterface unused) { + dismissCsdDialog(); + } + + private void dismissCsdDialog() { try { mContext.unregisterReceiver(mReceiver); } catch (IllegalArgumentException e) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 81a7bec52bb5..0e7c6625264c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -119,6 +119,8 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { private val _keyguardAlpha = MutableStateFlow(1f) override val keyguardAlpha: StateFlow<Float> = _keyguardAlpha + override val lastRootViewTapPosition: MutableStateFlow<Point?> = MutableStateFlow(null) + override fun setQuickSettingsVisible(isVisible: Boolean) { _isQuickSettingsVisible.value = isVisible } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/uimodenight/UiModeNightTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/uimodenight/UiModeNightTileKosmos.kt new file mode 100644 index 000000000000..f0e5807ca515 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/uimodenight/UiModeNightTileKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.uimodenight + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.qsEventLogger +import com.android.systemui.statusbar.policy.PolicyModule + +val Kosmos.qsUiModeNightTileConfig by + Kosmos.Fixture { PolicyModule.provideUiModeNightTileConfig(qsEventLogger) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/uimodenight/UiModeNightTileModelHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/uimodenight/UiModeNightTileModelHelper.kt new file mode 100644 index 000000000000..1fe18e3f4d8e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/uimodenight/UiModeNightTileModelHelper.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.uimodenight + +import android.content.res.Configuration +import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel +import java.time.LocalTime + +object UiModeNightTileModelHelper { + + const val DEFAULT_NIGHT_MODE_CUSTOM_TYPE = 0 + val defaultCustomNightEnd: LocalTime = LocalTime.MAX + val defaultCustomNightStart: LocalTime = LocalTime.MIN + + fun createModel( + nightMode: Boolean = false, + powerSave: Boolean = false, + uiMode: Int = Configuration.UI_MODE_NIGHT_NO, + isLocationEnabled: Boolean = true, + nighModeCustomType: Int = DEFAULT_NIGHT_MODE_CUSTOM_TYPE, + is24HourFormat: Boolean = false, + customNightModeEnd: LocalTime = defaultCustomNightEnd, + customNightModeStart: LocalTime = defaultCustomNightStart + ): UiModeNightTileModel { + return UiModeNightTileModel( + uiMode, + nightMode, + powerSave, + isLocationEnabled, + nighModeCustomType, + is24HourFormat, + customNightModeEnd, + customNightModeStart + ) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java index 209cac6688a2..5ae033c9870d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java @@ -22,11 +22,16 @@ import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; public class FakeBatteryController extends BaseLeakChecker<BatteryStateChangeCallback> implements BatteryController { private boolean mIsAodPowerSave = false; private boolean mWirelessCharging; + private boolean mPowerSaveMode = false; + + private final List<BatteryStateChangeCallback> mCallbacks = new ArrayList<>(); public FakeBatteryController(LeakCheck test) { super(test, "battery"); @@ -44,12 +49,18 @@ public class FakeBatteryController extends BaseLeakChecker<BatteryStateChangeCal @Override public void setPowerSaveMode(boolean powerSave) { - + mPowerSaveMode = powerSave; + for (BatteryStateChangeCallback callback: mCallbacks) { + callback.onPowerSaveChanged(powerSave); + } } + /** + * Note: this method ignores the View argument + */ @Override public void setPowerSaveMode(boolean powerSave, View view) { - + setPowerSaveMode(powerSave); } @Override @@ -59,7 +70,7 @@ public class FakeBatteryController extends BaseLeakChecker<BatteryStateChangeCal @Override public boolean isPowerSave() { - return false; + return mPowerSaveMode; } @Override @@ -79,4 +90,14 @@ public class FakeBatteryController extends BaseLeakChecker<BatteryStateChangeCal public void setWirelessCharging(boolean wirelessCharging) { mWirelessCharging = wirelessCharging; } + + @Override + public void addCallback(BatteryStateChangeCallback listener) { + mCallbacks.add(listener); + } + + @Override + public void removeCallback(BatteryStateChangeCallback listener) { + mCallbacks.remove(listener); + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeLocationController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeLocationController.java index 3c6327514f24..442d15b7bc95 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeLocationController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeLocationController.java @@ -26,6 +26,7 @@ public class FakeLocationController extends BaseLeakChecker<LocationChangeCallba implements LocationController { private final List<LocationChangeCallback> mCallbacks = new ArrayList<>(); + private boolean mLocationEnabled = false; public FakeLocationController(LeakCheck test) { super(test, "location"); @@ -38,13 +39,14 @@ public class FakeLocationController extends BaseLeakChecker<LocationChangeCallba @Override public boolean isLocationEnabled() { - return false; + return mLocationEnabled; } @Override public boolean setLocationEnabled(boolean enabled) { + mLocationEnabled = enabled; mCallbacks.forEach(callback -> callback.onLocationSettingsChanged(enabled)); - return false; + return true; } @Override diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 5bffe80a5c69..440e99632c86 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -4406,6 +4406,28 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @Override + public boolean isAccessibilityServiceWarningRequired(AccessibilityServiceInfo info) { + mSecurityPolicy.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY); + + // Warning is not required if the service is already enabled. + synchronized (mLock) { + final AccessibilityUserState userState = getCurrentUserStateLocked(); + if (userState.getEnabledServicesLocked().contains(info.getComponentName())) { + return false; + } + } + // Warning is not required if the service is already assigned to a shortcut. + for (int shortcutType : AccessibilityManager.SHORTCUT_TYPES) { + if (getAccessibilityShortcutTargets(shortcutType).contains( + info.getComponentName().flattenToString())) { + return false; + } + } + // Warning is required by default. + return true; + } + + @Override public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return; synchronized (mLock) { diff --git a/services/core/Android.bp b/services/core/Android.bp index 8ed3fd696bda..b4cf34e00c53 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -201,6 +201,7 @@ java_library_static { "biometrics_flags_lib", "am_flags_lib", "com_android_wm_shell_flags_lib", + "com.android.server.utils_aconfig-java", "service-jobscheduler-deviceidle.flags-aconfig-java", ], javac_shard_size: 50, diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index eb3ec2444210..05d07ae761c1 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -1464,15 +1464,17 @@ public class BinaryTransparencyService extends SystemService { FrameworkStatsLog.write(FrameworkStatsLog.VBMETA_DIGEST_REPORTED, mVbmetaDigest); if (android.security.Flags.binaryTransparencySepolicyHash()) { - byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes( - "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer()); - String sepolicyHashEncoded = null; - if (sepolicyHash != null) { - sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false); - Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded); - } - FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED, - sepolicyHashEncoded, mVbmetaDigest); + IoThread.getExecutor().execute(() -> { + byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes( + "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer()); + String sepolicyHashEncoded = null; + if (sepolicyHash != null) { + sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false); + Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded); + } + FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED, + sepolicyHashEncoded, mVbmetaDigest); + }); } } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 5f1a7e7e8123..71916843fe0b 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -241,6 +241,7 @@ import com.android.server.am.LowMemDetector.MemFactor; import com.android.server.am.ServiceRecord.ShortFgsInfo; import com.android.server.pm.KnownPackages; import com.android.server.uri.NeededUriGrants; +import com.android.server.utils.AnrTimer; import com.android.server.wm.ActivityServiceConnectionsHolder; import java.io.FileDescriptor; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 469f209eb9b5..2d687de2f734 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -475,6 +475,7 @@ import com.android.server.sdksandbox.SdkSandboxManagerLocal; import com.android.server.uri.GrantUri; import com.android.server.uri.NeededUriGrants; import com.android.server.uri.UriGrantsManagerInternal; +import com.android.server.utils.AnrTimer; import com.android.server.utils.PriorityDump; import com.android.server.utils.Slogf; import com.android.server.utils.TimingsTraceAndSlog; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index ad499911f84a..2cac7a020005 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -88,6 +88,7 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.server.am.BroadcastProcessQueue.BroadcastConsumer; import com.android.server.am.BroadcastProcessQueue.BroadcastPredicate; import com.android.server.am.BroadcastRecord.DeliveryState; +import com.android.server.utils.AnrTimer; import dalvik.annotation.optimization.NeverCompile; diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 2ed079ab0c62..d9e8dddddae4 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -9,14 +9,6 @@ flag { } flag { - name: "anr_timer_service_enabled" - namespace: "system_performance" - is_fixed_read_only: true - description: "Feature flag for the ANR timer service" - bug: "282428924" -} - -flag { name: "fgs_abuse_detection" namespace: "backstage_power" description: "Detect abusive FGS behavior for certain types (camera, mic, media, location)." diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java index b91e633bd3de..ffdab7dfbfa4 100644 --- a/services/core/java/com/android/server/audio/AdiDeviceState.java +++ b/services/core/java/com/android/server/audio/AdiDeviceState.java @@ -21,6 +21,8 @@ import static android.media.AudioSystem.DEVICE_NONE; import static android.media.AudioSystem.isBluetoothDevice; import static android.media.audio.Flags.automaticBtDeviceType; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; + import android.annotation.NonNull; import android.annotation.Nullable; import android.media.AudioDeviceAttributes; @@ -31,13 +33,16 @@ import android.text.TextUtils; import android.util.Log; import android.util.Pair; +import com.android.internal.annotations.VisibleForTesting; + import java.util.Objects; /** * Class representing all devices that were previously or are currently connected. Data is * persisted in {@link android.provider.Settings.Secure} */ -/*package*/ final class AdiDeviceState { +@VisibleForTesting(visibility = PACKAGE) +public final class AdiDeviceState { private static final String TAG = "AS.AdiDeviceState"; private static final String SETTING_FIELD_SEPARATOR = ","; diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 98b210f29db4..e54bf64df09e 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -31,6 +31,8 @@ import static android.media.AudioSystem.isBluetoothScoOutDevice; import static android.media.audio.Flags.automaticBtDeviceType; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; + import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; @@ -347,7 +349,8 @@ public class AudioDeviceInventory { * @return the found {@link AdiDeviceState} or {@code null} otherwise. */ @Nullable - AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) { + @VisibleForTesting(visibility = PACKAGE) + public AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) { Set<Integer> deviceSet; if (isBluetoothA2dpOutDevice(deviceType)) { deviceSet = DEVICE_OUT_ALL_A2DP_SET; diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 8cec24d1bbb5..f7b7aaa60a35 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -39,6 +39,7 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE; import static android.provider.Settings.Secure.VOLUME_HUSH_OFF; import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.media.audio.Flags.alarmMinVolumeZero; import static com.android.media.audio.Flags.bluetoothMacAddressAnonymization; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; @@ -6785,7 +6786,8 @@ public class AudioService extends IAudioService.Stub return mContentResolver; } - /*package*/ SettingsAdapter getSettings() { + @VisibleForTesting(visibility = PACKAGE) + public SettingsAdapter getSettings() { return mSettings; } @@ -11250,7 +11252,9 @@ public class AudioService extends IAudioService.Stub return mDeviceBroker.isBluetoothAudioDeviceCategoryFixed(address); } - /*package*/void onUpdatedAdiDeviceState(AdiDeviceState deviceState) { + /** Update the sound dose and spatializer state based on the new AdiDeviceState. */ + @VisibleForTesting(visibility = PACKAGE) + public void onUpdatedAdiDeviceState(AdiDeviceState deviceState) { if (deviceState == null) { return; } diff --git a/services/core/java/com/android/server/content/SyncJobService.java b/services/core/java/com/android/server/content/SyncJobService.java index cd3f0f0ca5b2..1da7f0c059b0 100644 --- a/services/core/java/com/android/server/content/SyncJobService.java +++ b/services/core/java/com/android/server/content/SyncJobService.java @@ -19,7 +19,6 @@ package com.android.server.content; import android.annotation.Nullable; import android.app.job.JobParameters; import android.app.job.JobService; -import android.content.pm.PackageManagerInternal; import android.os.Message; import android.os.SystemClock; import android.util.Log; @@ -29,7 +28,6 @@ import android.util.SparseBooleanArray; import android.util.SparseLongArray; import com.android.internal.annotations.GuardedBy; -import com.android.server.LocalServices; public class SyncJobService extends JobService { private static final String TAG = "SyncManager"; @@ -99,20 +97,6 @@ public class SyncJobService extends JobService { return true; } - // TODO(b/209852664): remove this logic from here once it's added within JobScheduler. - // JobScheduler should not call onStartJob for syncs whose source packages are stopped. - // Until JS adds the relevant logic, this is a temporary solution to keep deferring syncs - // for packages in the stopped state. - if (android.content.pm.Flags.stayStopped()) { - if (LocalServices.getService(PackageManagerInternal.class) - .isPackageStopped(op.owningPackage, op.target.userId)) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Slog.d(TAG, "Skipping sync for force-stopped package: " + op.owningPackage); - } - return false; - } - } - boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); synchronized (sLock) { final int jobId = params.getJobId(); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 90c406357fd8..df9e7417054b 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -2908,46 +2908,69 @@ class MediaRouter2ServiceImpl { if (service == null) { return; } - List<RouterRecord> activeRouterRecords = Collections.emptyList(); + List<RouterRecord> activeRouterRecords; List<RouterRecord> allRouterRecords = getRouterRecords(); - List<ManagerRecord> managerRecords = getManagerRecords(); - - boolean isManagerScanning = false; - if (Flags.disableScreenOffBroadcastReceiver() - || service.mPowerManager.isInteractive()) { - isManagerScanning = managerRecords.stream().anyMatch(manager -> - manager.mIsScanning && service.mActivityManager - .getPackageImportance(manager.mOwnerPackageName) - <= sPackageImportanceForScanning); - - if (isManagerScanning) { - activeRouterRecords = allRouterRecords; - } else { - activeRouterRecords = - allRouterRecords.stream() - .filter( - record -> - service.mActivityManager.getPackageImportance( - record.mPackageName) - <= sPackageImportanceForScanning) - .collect(Collectors.toList()); - } + + boolean areManagersScanning = areManagersScanning(service, getManagerRecords()); + + if (areManagersScanning) { + activeRouterRecords = allRouterRecords; + } else { + activeRouterRecords = getIndividuallyActiveRouters(service, allRouterRecords); + } + + updateManagerScanningForProviders(areManagersScanning); + + Set<String> activelyScanningPackages = new HashSet<>(); + RouteDiscoveryPreference newPreference = + buildCompositeDiscoveryPreference( + activeRouterRecords, areManagersScanning, activelyScanningPackages); + + if (updateScanningOnUserRecord(service, activelyScanningPackages, newPreference)) { + updateDiscoveryPreferenceForProviders(activelyScanningPackages); } + } + private void updateDiscoveryPreferenceForProviders(Set<String> activelyScanningPackages) { for (MediaRoute2Provider provider : mRouteProviders) { - if (provider instanceof MediaRoute2ProviderServiceProxy) { - ((MediaRoute2ProviderServiceProxy) provider) - .setManagerScanning(isManagerScanning); + provider.updateDiscoveryPreference( + activelyScanningPackages, mUserRecord.mCompositeDiscoveryPreference); + } + } + + private boolean updateScanningOnUserRecord( + MediaRouter2ServiceImpl service, + Set<String> activelyScanningPackages, + RouteDiscoveryPreference newPreference) { + synchronized (service.mLock) { + if (newPreference.equals(mUserRecord.mCompositeDiscoveryPreference) + && activelyScanningPackages.equals(mUserRecord.mActivelyScanningPackages)) { + return false; } + mUserRecord.mCompositeDiscoveryPreference = newPreference; + mUserRecord.mActivelyScanningPackages = activelyScanningPackages; } + return true; + } - // Build a composite RouteDiscoveryPreference that matches all of the routes - // that match one or more of the individual discovery preferences. It may also - // match additional routes. The composite RouteDiscoveryPreference can be used - // to query route providers once to obtain all of the routes of interest, which - // can be subsequently filtered for the individual discovery preferences. + /** + * Returns a composite {@link RouteDiscoveryPreference} that aggregates every router + * record's individual discovery preference. + * + * <p>The {@link RouteDiscoveryPreference#shouldPerformActiveScan() active scan value} of + * the composite discovery preference is true if one of the router records is actively + * scanning or if {@code shouldForceActiveScan} is true. + * + * <p>The composite RouteDiscoveryPreference is used to query route providers once to obtain + * all the routes of interest, which can be subsequently filtered for the individual + * discovery preferences. + */ + @NonNull + private static RouteDiscoveryPreference buildCompositeDiscoveryPreference( + List<RouterRecord> activeRouterRecords, + boolean shouldForceActiveScan, + Set<String> activelyScanningPackages) { Set<String> preferredFeatures = new HashSet<>(); - Set<String> activelyScanningPackages = new HashSet<>(); boolean activeScan = false; for (RouterRecord activeRouterRecord : activeRouterRecords) { RouteDiscoveryPreference preference = activeRouterRecord.mDiscoveryPreference; @@ -2957,21 +2980,51 @@ class MediaRouter2ServiceImpl { activelyScanningPackages.add(activeRouterRecord.mPackageName); } } - RouteDiscoveryPreference newPreference = new RouteDiscoveryPreference.Builder( - List.copyOf(preferredFeatures), activeScan || isManagerScanning).build(); + return new RouteDiscoveryPreference.Builder( + List.copyOf(preferredFeatures), activeScan || shouldForceActiveScan) + .build(); + } - synchronized (service.mLock) { - if (newPreference.equals(mUserRecord.mCompositeDiscoveryPreference) - && activelyScanningPackages.equals(mUserRecord.mActivelyScanningPackages)) { - return; + private void updateManagerScanningForProviders(boolean isManagerScanning) { + for (MediaRoute2Provider provider : mRouteProviders) { + if (provider instanceof MediaRoute2ProviderServiceProxy) { + ((MediaRoute2ProviderServiceProxy) provider) + .setManagerScanning(isManagerScanning); } - mUserRecord.mCompositeDiscoveryPreference = newPreference; - mUserRecord.mActivelyScanningPackages = activelyScanningPackages; } - for (MediaRoute2Provider provider : mRouteProviders) { - provider.updateDiscoveryPreference( - activelyScanningPackages, mUserRecord.mCompositeDiscoveryPreference); + } + + @NonNull + private static List<RouterRecord> getIndividuallyActiveRouters( + MediaRouter2ServiceImpl service, List<RouterRecord> allRouterRecords) { + if (!Flags.disableScreenOffBroadcastReceiver() + && !service.mPowerManager.isInteractive()) { + return Collections.emptyList(); } + + return allRouterRecords.stream() + .filter( + record -> + service.mActivityManager.getPackageImportance( + record.mPackageName) + <= sPackageImportanceForScanning) + .collect(Collectors.toList()); + } + + private static boolean areManagersScanning( + MediaRouter2ServiceImpl service, List<ManagerRecord> managerRecords) { + if (!Flags.disableScreenOffBroadcastReceiver() + && !service.mPowerManager.isInteractive()) { + return false; + } + + return managerRecords.stream() + .anyMatch( + manager -> + manager.mIsScanning + && service.mActivityManager.getPackageImportance( + manager.mOwnerPackageName) + <= sPackageImportanceForScanning); } private MediaRoute2Provider findProvider(@Nullable String providerId) { diff --git a/services/core/java/com/android/server/utils/Android.bp b/services/core/java/com/android/server/utils/Android.bp new file mode 100644 index 000000000000..3a334bee93ff --- /dev/null +++ b/services/core/java/com/android/server/utils/Android.bp @@ -0,0 +1,10 @@ +aconfig_declarations { + name: "com.android.server.utils-aconfig", + package: "com.android.server.utils", + srcs: ["*.aconfig"], +} + +java_aconfig_library { + name: "com.android.server.utils_aconfig-java", + aconfig_declarations: "com.android.server.utils-aconfig", +} diff --git a/services/core/java/com/android/server/am/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java index 3e17930e3cb9..2b6dffb2b271 100644 --- a/services/core/java/com/android/server/am/AnrTimer.java +++ b/services/core/java/com/android/server/utils/AnrTimer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.am; +package com.android.server.utils; import static android.text.TextUtils.formatSimple; @@ -77,7 +77,7 @@ import java.util.concurrent.atomic.AtomicInteger; * * @hide */ -class AnrTimer<V> { +public class AnrTimer<V> { /** * The log tag. @@ -568,7 +568,7 @@ class AnrTimer<V> { * @param label A name for this instance. * @param extend A flag to indicate if expired timers can be granted extensions. */ - AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) { + public AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) { this(handler, what, label, extend, new Injector(handler)); } @@ -580,7 +580,7 @@ class AnrTimer<V> { * @param what The "what" parameter for the expiration message. * @param label A name for this instance. */ - AnrTimer(@NonNull Handler handler, int what, @NonNull String label) { + public AnrTimer(@NonNull Handler handler, int what, @NonNull String label) { this(handler, what, label, false); } @@ -591,7 +591,7 @@ class AnrTimer<V> { * * @return true if the service is flag-enabled. */ - boolean serviceEnabled() { + public boolean serviceEnabled() { return mFeature.enabled(); } @@ -856,7 +856,7 @@ class AnrTimer<V> { * @param timeoutMs The timer timeout, in milliseconds. * @return true if the timer was successfully created. */ - boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) { + public boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) { return mFeature.start(arg, pid, uid, timeoutMs); } @@ -867,7 +867,7 @@ class AnrTimer<V> { * * @return true if the timer was found and was running. */ - boolean cancel(@NonNull V arg) { + public boolean cancel(@NonNull V arg) { return mFeature.cancel(arg); } @@ -878,7 +878,7 @@ class AnrTimer<V> { * * @return true if the timer was found and was expired. */ - boolean accept(@NonNull V arg) { + public boolean accept(@NonNull V arg) { return mFeature.accept(arg); } @@ -892,7 +892,7 @@ class AnrTimer<V> { * * @return true if the timer was found and was expired. */ - boolean discard(@NonNull V arg) { + public boolean discard(@NonNull V arg) { return mFeature.discard(arg); } @@ -1010,7 +1010,7 @@ class AnrTimer<V> { /** * Dumpsys output. */ - static void dump(@NonNull PrintWriter pw, boolean verbose) { + public static void dump(@NonNull PrintWriter pw, boolean verbose) { final IndentingPrintWriter ipw = new IndentingPrintWriter(pw); ipw.println("AnrTimer statistics"); ipw.increaseIndent(); diff --git a/services/core/java/com/android/server/utils/flags.aconfig b/services/core/java/com/android/server/utils/flags.aconfig new file mode 100644 index 000000000000..489e21ab06ca --- /dev/null +++ b/services/core/java/com/android/server/utils/flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.server.utils" + +flag { + name: "anr_timer_service_enabled" + namespace: "system_performance" + is_fixed_read_only: true + description: "Feature flag for the ANR timer service" + bug: "282428924" +} diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 4625b4fe07ef..f8b22c97e218 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -614,6 +614,15 @@ public class BackgroundActivityStartController { == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; if (callerCanAllow && realCallerCanAllow) { // Both caller and real caller allow with system defined behavior + if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) { + // Will be allowed even with BAL hardening. + if (DEBUG_ACTIVITY_STARTS) { + Slog.d(TAG, "Activity start allowed by caller. " + + state.dump(resultForCaller, resultForRealCaller)); + } + // return the realCaller result for backwards compatibility + return statsLog(resultForRealCaller, state); + } if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { Slog.wtf(TAG, "With Android 15 BAL hardening this activity start may be blocked" @@ -632,6 +641,14 @@ public class BackgroundActivityStartController { } if (callerCanAllow) { // Allowed before V by creator + if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) { + // Will be allowed even with BAL hardening. + if (DEBUG_ACTIVITY_STARTS) { + Slog.d(TAG, "Activity start allowed by caller. " + + state.dump(resultForCaller, resultForRealCaller)); + } + return statsLog(resultForCaller, state); + } if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { Slog.wtf(TAG, "With Android 15 BAL hardening this activity start may be blocked" diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java new file mode 100644 index 000000000000..cdae8c6ab004 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.job.controllers; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; +import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED; + +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.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManagerInternal; +import android.app.AppGlobals; +import android.app.job.JobInfo; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.net.Uri; +import android.os.UserHandle; +import android.platform.test.flag.junit.SetFlagsRule; +import android.util.ArraySet; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.AppStateTracker; +import com.android.server.AppStateTrackerImpl; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobStore; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +@RunWith(AndroidJUnit4.class) +public class BackgroundJobsControllerTest { + private static final int CALLING_UID = 1000; + private static final String CALLING_PACKAGE = "com.test.calling.package"; + private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests"; + private static final int SOURCE_UID = 10001; + private static final int ALTERNATE_UID = 12345; + private static final String ALTERNATE_SOURCE_PACKAGE = "com.test.alternate.package"; + private static final int SOURCE_USER_ID = 0; + + private BackgroundJobsController mBackgroundJobsController; + private BroadcastReceiver mStoppedReceiver; + private JobStore mJobStore; + + private MockitoSession mMockingSession; + @Mock + private Context mContext; + @Mock + private AppStateTrackerImpl mAppStateTrackerImpl; + @Mock + private IPackageManager mIPackageManager; + @Mock + private JobSchedulerService mJobSchedulerService; + @Mock + private PackageManagerInternal mPackageManagerInternal; + @Mock + private PackageManager mPackageManager; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + mMockingSession = mockitoSession() + .initMocks(this) + .mockStatic(AppGlobals.class) + .mockStatic(LocalServices.class) + .strictness(Strictness.LENIENT) + .startMocking(); + + // Called in StateController constructor. + when(mJobSchedulerService.getTestableContext()).thenReturn(mContext); + when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService); + // Called in BackgroundJobsController constructor. + doReturn(mock(ActivityManagerInternal.class)) + .when(() -> LocalServices.getService(ActivityManagerInternal.class)); + doReturn(mAppStateTrackerImpl) + .when(() -> LocalServices.getService(AppStateTracker.class)); + doReturn(mPackageManagerInternal) + .when(() -> LocalServices.getService(PackageManagerInternal.class)); + mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir()); + when(mJobSchedulerService.getJobStore()).thenReturn(mJobStore); + // Called in JobStatus constructor. + doReturn(mIPackageManager).when(AppGlobals::getPackageManager); + + doReturn(false).when(mAppStateTrackerImpl) + .areJobsRestricted(anyInt(), anyString(), anyBoolean()); + doReturn(true).when(mAppStateTrackerImpl) + .isRunAnyInBackgroundAppOpsAllowed(anyInt(), anyString()); + + // Initialize real objects. + // Capture the listeners. + ArgumentCaptor<BroadcastReceiver> receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + mBackgroundJobsController = new BackgroundJobsController(mJobSchedulerService); + mBackgroundJobsController.startTrackingLocked(); + + verify(mContext).registerReceiverAsUser(receiverCaptor.capture(), any(), + ArgumentMatchers.argThat(filter -> + filter.hasAction(Intent.ACTION_PACKAGE_RESTARTED) + && filter.hasAction(Intent.ACTION_PACKAGE_UNSTOPPED)), + any(), any()); + mStoppedReceiver = receiverCaptor.getValue(); + + // Need to do this since we're using a mock JS and not a real object. + doReturn(new ArraySet<>(new String[]{SOURCE_PACKAGE})) + .when(mJobSchedulerService).getPackagesForUidLocked(SOURCE_UID); + doReturn(new ArraySet<>(new String[]{ALTERNATE_SOURCE_PACKAGE})) + .when(mJobSchedulerService).getPackagesForUidLocked(ALTERNATE_UID); + setPackageUid(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE); + setPackageUid(SOURCE_UID, SOURCE_PACKAGE); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + private void setPackageUid(final int uid, final String pkgName) throws Exception { + doReturn(uid).when(mIPackageManager) + .getPackageUid(eq(pkgName), anyLong(), eq(UserHandle.getUserId(uid))); + } + + private void setStoppedState(int uid, String pkgName, boolean stopped) { + Intent intent = new Intent( + stopped ? Intent.ACTION_PACKAGE_RESTARTED : Intent.ACTION_PACKAGE_UNSTOPPED); + intent.putExtra(Intent.EXTRA_UID, uid); + intent.setData(Uri.fromParts(IntentFilter.SCHEME_PACKAGE, pkgName, null)); + mStoppedReceiver.onReceive(mContext, intent); + } + + private void setUidBias(int uid, int bias) { + int prevBias = mJobSchedulerService.getUidBias(uid); + doReturn(bias).when(mJobSchedulerService).getUidBias(uid); + synchronized (mBackgroundJobsController.mLock) { + mBackgroundJobsController.onUidBiasChangedLocked(uid, prevBias, bias); + } + } + + private void trackJobs(JobStatus... jobs) { + for (JobStatus job : jobs) { + mJobStore.add(job); + synchronized (mBackgroundJobsController.mLock) { + mBackgroundJobsController.maybeStartTrackingJobLocked(job, null); + } + } + } + + private JobInfo.Builder createBaseJobInfoBuilder(String pkgName, int jobId) { + final ComponentName cn = spy(new ComponentName(pkgName, "TestBJCJobService")); + doReturn("TestBJCJobService").when(cn).flattenToShortString(); + return new JobInfo.Builder(jobId, cn); + } + + private JobStatus createJobStatus(String testTag, String packageName, int callingUid, + JobInfo jobInfo) { + JobStatus js = JobStatus.createFromJobInfo( + jobInfo, callingUid, packageName, SOURCE_USER_ID, "BJCTest", testTag); + js.serviceProcessName = "testProcess"; + // Make sure tests aren't passing just because the default bucket is likely ACTIVE. + js.setStandbyBucket(FREQUENT_INDEX); + return js; + } + + @Test + public void testStopped_disabled() { + mSetFlagsRule.disableFlags(android.content.pm.Flags.FLAG_STAY_STOPPED); + // Scheduled by SOURCE_UID:SOURCE_PACKAGE for itself. + JobStatus directJob1 = createJobStatus("testStopped", SOURCE_PACKAGE, SOURCE_UID, + createBaseJobInfoBuilder(SOURCE_PACKAGE, 1).build()); + // Scheduled by ALTERNATE_UID:ALTERNATE_SOURCE_PACKAGE for itself. + JobStatus directJob2 = createJobStatus("testStopped", + ALTERNATE_SOURCE_PACKAGE, ALTERNATE_UID, + createBaseJobInfoBuilder(ALTERNATE_SOURCE_PACKAGE, 2).build()); + // Scheduled by CALLING_PACKAGE for SOURCE_PACKAGE. + JobStatus proxyJob1 = createJobStatus("testStopped", SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(CALLING_PACKAGE, 3).build()); + // Scheduled by CALLING_PACKAGE for ALTERNATE_SOURCE_PACKAGE. + JobStatus proxyJob2 = createJobStatus("testStopped", + ALTERNATE_SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(CALLING_PACKAGE, 4).build()); + + trackJobs(directJob1, directJob2, proxyJob1, proxyJob2); + + setStoppedState(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE, true); + assertTrue(directJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob1.isUserBgRestricted()); + assertTrue(directJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob2.isUserBgRestricted()); + assertTrue(proxyJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob1.isUserBgRestricted()); + assertTrue(proxyJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob2.isUserBgRestricted()); + + setStoppedState(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE, false); + assertTrue(directJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob1.isUserBgRestricted()); + assertTrue(directJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob2.isUserBgRestricted()); + assertTrue(proxyJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob1.isUserBgRestricted()); + assertTrue(proxyJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob2.isUserBgRestricted()); + } + + @Test + public void testStopped_enabled() { + mSetFlagsRule.enableFlags(android.content.pm.Flags.FLAG_STAY_STOPPED); + // Scheduled by SOURCE_UID:SOURCE_PACKAGE for itself. + JobStatus directJob1 = createJobStatus("testStopped", SOURCE_PACKAGE, SOURCE_UID, + createBaseJobInfoBuilder(SOURCE_PACKAGE, 1).build()); + // Scheduled by ALTERNATE_UID:ALTERNATE_SOURCE_PACKAGE for itself. + JobStatus directJob2 = createJobStatus("testStopped", + ALTERNATE_SOURCE_PACKAGE, ALTERNATE_UID, + createBaseJobInfoBuilder(ALTERNATE_SOURCE_PACKAGE, 2).build()); + // Scheduled by CALLING_PACKAGE for SOURCE_PACKAGE. + JobStatus proxyJob1 = createJobStatus("testStopped", SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(CALLING_PACKAGE, 3).build()); + // Scheduled by CALLING_PACKAGE for ALTERNATE_SOURCE_PACKAGE. + JobStatus proxyJob2 = createJobStatus("testStopped", + ALTERNATE_SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(CALLING_PACKAGE, 4).build()); + + trackJobs(directJob1, directJob2, proxyJob1, proxyJob2); + + setStoppedState(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE, true); + assertTrue(directJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob1.isUserBgRestricted()); + assertFalse(directJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertTrue(directJob2.isUserBgRestricted()); + assertTrue(proxyJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob1.isUserBgRestricted()); + assertFalse(proxyJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertTrue(proxyJob2.isUserBgRestricted()); + + setStoppedState(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE, false); + assertTrue(directJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob1.isUserBgRestricted()); + assertTrue(directJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob2.isUserBgRestricted()); + assertTrue(proxyJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob1.isUserBgRestricted()); + assertTrue(proxyJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob2.isUserBgRestricted()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 800350a7d326..57c3a1d5f364 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -21,6 +21,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; +import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; @@ -852,6 +853,53 @@ public class AccessibilityManagerServiceTest { assertThat(lockState.get()).containsExactly(false); } + @Test + @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) + public void testIsAccessibilityServiceWarningRequired_requiredByDefault() { + mockManageAccessibilityGranted(mTestableContext); + final AccessibilityServiceInfo info = new AccessibilityServiceInfo(); + info.setComponentName(COMPONENT_NAME); + + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info)).isTrue(); + } + + @Test + @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) + public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() { + mockManageAccessibilityGranted(mTestableContext); + final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo(); + info_a.setComponentName(COMPONENT_NAME); + final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); + info_b.setComponentName(new ComponentName("package_b", "class_b")); + final AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mEnabledServices.clear(); + userState.mEnabledServices.add(info_b.getComponentName()); + + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_a)).isTrue(); + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_b)).isFalse(); + } + + @Test + @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) + public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() { + mockManageAccessibilityGranted(mTestableContext); + final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo(); + info_a.setComponentName(new ComponentName("package_a", "class_a")); + final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); + info_b.setComponentName(new ComponentName("package_b", "class_b")); + final AccessibilityServiceInfo info_c = new AccessibilityServiceInfo(); + info_c.setComponentName(new ComponentName("package_c", "class_c")); + final AccessibilityUserState userState = mA11yms.getCurrentUserState(); + userState.mAccessibilityButtonTargets.clear(); + userState.mAccessibilityButtonTargets.add(info_b.getComponentName().flattenToString()); + userState.mAccessibilityShortcutKeyTargets.clear(); + userState.mAccessibilityShortcutKeyTargets.add(info_c.getComponentName().flattenToString()); + + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_a)).isTrue(); + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_b)).isFalse(); + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse(); + } + // Single package intents can trigger multiple PackageMonitor callbacks. // Collect the state of the lock in a set, since tests only care if calls // were all locked or all unlocked. diff --git a/services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java deleted file mode 100644 index 44d676052352..000000000000 --- a/services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java +++ /dev/null @@ -1,389 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.am; - -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 static org.junit.Assert.fail; - -import android.platform.test.annotations.Presubmit; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; - -import android.util.Log; - -import androidx.test.filters.SmallTest; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * Build/Install/Run: - * atest FrameworksServicesTests:AnrTimerTest - */ -@SmallTest -@Presubmit -public class AnrTimerTest { - - /** - * A handler that allows control over when to dispatch messages and callbacks. Because most - * Handler methods are final, the only thing this handler can intercept is sending messages. - * This handler allows unit tests to be written without a need to sleep (which leads to flaky - * tests). - * - * This code was cloned from {@link com.android.systemui.utils.os.FakeHandler}. - */ - static class TestHandler extends Handler { - - private boolean mImmediate = true; - private ArrayList<Message> mQueuedMessages = new ArrayList<>(); - - ArrayList<Long> mDelays = new ArrayList<>(); - - TestHandler(Looper looper, Callback callback, boolean immediate) { - super(looper, callback); - mImmediate = immediate; - } - - TestHandler(Looper looper, Callback callback) { - this(looper, callback, true); - } - - /** - * Override sendMessageAtTime. In immediate mode, the message is immediately dispatched. - * In non-immediate mode, the message is enqueued to the real handler. In both cases, the - * original delay is computed by comparing the target dispatch time with 'now'. This - * computation is prone to errors if the code experiences delays. The computed time is - * captured in the mDelays list. - */ - @Override - public boolean sendMessageAtTime(Message msg, long uptimeMillis) { - long delay = uptimeMillis - SystemClock.uptimeMillis(); - mDelays.add(delay); - if (mImmediate) { - mQueuedMessages.add(msg); - dispatchQueuedMessages(); - } else { - super.sendMessageAtTime(msg, uptimeMillis); - } - return true; - } - - void setImmediate(boolean immediate) { - mImmediate = immediate; - } - - /** Dispatch any messages that have been queued on the calling thread. */ - void dispatchQueuedMessages() { - ArrayList<Message> messages = new ArrayList<>(mQueuedMessages); - mQueuedMessages.clear(); - for (Message msg : messages) { - dispatchMessage(msg); - } - } - - /** - * Compare the captured delays with the input array. The comparison is fuzzy because the - * captured delay (see sendMessageAtTime) is affected by process delays. - */ - void verifyDelays(long[] r) { - final long FUZZ = 10; - assertEquals(r.length, mDelays.size()); - for (int i = 0; i < mDelays.size(); i++) { - long t = r[i]; - long v = mDelays.get(i); - assertTrue(v >= t - FUZZ && v <= t + FUZZ); - } - } - } - - private Handler mHandler; - private CountDownLatch mLatch = null; - private ArrayList<Message> mMessages; - - // The commonly used message timeout key. - private static final int MSG_TIMEOUT = 1; - - @Before - public void setUp() { - mHandler = new Handler(Looper.getMainLooper(), this::expirationHandler); - mMessages = new ArrayList<>(); - mLatch = new CountDownLatch(1); - AnrTimer.resetTimerListForHermeticTest(); - } - - @After - public void tearDown() { - mHandler = null; - mMessages = null; - } - - // When a timer expires, set the expiration time in the message and add it to the queue. - private boolean expirationHandler(Message msg) { - mMessages.add(Message.obtain(msg)); - mLatch.countDown(); - return false; - } - - // The test argument includes a pid and uid, and a tag. The tag is used to distinguish - // different message instances. - private static class TestArg { - final int pid; - final int uid; - final int tag; - - TestArg(int pid, int uid, int tag) { - this.pid = pid; - this.uid = uid; - this.tag = tag; - } - @Override - public String toString() { - return String.format("pid=%d uid=%d tag=%d", pid, uid, tag); - } - } - - /** - * An instrumented AnrTimer. - */ - private class TestAnrTimer extends AnrTimer { - // A local copy of 'what'. The field in AnrTimer is private. - final int mWhat; - - TestAnrTimer(Handler h, int key, String tag) { - super(h, key, tag); - mWhat = key; - } - - TestAnrTimer() { - this(mHandler, MSG_TIMEOUT, caller()); - } - - TestAnrTimer(Handler h, int key, String tag, boolean extend, TestInjector injector) { - super(h, key, tag, extend, injector); - mWhat = key; - } - - TestAnrTimer(boolean extend, TestInjector injector) { - this(mHandler, MSG_TIMEOUT, caller(), extend, injector); - } - - // Return the name of method that called the constructor, assuming that this function is - // called from inside the constructor. The calling method is used to name the AnrTimer - // instance so that logs are easier to understand. - private static String caller() { - final int n = 4; - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - if (stack.length < n+1) return "test"; - return stack[n].getMethodName(); - } - - boolean start(TestArg arg, long millis) { - return start(arg, arg.pid, arg.uid, millis); - } - - int what() { - return mWhat; - } - } - - private static class TestTracker extends AnrTimer.CpuTracker { - long index = 0; - final int skip; - TestTracker(int skip) { - this.skip = skip; - } - long delay(int pid) { - return index++ * skip; - } - } - - private class TestInjector extends AnrTimer.Injector { - final boolean mImmediate; - final AnrTimer.CpuTracker mTracker; - TestHandler mTestHandler; - - TestInjector(int skip, boolean immediate) { - super(mHandler); - mTracker = new TestTracker(skip); - mImmediate = immediate; - } - - TestInjector(int skip) { - this(skip, true); - } - - @Override - Handler newHandler(Handler.Callback callback) { - if (mTestHandler == null) { - mTestHandler = new TestHandler(mHandler.getLooper(), callback, mImmediate); - } - return mTestHandler; - } - - /** Fetch the allocated handle. This does not check for nulls. */ - TestHandler getHandler() { - return mTestHandler; - } - - /** - * This override returns the tracker supplied in the constructor. It does not create a - * new one. - */ - @Override - AnrTimer.CpuTracker newTracker() { - return mTracker; - } - - /** For test purposes, always enable the feature. */ - @Override - boolean isFeatureEnabled() { - return true; - } - } - - // Tests - // 1. Start a timer and wait for expiration. - // 2. Start a timer and cancel it. Verify no expiration. - // 3. Start a timer. Shortly thereafter, restart it. Verify only one expiration. - // 4. Start a couple of timers. Verify max active timers. Discard one and verify the active - // count drops by 1. Accept one and verify the active count drops by 1. - - @Test - public void testSimpleTimeout() throws Exception { - // Create an immediate TestHandler. - TestInjector injector = new TestInjector(0); - TestAnrTimer timer = new TestAnrTimer(false, injector); - TestArg t = new TestArg(1, 1, 3); - assertTrue(timer.start(t, 10)); - // Delivery is immediate but occurs on a different thread. - assertTrue(mLatch.await(100, TimeUnit.MILLISECONDS)); - assertEquals(1, mMessages.size()); - Message m = mMessages.get(0); - assertEquals(timer.what(), m.what); - assertEquals(t, m.obj); - - // Verify that the timer is still present. - assertEquals(1, AnrTimer.sizeOfTimerList()); - assertTrue(timer.accept(t)); - assertEquals(0, AnrTimer.sizeOfTimerList()); - - // Verify that the timer no longer exists. - assertFalse(timer.accept(t)); - } - - @Test - public void testCancel() throws Exception { - // Create an non-immediate TestHandler. - TestInjector injector = new TestInjector(0, false); - TestAnrTimer timer = new TestAnrTimer(false, injector); - - Handler handler = injector.getHandler(); - assertNotNull(handler); - assertTrue(handler instanceof TestHandler); - - // The tests that follow check for a 'what' of 0 (zero), which is the message key used - // by AnrTimer internally. - TestArg t = new TestArg(1, 1, 3); - assertFalse(handler.hasMessages(0)); - assertTrue(timer.start(t, 100)); - assertTrue(handler.hasMessages(0)); - assertTrue(timer.cancel(t)); - assertFalse(handler.hasMessages(0)); - - // Verify that no expiration messages were delivered. - assertEquals(0, mMessages.size()); - assertEquals(0, AnrTimer.sizeOfTimerList()); - } - - @Test - public void testRestart() throws Exception { - // Create an non-immediate TestHandler. - TestInjector injector = new TestInjector(0, false); - TestAnrTimer timer = new TestAnrTimer(false, injector); - - TestArg t = new TestArg(1, 1, 3); - assertTrue(timer.start(t, 2500)); - assertTrue(timer.start(t, 1000)); - - // Verify that the test handler saw two timeouts. - injector.getHandler().verifyDelays(new long[] { 2500, 1000 }); - - // Verify that there is a single timer. Then cancel it. - assertEquals(1, AnrTimer.sizeOfTimerList()); - assertTrue(timer.cancel(t)); - assertEquals(0, AnrTimer.sizeOfTimerList()); - } - - @Test - public void testExtendNormal() throws Exception { - // Create an immediate TestHandler. - TestInjector injector = new TestInjector(5); - TestAnrTimer timer = new TestAnrTimer(true, injector); - TestArg t = new TestArg(1, 1, 3); - assertTrue(timer.start(t, 10)); - - assertTrue(mLatch.await(100, TimeUnit.MILLISECONDS)); - assertEquals(1, mMessages.size()); - Message m = mMessages.get(0); - assertEquals(timer.what(), m.what); - assertEquals(t, m.obj); - - // Verify that the test handler saw two timeouts: one of 10ms and one of 5ms. - injector.getHandler().verifyDelays(new long[] { 10, 5 }); - - // Verify that the timer is still present. Then remove it and verify that the list is - // empty. - assertEquals(1, AnrTimer.sizeOfTimerList()); - assertTrue(timer.accept(t)); - assertEquals(0, AnrTimer.sizeOfTimerList()); - } - - @Test - public void testExtendOversize() throws Exception { - // Create an immediate TestHandler. - TestInjector injector = new TestInjector(25); - TestAnrTimer timer = new TestAnrTimer(true, injector); - TestArg t = new TestArg(1, 1, 3); - assertTrue(timer.start(t, 10)); - - assertTrue(mLatch.await(100, TimeUnit.MILLISECONDS)); - assertEquals(1, mMessages.size()); - Message m = mMessages.get(0); - assertEquals(timer.what(), m.what); - assertEquals(t, m.obj); - - // Verify that the test handler saw two timeouts: one of 10ms and one of 10ms. - injector.getHandler().verifyDelays(new long[] { 10, 10 }); - - // Verify that the timer is still present. Then remove it and verify that the list is - // empty. - assertEquals(1, AnrTimer.sizeOfTimerList()); - assertTrue(timer.accept(t)); - assertEquals(0, AnrTimer.sizeOfTimerList()); - } -} diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java index 0d58542ab040..cc3c880b8927 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java @@ -15,14 +15,28 @@ */ package com.android.server.audio; +import static android.bluetooth.BluetoothDevice.DEVICE_TYPE_DEFAULT; +import static android.bluetooth.BluetoothDevice.DEVICE_TYPE_HEADSET; +import static android.media.audio.Flags.automaticBtDeviceType; + +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 static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; @@ -40,10 +54,10 @@ import androidx.test.filters.MediumTest; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.Spy; @@ -70,6 +84,9 @@ public class AudioDeviceBrokerTest { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); mMockAudioService = mock(AudioService.class); + SettingsAdapter mockAdapter = mock(SettingsAdapter.class); + when(mMockAudioService.getSettings()).thenReturn(mockAdapter); + when(mockAdapter.getSecureStringForUser(any(), any(), anyInt())).thenReturn(""); when(mMockAudioService.getBluetoothContextualVolumeStream()) .thenReturn(AudioSystem.STREAM_MUSIC); @@ -82,7 +99,6 @@ public class AudioDeviceBrokerTest { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05"); - Assert.assertNotNull("invalid null BT device", mFakeBtDevice); } @After @@ -100,15 +116,14 @@ public class AudioDeviceBrokerTest { @Test public void testPostA2dpDeviceConnectionChange() throws Exception { Log.i(TAG, "starting testPostA2dpDeviceConnectionChange"); - Assert.assertNotNull("invalid null BT device", mFakeBtDevice); + assertNotNull("invalid null BT device", mFakeBtDevice); mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged( new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null, BluetoothProfileConnectionInfo.createA2dpInfo(true, 1), "testSource")); Thread.sleep(2 * MAX_MESSAGE_HANDLING_DELAY_MS); verify(mSpyDevInventory, times(1)).setBluetoothActiveDevice( - any(AudioDeviceBroker.BtDeviceInfo.class) - ); + any(AudioDeviceBroker.BtDeviceInfo.class)); // verify the connection was reported to AudioSystem checkSingleSystemConnection(mFakeBtDevice); @@ -212,7 +227,7 @@ public class AudioDeviceBrokerTest { AudioManager.DEVICE_OUT_SPEAKER, null); new AdiDeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, null); - Assert.fail(); + fail(); } catch (NullPointerException e) { } } @@ -228,11 +243,114 @@ public class AudioDeviceBrokerTest { final AdiDeviceState result = AdiDeviceState.fromPersistedString(persistString); Log.i(TAG, "original:" + devState); Log.i(TAG, "result :" + result); - Assert.assertEquals(devState, result); + assertEquals(devState, result); + } + + @Test + public void testIsBluetoothAudioDeviceCategoryFixed() throws Exception { + Log.i(TAG, "starting testIsBluetoothAudioDeviceCategoryFixed"); + + if (!automaticBtDeviceType()) { + Log.i(TAG, "Enable automaticBtDeviceType flag to run the test " + + "testIsBluetoothAudioDeviceCategoryFixed"); + return; + } + assertNotNull("invalid null BT device", mFakeBtDevice); + + final AdiDeviceState devState = new AdiDeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, mFakeBtDevice.getAddress()); + doReturn(devState).when(mSpyDevInventory).findBtDeviceStateForAddress( + mFakeBtDevice.getAddress(), AudioManager.DEVICE_OUT_BLUETOOTH_A2DP); + try { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .adoptShellPermissionIdentity(Manifest.permission.BLUETOOTH_PRIVILEGED); + + // no metadata set + assertTrue(mFakeBtDevice.setMetadata(BluetoothDevice.METADATA_DEVICE_TYPE, + DEVICE_TYPE_DEFAULT.getBytes())); + assertFalse( + mAudioDeviceBroker.isBluetoothAudioDeviceCategoryFixed( + mFakeBtDevice.getAddress())); + + // metadata set + assertTrue(mFakeBtDevice.setMetadata(BluetoothDevice.METADATA_DEVICE_TYPE, + DEVICE_TYPE_HEADSET.getBytes())); + assertTrue(mAudioDeviceBroker.isBluetoothAudioDeviceCategoryFixed( + mFakeBtDevice.getAddress())); + } finally { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .dropShellPermissionIdentity(); + } + } + + @Test + public void testGetAndUpdateBtAdiDeviceStateCategoryForAddress() throws Exception { + Log.i(TAG, "starting testGetAndUpdateBtAdiDeviceStateCategoryForAddress"); + + if (!automaticBtDeviceType()) { + Log.i(TAG, "Enable automaticBtDeviceType flag to run the test " + + "testGetAndUpdateBtAdiDeviceStateCategoryForAddress"); + return; + } + assertNotNull("invalid null BT device", mFakeBtDevice); + + final AdiDeviceState devState = new AdiDeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, mFakeBtDevice.getAddress()); + devState.setAudioDeviceCategory(AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER); + doReturn(devState).when(mSpyDevInventory).findBtDeviceStateForAddress( + eq(mFakeBtDevice.getAddress()), anyInt()); + try { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .adoptShellPermissionIdentity(Manifest.permission.BLUETOOTH_PRIVILEGED); + + // no metadata set + assertTrue(mFakeBtDevice.setMetadata(BluetoothDevice.METADATA_DEVICE_TYPE, + DEVICE_TYPE_DEFAULT.getBytes())); + assertEquals(AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER, + mAudioDeviceBroker.getAndUpdateBtAdiDeviceStateCategoryForAddress( + mFakeBtDevice.getAddress())); + verify(mMockAudioService, + timeout(MAX_MESSAGE_HANDLING_DELAY_MS).times(0)).onUpdatedAdiDeviceState( + eq(devState)); + + // metadata set + assertTrue(mFakeBtDevice.setMetadata(BluetoothDevice.METADATA_DEVICE_TYPE, + DEVICE_TYPE_HEADSET.getBytes())); + assertEquals(AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES, + mAudioDeviceBroker.getAndUpdateBtAdiDeviceStateCategoryForAddress( + mFakeBtDevice.getAddress())); + verify(mMockAudioService, + timeout(MAX_MESSAGE_HANDLING_DELAY_MS)).onUpdatedAdiDeviceState( + any()); + } finally { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .dropShellPermissionIdentity(); + } + } + + @Test + public void testAddAudioDeviceWithCategoryInInventoryIfNeeded() throws Exception { + Log.i(TAG, "starting testAddAudioDeviceWithCategoryInInventoryIfNeeded"); + + if (!automaticBtDeviceType()) { + Log.i(TAG, "Enable automaticBtDeviceType flag to run the test " + + "testAddAudioDeviceWithCategoryInInventoryIfNeeded"); + return; + } + assertNotNull("invalid null BT device", mFakeBtDevice); + + mAudioDeviceBroker.addAudioDeviceWithCategoryInInventoryIfNeeded( + mFakeBtDevice.getAddress(), AudioManager.AUDIO_DEVICE_CATEGORY_OTHER); + + verify(mMockAudioService, + timeout(MAX_MESSAGE_HANDLING_DELAY_MS).atLeast(1)).onUpdatedAdiDeviceState( + ArgumentMatchers.argThat(devState -> devState.getAudioDeviceCategory() + == AudioManager.AUDIO_DEVICE_CATEGORY_OTHER)); } private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection, boolean mockMediaPlayback, boolean guaranteeSingleConnection) throws Exception { + assertNotNull("invalid null BT device", mFakeBtDevice); when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC)) .thenReturn(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); when(mMockAudioService.isInCommunication()).thenReturn(false); diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java new file mode 100644 index 000000000000..330dbb83e949 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import android.platform.test.annotations.Presubmit; +import androidx.test.filters.SmallTest; + +import com.android.internal.annotations.GuardedBy; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@SmallTest +@Presubmit +public class AnrTimerTest { + + // The commonly used message timeout key. + private static final int MSG_TIMEOUT = 1; + + // The test argument includes a pid and uid, and a tag. The tag is used to distinguish + // different message instances. Additional fields (like what) capture delivery information + // that is checked by the test. + private static class TestArg { + final int pid; + final int uid; + int what; + + TestArg(int pid, int uid) { + this.pid = pid; + this.uid = uid; + this.what = 0; + } + } + + /** + * The test handler is a self-contained object for a single test. + */ + private static class Helper { + final Object mLock = new Object(); + + final Handler mHandler; + final CountDownLatch mLatch; + @GuardedBy("mLock") + final ArrayList<TestArg> mMessages; + + Helper(int expect) { + mHandler = new Handler(Looper.getMainLooper(), this::expirationHandler); + mMessages = new ArrayList<>(); + mLatch = new CountDownLatch(expect); + } + + /** + * When a timer expires, the object must be a TestArg. Update the TestArg with + * expiration metadata and save it. + */ + private boolean expirationHandler(Message msg) { + synchronized (mLock) { + TestArg arg = (TestArg) msg.obj; + arg.what = msg.what; + mMessages.add(arg); + mLatch.countDown(); + return false; + } + } + + boolean await(long timeout) throws InterruptedException { + // No need to synchronize, as the CountDownLatch is already thread-safe. + return mLatch.await(timeout, TimeUnit.MILLISECONDS); + } + + /** + * Fetch the received messages. Fail if the count of received messages is other than the + * expected count. + */ + TestArg[] messages(int expected) { + synchronized (mLock) { + assertEquals(expected, mMessages.size()); + return mMessages.toArray(new TestArg[expected]); + } + } + } + + /** + * An instrumented AnrTimer. + */ + private static class TestAnrTimer extends AnrTimer<TestArg> { + private TestAnrTimer(Handler h, int key, String tag) { + super(h, key, tag); + } + + TestAnrTimer(Helper helper) { + this(helper.mHandler, MSG_TIMEOUT, caller()); + } + + void start(TestArg arg, long millis) { + start(arg, arg.pid, arg.uid, millis); + } + + // Return the name of method that called the constructor, assuming that this function is + // called from inside the constructor. The calling method is used to name the AnrTimer + // instance so that logs are easier to understand. + private static String caller() { + final int n = 4; + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + if (stack.length < n+1) return "test"; + return stack[n].getMethodName(); + } + } + + void validate(TestArg expected, TestArg actual) { + assertEquals(expected, actual); + assertEquals(actual.what, MSG_TIMEOUT); + } + + + /** + * Verify that a simple expiration succeeds. The timer is started for 10ms. The test + * procedure waits 5s for the expiration message, but under correct operation, the test will + * only take 10ms + */ + @Test + public void testSimpleTimeout() throws Exception { + Helper helper = new Helper(1); + TestAnrTimer timer = new TestAnrTimer(helper); + TestArg t = new TestArg(1, 1); + timer.start(t, 10); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(1); + validate(t, result[0]); + } + + /** + * Verify that if three timers are scheduled, they are delivered in time order. + */ + @Test + public void testMultipleTimers() throws Exception { + // Expect three messages. + Helper helper = new Helper(3); + TestAnrTimer timer = new TestAnrTimer(helper); + TestArg t1 = new TestArg(1, 1); + TestArg t2 = new TestArg(1, 2); + TestArg t3 = new TestArg(1, 3); + timer.start(t1, 50); + timer.start(t2, 60); + timer.start(t3, 40); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(3); + validate(t3, result[0]); + validate(t1, result[1]); + validate(t2, result[2]); + } + + /** + * Verify that a canceled timer is not delivered. + */ + @Test + public void testCancelTimer() throws Exception { + // Expect two messages. + Helper helper = new Helper(2); + TestAnrTimer timer = new TestAnrTimer(helper); + TestArg t1 = new TestArg(1, 1); + TestArg t2 = new TestArg(1, 2); + TestArg t3 = new TestArg(1, 3); + timer.start(t1, 50); + timer.start(t2, 60); + timer.start(t3, 40); + // Briefly pause. + assertFalse(helper.await(10)); + timer.cancel(t1); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(2); + validate(t3, result[0]); + validate(t2, result[1]); + } +} diff --git a/tests/FlickerTests/ActivityEmbedding/Android.bp b/tests/FlickerTests/ActivityEmbedding/Android.bp index 9eeec7c8ddda..2cdf54248ebc 100644 --- a/tests/FlickerTests/ActivityEmbedding/Android.bp +++ b/tests/FlickerTests/ActivityEmbedding/Android.bp @@ -29,6 +29,8 @@ android_test { manifest: "AndroidManifest.xml", package_name: "com.android.server.wm.flicker", instrumentation_target_package: "com.android.server.wm.flicker", + test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], static_libs: ["FlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidManifest.xml b/tests/FlickerTests/ActivityEmbedding/AndroidManifest.xml index f867ffb679c5..6f8f008cf85b 100644 --- a/tests/FlickerTests/ActivityEmbedding/AndroidManifest.xml +++ b/tests/FlickerTests/ActivityEmbedding/AndroidManifest.xml @@ -17,7 +17,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - package="com.android.server.wm.flick"> + package="com.android.server.wm.flicker"> <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> <!-- Read and write traces from external storage --> diff --git a/tests/FlickerTests/ActivityEmbedding/trace_config/trace_config.textproto b/tests/FlickerTests/ActivityEmbedding/trace_config/trace_config.textproto index c9a35aca9085..c4edc1a5c0f7 100644 --- a/tests/FlickerTests/ActivityEmbedding/trace_config/trace_config.textproto +++ b/tests/FlickerTests/ActivityEmbedding/trace_config/trace_config.textproto @@ -62,12 +62,6 @@ data_sources: { atrace_categories: "binder_driver" atrace_categories: "sched_process_exit" atrace_apps: "com.android.server.wm.flicker" - atrace_apps: "com.android.server.wm.flicker.other" - atrace_apps: "com.android.server.wm.flicker.close" - atrace_apps: "com.android.server.wm.flicker.ime" - atrace_apps: "com.android.server.wm.flicker.launch" - atrace_apps: "com.android.server.wm.flicker.quickswitch" - atrace_apps: "com.android.server.wm.flicker.rotation" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" atrace_apps: "com.google.android.apps.nexuslauncher" diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp index 514f89511fb1..1d71f95ef64f 100644 --- a/tests/FlickerTests/Android.bp +++ b/tests/FlickerTests/Android.bp @@ -23,13 +23,6 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -filegroup { - name: "FlickerServiceTests-src", - srcs: [ - "src/**/*", - ], -} - java_defaults { name: "FlickerTestsDefault", platform_apis: true, @@ -49,10 +42,7 @@ java_defaults { "wm-flicker-common-app-helpers", "wm-shell-flicker-utils", ], - data: [ - ":FlickerTestApp", - "trace_config/*", - ], + data: [":FlickerTestApp"], } java_library { diff --git a/tests/FlickerTests/AppClose/Android.bp b/tests/FlickerTests/AppClose/Android.bp index 151d12f2a8ca..93fdd652510d 100644 --- a/tests/FlickerTests/AppClose/Android.bp +++ b/tests/FlickerTests/AppClose/Android.bp @@ -30,4 +30,5 @@ android_test { test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], static_libs: ["FlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/tests/FlickerTests/AppClose/trace_config/trace_config.textproto b/tests/FlickerTests/AppClose/trace_config/trace_config.textproto index c9a35aca9085..6a0afc60bc95 100644 --- a/tests/FlickerTests/AppClose/trace_config/trace_config.textproto +++ b/tests/FlickerTests/AppClose/trace_config/trace_config.textproto @@ -61,13 +61,7 @@ data_sources: { atrace_categories: "input" atrace_categories: "binder_driver" atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker" - atrace_apps: "com.android.server.wm.flicker.other" atrace_apps: "com.android.server.wm.flicker.close" - atrace_apps: "com.android.server.wm.flicker.ime" - atrace_apps: "com.android.server.wm.flicker.launch" - atrace_apps: "com.android.server.wm.flicker.quickswitch" - atrace_apps: "com.android.server.wm.flicker.rotation" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" atrace_apps: "com.google.android.apps.nexuslauncher" diff --git a/tests/FlickerTests/AppLaunch/Android.bp b/tests/FlickerTests/AppLaunch/Android.bp index f33384df6e7f..f5e962124b37 100644 --- a/tests/FlickerTests/AppLaunch/Android.bp +++ b/tests/FlickerTests/AppLaunch/Android.bp @@ -50,6 +50,7 @@ android_test { "FlickerTestsBase", "FlickerTestsAppLaunchCommon", ], + data: ["trace_config/*"], } android_test { @@ -66,4 +67,5 @@ android_test { "FlickerTestsBase", "FlickerTestsAppLaunchCommon", ], + data: ["trace_config/*"], } diff --git a/tests/FlickerTests/AppLaunch/trace_config/trace_config.textproto b/tests/FlickerTests/AppLaunch/trace_config/trace_config.textproto index c9a35aca9085..f27177ffee3e 100644 --- a/tests/FlickerTests/AppLaunch/trace_config/trace_config.textproto +++ b/tests/FlickerTests/AppLaunch/trace_config/trace_config.textproto @@ -61,13 +61,7 @@ data_sources: { atrace_categories: "input" atrace_categories: "binder_driver" atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker" - atrace_apps: "com.android.server.wm.flicker.other" - atrace_apps: "com.android.server.wm.flicker.close" - atrace_apps: "com.android.server.wm.flicker.ime" atrace_apps: "com.android.server.wm.flicker.launch" - atrace_apps: "com.android.server.wm.flicker.quickswitch" - atrace_apps: "com.android.server.wm.flicker.rotation" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" atrace_apps: "com.google.android.apps.nexuslauncher" diff --git a/tests/FlickerTests/FlickerService/Android.bp b/tests/FlickerTests/FlickerService/Android.bp index 1a381150dfb0..ef74e942bdba 100644 --- a/tests/FlickerTests/FlickerService/Android.bp +++ b/tests/FlickerTests/FlickerService/Android.bp @@ -30,4 +30,5 @@ android_test { test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], static_libs: ["FlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/tests/FlickerTests/FlickerService/trace_config/trace_config.textproto b/tests/FlickerTests/FlickerService/trace_config/trace_config.textproto index c9a35aca9085..a4f3ecfa8976 100644 --- a/tests/FlickerTests/FlickerService/trace_config/trace_config.textproto +++ b/tests/FlickerTests/FlickerService/trace_config/trace_config.textproto @@ -61,13 +61,7 @@ data_sources: { atrace_categories: "input" atrace_categories: "binder_driver" atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker" - atrace_apps: "com.android.server.wm.flicker.other" - atrace_apps: "com.android.server.wm.flicker.close" - atrace_apps: "com.android.server.wm.flicker.ime" - atrace_apps: "com.android.server.wm.flicker.launch" - atrace_apps: "com.android.server.wm.flicker.quickswitch" - atrace_apps: "com.android.server.wm.flicker.rotation" + atrace_apps: "com.android.server.wm.flicker.service" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" atrace_apps: "com.google.android.apps.nexuslauncher" diff --git a/tests/FlickerTests/IME/Android.bp b/tests/FlickerTests/IME/Android.bp index 057d9fcdb796..1141e5f3ae2f 100644 --- a/tests/FlickerTests/IME/Android.bp +++ b/tests/FlickerTests/IME/Android.bp @@ -40,6 +40,7 @@ android_test { test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], static_libs: ["FlickerTestsBase"], + data: ["trace_config/*"], } java_library { @@ -59,6 +60,7 @@ android_test { "FlickerTestsBase", "FlickerTestsImeCommon", ], + data: ["trace_config/*"], } android_test { @@ -75,4 +77,5 @@ android_test { "FlickerTestsBase", "FlickerTestsImeCommon", ], + data: ["trace_config/*"], } diff --git a/tests/FlickerTests/IME/trace_config/trace_config.textproto b/tests/FlickerTests/IME/trace_config/trace_config.textproto index c9a35aca9085..b722fe5bc00a 100644 --- a/tests/FlickerTests/IME/trace_config/trace_config.textproto +++ b/tests/FlickerTests/IME/trace_config/trace_config.textproto @@ -61,13 +61,7 @@ data_sources: { atrace_categories: "input" atrace_categories: "binder_driver" atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker" - atrace_apps: "com.android.server.wm.flicker.other" - atrace_apps: "com.android.server.wm.flicker.close" atrace_apps: "com.android.server.wm.flicker.ime" - atrace_apps: "com.android.server.wm.flicker.launch" - atrace_apps: "com.android.server.wm.flicker.quickswitch" - atrace_apps: "com.android.server.wm.flicker.rotation" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" atrace_apps: "com.google.android.apps.nexuslauncher" diff --git a/tests/FlickerTests/Notification/Android.bp b/tests/FlickerTests/Notification/Android.bp index 5bed568aacd1..4648383b2771 100644 --- a/tests/FlickerTests/Notification/Android.bp +++ b/tests/FlickerTests/Notification/Android.bp @@ -30,4 +30,5 @@ android_test { test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], static_libs: ["FlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/tests/FlickerTests/Notification/trace_config/trace_config.textproto b/tests/FlickerTests/Notification/trace_config/trace_config.textproto index c9a35aca9085..dc8c88c5b41c 100644 --- a/tests/FlickerTests/Notification/trace_config/trace_config.textproto +++ b/tests/FlickerTests/Notification/trace_config/trace_config.textproto @@ -61,13 +61,7 @@ data_sources: { atrace_categories: "input" atrace_categories: "binder_driver" atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker" - atrace_apps: "com.android.server.wm.flicker.other" - atrace_apps: "com.android.server.wm.flicker.close" - atrace_apps: "com.android.server.wm.flicker.ime" - atrace_apps: "com.android.server.wm.flicker.launch" - atrace_apps: "com.android.server.wm.flicker.quickswitch" - atrace_apps: "com.android.server.wm.flicker.rotation" + atrace_apps: "com.android.server.wm.flicker.notification" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" atrace_apps: "com.google.android.apps.nexuslauncher" diff --git a/tests/FlickerTests/QuickSwitch/Android.bp b/tests/FlickerTests/QuickSwitch/Android.bp index 64f718333a59..8755d0e3b304 100644 --- a/tests/FlickerTests/QuickSwitch/Android.bp +++ b/tests/FlickerTests/QuickSwitch/Android.bp @@ -30,4 +30,5 @@ android_test { test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], static_libs: ["FlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/tests/FlickerTests/QuickSwitch/trace_config/trace_config.textproto b/tests/FlickerTests/QuickSwitch/trace_config/trace_config.textproto index c9a35aca9085..cd70ad59f1ed 100644 --- a/tests/FlickerTests/QuickSwitch/trace_config/trace_config.textproto +++ b/tests/FlickerTests/QuickSwitch/trace_config/trace_config.textproto @@ -61,13 +61,7 @@ data_sources: { atrace_categories: "input" atrace_categories: "binder_driver" atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker" - atrace_apps: "com.android.server.wm.flicker.other" - atrace_apps: "com.android.server.wm.flicker.close" - atrace_apps: "com.android.server.wm.flicker.ime" - atrace_apps: "com.android.server.wm.flicker.launch" atrace_apps: "com.android.server.wm.flicker.quickswitch" - atrace_apps: "com.android.server.wm.flicker.rotation" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" atrace_apps: "com.google.android.apps.nexuslauncher" diff --git a/tests/FlickerTests/Rotation/Android.bp b/tests/FlickerTests/Rotation/Android.bp index 8e93b5b340c4..233a27691e21 100644 --- a/tests/FlickerTests/Rotation/Android.bp +++ b/tests/FlickerTests/Rotation/Android.bp @@ -30,4 +30,5 @@ android_test { test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], static_libs: ["FlickerTestsBase"], + data: ["trace_config/*"], } diff --git a/tests/FlickerTests/Rotation/trace_config/trace_config.textproto b/tests/FlickerTests/Rotation/trace_config/trace_config.textproto index c9a35aca9085..eeb542f2a349 100644 --- a/tests/FlickerTests/Rotation/trace_config/trace_config.textproto +++ b/tests/FlickerTests/Rotation/trace_config/trace_config.textproto @@ -61,12 +61,6 @@ data_sources: { atrace_categories: "input" atrace_categories: "binder_driver" atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker" - atrace_apps: "com.android.server.wm.flicker.other" - atrace_apps: "com.android.server.wm.flicker.close" - atrace_apps: "com.android.server.wm.flicker.ime" - atrace_apps: "com.android.server.wm.flicker.launch" - atrace_apps: "com.android.server.wm.flicker.quickswitch" atrace_apps: "com.android.server.wm.flicker.rotation" atrace_apps: "com.android.server.wm.flicker.testapp" atrace_apps: "com.android.systemui" diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt index ad272a052220..ce92eac3fc59 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt @@ -40,10 +40,9 @@ abstract class BaseTest constructor( protected val flicker: LegacyFlickerTest, protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(), - protected val tapl: LauncherInstrumentation = LauncherInstrumentation() ) { - init { - tapl.setExpectedRotationCheckEnabled(true) + protected val tapl: LauncherInstrumentation by lazy { + LauncherInstrumentation().also { it.expectedRotationCheckEnabled = true } } private val logTag = this::class.java.simpleName |