diff options
455 files changed, 10549 insertions, 6243 deletions
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index b42f7bc0ca94..e8571757c6f7 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -1,6 +1,7 @@ [Builtin Hooks] clang_format = true bpfmt = true +ktfmt = true [Builtin Hooks Options] # Only turn on clang-format check for the following subfolders. @@ -17,6 +18,7 @@ clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp tests/ tools/ bpfmt = -d +ktfmt = --kotlinlang-style --include-dirs=services/permission,packages/SystemUI [Hook Scripts] checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} @@ -25,9 +27,10 @@ hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/c hidden_api_txt_exclude_hook = ${REPO_ROOT}/frameworks/base/tools/hiddenapi/exclude.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT} -ktfmt_hook = ${REPO_ROOT}/external/ktfmt/ktfmt.py --check -i ${REPO_ROOT}/frameworks/base/ktfmt_includes.txt ${PREUPLOAD_FILES} - ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES} # This flag check hook runs only for "packages/SystemUI" subdirectory. If you want to include this check for other subdirectories, please modify flag_check.py. flag_hook = ${REPO_ROOT}/frameworks/base/packages/SystemUI/flag_check.py --msg=${PREUPLOAD_COMMIT_MESSAGE} --files=${PREUPLOAD_FILES} --project=${REPO_PATH} + +[Tool Paths] +ktfmt = ${REPO_ROOT}/prebuilts/build-tools/common/framework/ktfmt.jar diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 11fa7b75182f..c2aeadaea65c 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -619,6 +619,7 @@ public class DeviceIdleController extends SystemService * List of end times for app-IDs that are temporarily marked as being allowed to access * the network and acquire wakelocks. Times are in milliseconds. */ + @GuardedBy("this") private final SparseArray<Pair<MutableLong, String>> mTempWhitelistAppIdEndTimes = new SparseArray<>(); @@ -5010,7 +5011,9 @@ public class DeviceIdleController extends SystemService if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) { return -1; } - dumpTempWhitelistSchedule(pw, false); + synchronized (this) { + dumpTempWhitelistScheduleLocked(pw, false); + } } } else if ("except-idle-whitelist".equals(cmd)) { getContext().enforceCallingOrSelfPermission( @@ -5294,7 +5297,7 @@ public class DeviceIdleController extends SystemService pw.println(); } } - dumpTempWhitelistSchedule(pw, true); + dumpTempWhitelistScheduleLocked(pw, true); size = mTempWhitelistAppIdArray != null ? mTempWhitelistAppIdArray.length : 0; if (size > 0) { @@ -5422,7 +5425,8 @@ public class DeviceIdleController extends SystemService } } - void dumpTempWhitelistSchedule(PrintWriter pw, boolean printTitle) { + @GuardedBy("this") + void dumpTempWhitelistScheduleLocked(PrintWriter pw, boolean printTitle) { final int size = mTempWhitelistAppIdEndTimes.size(); if (size > 0) { String prefix = ""; diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java index adee322f60cf..f722e41c6195 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java @@ -48,6 +48,7 @@ public final class IdleController extends RestrictingController implements Idlen private static final String TAG = "JobScheduler.IdleController"; // Policy: we decide that we're "idle" if the device has been unused / // screen off or dreaming or wireless charging dock idle for at least this long + @GuardedBy("mLock") final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>(); IdlenessTracker mIdleTracker; private final FlexibilityController mFlexibilityController; @@ -118,8 +119,10 @@ public final class IdleController extends RestrictingController implements Idlen for (int i = mTrackedTasks.size()-1; i >= 0; i--) { mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(nowElapsed, isIdle); } + if (!mTrackedTasks.isEmpty()) { + mStateChangedListener.onControllerStateChanged(mTrackedTasks); + } } - mStateChangedListener.onControllerStateChanged(mTrackedTasks); } /** diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 96315ebccc49..50d97cf0626f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -14435,6 +14435,7 @@ package android.telephony { method @NonNull public android.telephony.CarrierRestrictionRules build(); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setAllCarriersAllowed(); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setAllowedCarriers(@NonNull java.util.List<android.service.carrier.CarrierIdentifier>); + method @FlaggedApi("com.android.internal.telephony.flags.set_carrier_restriction_status") @NonNull public android.telephony.CarrierRestrictionRules.Builder setCarrierRestrictionStatus(int); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setDefaultCarrierRestriction(int); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setExcludedCarriers(@NonNull java.util.List<android.service.carrier.CarrierIdentifier>); method @NonNull public android.telephony.CarrierRestrictionRules.Builder setMultiSimPolicy(int); diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index fd9600c1f87f..65628d32e583 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -16,6 +16,7 @@ package android.accessibilityservice; +import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_CONTROL_MAGNIFICATION; import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; @@ -69,6 +70,8 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityWindowInfo; import android.view.inputmethod.EditorInfo; +import androidx.annotation.GuardedBy; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.CancellationGroup; import com.android.internal.inputmethod.IAccessibilityInputMethodSession; @@ -640,6 +643,8 @@ public abstract class AccessibilityService extends Service { /** The detected gesture information for different displays */ boolean onGesture(AccessibilityGestureEvent gestureInfo); boolean onKeyEvent(KeyEvent event); + /** Magnification SystemUI connection changed callbacks */ + void onMagnificationSystemUIConnectionChanged(boolean connected); /** Magnification changed callbacks for different displays */ void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config); @@ -790,7 +795,6 @@ public abstract class AccessibilityService extends Service { public static final String KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP = "screenshot_timestamp"; - /** * Annotations for result codes of attaching accessibility overlays. * @@ -837,6 +841,13 @@ public abstract class AccessibilityService extends Service { private WindowManager mWindowManager; + @GuardedBy("mLock") + private boolean mServiceConnected; + @GuardedBy("mLock") + private boolean mMagnificationSystemUIConnected; + @GuardedBy("mLock") + private boolean mServiceConnectedNotified; + /** List of magnification controllers, mapping from displayId -> MagnificationController. */ private final SparseArray<MagnificationController> mMagnificationControllers = new SparseArray<>(0); @@ -886,11 +897,14 @@ public abstract class AccessibilityService extends Service { for (int i = 0; i < mMagnificationControllers.size(); i++) { mMagnificationControllers.valueAt(i).onServiceConnectedLocked(); } + checkIsMagnificationSystemUIConnectedAlready(); final AccessibilityServiceInfo info = getServiceInfo(); if (info != null) { updateInputMethod(info); mMotionEventSources = info.getMotionEventSources(); } + mServiceConnected = true; + mServiceConnectedNotified = false; } if (mSoftKeyboardController != null) { mSoftKeyboardController.onServiceConnected(); @@ -898,7 +912,57 @@ public abstract class AccessibilityService extends Service { // The client gets to handle service connection last, after we've set // up any state upon which their code may rely. - onServiceConnected(); + if (android.view.accessibility.Flags + .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) { + notifyOnServiceConnectedIfReady(); + } else { + onServiceConnected(); + } + } + + private void notifyOnServiceConnectedIfReady() { + synchronized (mLock) { + if (mServiceConnectedNotified) { + return; + } + boolean canControlMagnification; + final AccessibilityServiceInfo info = getServiceInfo(); + if (info != null) { + int flagMask = CAPABILITY_CAN_CONTROL_MAGNIFICATION; + canControlMagnification = (info.getCapabilities() & flagMask) == flagMask; + } else { + canControlMagnification = false; + } + boolean ready = canControlMagnification + ? (mServiceConnected && mMagnificationSystemUIConnected) + : mServiceConnected; + if (ready) { + getMainExecutor().execute(() -> onServiceConnected()); + mServiceConnectedNotified = true; + } + } + } + + @GuardedBy("mLock") + private void checkIsMagnificationSystemUIConnectedAlready() { + if (!android.view.accessibility.Flags + .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) { + return; + } + if (mMagnificationSystemUIConnected) { + return; + } + final IAccessibilityServiceConnection connection = + AccessibilityInteractionClient.getInstance(this).getConnection(mConnectionId); + if (connection != null) { + try { + boolean connected = connection.isMagnificationSystemUIConnected(); + mMagnificationSystemUIConnected = connected; + } catch (RemoteException re) { + Log.w(LOG_TAG, "Failed to check magnification system ui connection", re); + re.rethrowFromSystemServer(); + } + } } private void updateInputMethod(AccessibilityServiceInfo info) { @@ -1360,6 +1424,22 @@ public abstract class AccessibilityService extends Service { } } + private void onMagnificationSystemUIConnectionChanged(boolean connected) { + if (!android.view.accessibility.Flags + .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) { + return; + } + + synchronized (mLock) { + boolean changed = (mMagnificationSystemUIConnected != connected); + mMagnificationSystemUIConnected = connected; + + if (changed) { + notifyOnServiceConnectedIfReady(); + } + } + } + private void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { MagnificationController controller; @@ -2823,6 +2903,11 @@ public abstract class AccessibilityService extends Service { } @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) { + AccessibilityService.this.onMagnificationSystemUIConnectionChanged(connected); + } + + @Override public void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { AccessibilityService.this.onMagnificationChanged(displayId, region, config); @@ -3032,6 +3117,16 @@ public abstract class AccessibilityService extends Service { }); } + @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) { + mExecutor.execute(() -> { + if (mConnectionId != AccessibilityInteractionClient.NO_ID) { + mCallback.onMagnificationSystemUIConnectionChanged(connected); + } + return; + }); + } + /** Magnification changed callbacks for different displays */ public void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl index 3bc61e560d8c..f1479ef79dd9 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl @@ -48,6 +48,8 @@ import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; void onKeyEvent(in KeyEvent event, int sequence); + void onMagnificationSystemUIConnectionChanged(boolean connected); + void onMagnificationChanged(int displayId, in Region region, in MagnificationConfig config); void onMotionEvent(in MotionEvent event); diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 713d8e5dd12f..149e7194a43b 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -130,6 +130,9 @@ interface IAccessibilityServiceConnection { void setMagnificationCallbackEnabled(int displayId, boolean enabled); @RequiresNoPermission + boolean isMagnificationSystemUIConnected(); + + @RequiresNoPermission boolean setSoftKeyboardShowMode(int showMode); @RequiresNoPermission diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 2887d228e1b6..fa8fe3bf5458 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -1755,6 +1755,12 @@ public class ActivityManager { private int mNavigationBarColor; @Appearance private int mSystemBarsAppearance; + /** + * Similar to {@link TaskDescription#mSystemBarsAppearance}, but is taken from the topmost + * fully opaque (i.e. non transparent) activity in the task. + */ + @Appearance + private int mTopOpaqueSystemBarsAppearance; private boolean mEnsureStatusBarContrastWhenTransparent; private boolean mEnsureNavigationBarContrastWhenTransparent; private int mResizeMode; @@ -1855,7 +1861,7 @@ public class ActivityManager { final Icon icon = mIconRes == Resources.ID_NULL ? null : Icon.createWithResource(ActivityThread.currentPackageName(), mIconRes); return new TaskDescription(mLabel, icon, mPrimaryColor, mBackgroundColor, - mStatusBarColor, mNavigationBarColor, 0, false, false, + mStatusBarColor, mNavigationBarColor, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); } } @@ -1874,7 +1880,7 @@ public class ActivityManager { @Deprecated public TaskDescription(String label, @DrawableRes int iconRes, int colorPrimary) { this(label, Icon.createWithResource(ActivityThread.currentPackageName(), iconRes), - colorPrimary, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + colorPrimary, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) { throw new RuntimeException("A TaskDescription's primary color should be opaque"); } @@ -1892,7 +1898,7 @@ public class ActivityManager { @Deprecated public TaskDescription(String label, @DrawableRes int iconRes) { this(label, Icon.createWithResource(ActivityThread.currentPackageName(), iconRes), - 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + 0, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); } /** @@ -1904,7 +1910,7 @@ public class ActivityManager { */ @Deprecated public TaskDescription(String label) { - this(label, null, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + this(label, null, 0, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); } /** @@ -1914,7 +1920,7 @@ public class ActivityManager { */ @Deprecated public TaskDescription() { - this(null, null, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + this(null, null, 0, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); } /** @@ -1930,7 +1936,7 @@ public class ActivityManager { @Deprecated public TaskDescription(String label, Bitmap icon, int colorPrimary) { this(label, icon != null ? Icon.createWithBitmap(icon) : null, colorPrimary, 0, 0, 0, - 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); + 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) { throw new RuntimeException("A TaskDescription's primary color should be opaque"); } @@ -1946,7 +1952,7 @@ public class ActivityManager { */ @Deprecated public TaskDescription(String label, Bitmap icon) { - this(label, icon != null ? Icon.createWithBitmap(icon) : null, 0, 0, 0, 0, 0, false, + this(label, icon != null ? Icon.createWithBitmap(icon) : null, 0, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0); } @@ -1955,6 +1961,7 @@ public class ActivityManager { int colorPrimary, int colorBackground, int statusBarColor, int navigationBarColor, @Appearance int systemBarsAppearance, + @Appearance int topOpaqueSystemBarsAppearance, boolean ensureStatusBarContrastWhenTransparent, boolean ensureNavigationBarContrastWhenTransparent, int resizeMode, int minWidth, int minHeight, int colorBackgroundFloating) { @@ -1965,6 +1972,7 @@ public class ActivityManager { mStatusBarColor = statusBarColor; mNavigationBarColor = navigationBarColor; mSystemBarsAppearance = systemBarsAppearance; + mTopOpaqueSystemBarsAppearance = topOpaqueSystemBarsAppearance; mEnsureStatusBarContrastWhenTransparent = ensureStatusBarContrastWhenTransparent; mEnsureNavigationBarContrastWhenTransparent = ensureNavigationBarContrastWhenTransparent; @@ -1994,6 +2002,7 @@ public class ActivityManager { mStatusBarColor = other.mStatusBarColor; mNavigationBarColor = other.mNavigationBarColor; mSystemBarsAppearance = other.mSystemBarsAppearance; + mTopOpaqueSystemBarsAppearance = other.mTopOpaqueSystemBarsAppearance; mEnsureStatusBarContrastWhenTransparent = other.mEnsureStatusBarContrastWhenTransparent; mEnsureNavigationBarContrastWhenTransparent = other.mEnsureNavigationBarContrastWhenTransparent; @@ -2026,6 +2035,9 @@ public class ActivityManager { if (other.mSystemBarsAppearance != 0) { mSystemBarsAppearance = other.mSystemBarsAppearance; } + if (other.mTopOpaqueSystemBarsAppearance != 0) { + mTopOpaqueSystemBarsAppearance = other.mTopOpaqueSystemBarsAppearance; + } mEnsureStatusBarContrastWhenTransparent = other.mEnsureStatusBarContrastWhenTransparent; mEnsureNavigationBarContrastWhenTransparent = @@ -2305,6 +2317,14 @@ public class ActivityManager { /** * @hide */ + @Appearance + public int getTopOpaqueSystemBarsAppearance() { + return mTopOpaqueSystemBarsAppearance; + } + + /** + * @hide + */ public void setEnsureStatusBarContrastWhenTransparent( boolean ensureStatusBarContrastWhenTransparent) { mEnsureStatusBarContrastWhenTransparent = ensureStatusBarContrastWhenTransparent; @@ -2320,6 +2340,13 @@ public class ActivityManager { /** * @hide */ + public void setTopOpaqueSystemBarsAppearance(int topOpaqueSystemBarsAppearance) { + mTopOpaqueSystemBarsAppearance = topOpaqueSystemBarsAppearance; + } + + /** + * @hide + */ public boolean getEnsureNavigationBarContrastWhenTransparent() { return mEnsureNavigationBarContrastWhenTransparent; } @@ -2442,6 +2469,7 @@ public class ActivityManager { dest.writeInt(mStatusBarColor); dest.writeInt(mNavigationBarColor); dest.writeInt(mSystemBarsAppearance); + dest.writeInt(mTopOpaqueSystemBarsAppearance); dest.writeBoolean(mEnsureStatusBarContrastWhenTransparent); dest.writeBoolean(mEnsureNavigationBarContrastWhenTransparent); dest.writeInt(mResizeMode); @@ -2466,6 +2494,7 @@ public class ActivityManager { mStatusBarColor = source.readInt(); mNavigationBarColor = source.readInt(); mSystemBarsAppearance = source.readInt(); + mTopOpaqueSystemBarsAppearance = source.readInt(); mEnsureStatusBarContrastWhenTransparent = source.readBoolean(); mEnsureNavigationBarContrastWhenTransparent = source.readBoolean(); mResizeMode = source.readInt(); @@ -2498,7 +2527,8 @@ public class ActivityManager { + " resizeMode: " + ActivityInfo.resizeModeToString(mResizeMode) + " minWidth: " + mMinWidth + " minHeight: " + mMinHeight + " colorBackgrounFloating: " + mColorBackgroundFloating - + " systemBarsAppearance: " + mSystemBarsAppearance; + + " systemBarsAppearance: " + mSystemBarsAppearance + + " topOpaqueSystemBarsAppearance: " + mTopOpaqueSystemBarsAppearance; } @Override @@ -2519,6 +2549,7 @@ public class ActivityManager { result = result * 31 + mStatusBarColor; result = result * 31 + mNavigationBarColor; result = result * 31 + mSystemBarsAppearance; + result = result * 31 + mTopOpaqueSystemBarsAppearance; result = result * 31 + (mEnsureStatusBarContrastWhenTransparent ? 1 : 0); result = result * 31 + (mEnsureNavigationBarContrastWhenTransparent ? 1 : 0); result = result * 31 + mResizeMode; @@ -2542,6 +2573,7 @@ public class ActivityManager { && mStatusBarColor == other.mStatusBarColor && mNavigationBarColor == other.mNavigationBarColor && mSystemBarsAppearance == other.mSystemBarsAppearance + && mTopOpaqueSystemBarsAppearance == other.mTopOpaqueSystemBarsAppearance && mEnsureStatusBarContrastWhenTransparent == other.mEnsureStatusBarContrastWhenTransparent && mEnsureNavigationBarContrastWhenTransparent diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index c0f723241c82..5956e2bde242 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -2617,6 +2617,9 @@ public class ApplicationPackageManager extends PackageManager { try { Objects.requireNonNull(packageName); return mPM.isAppArchivable(packageName, new UserHandle(getUserId())); + } catch (ParcelableException e) { + e.maybeRethrow(NameNotFoundException.class); + throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 4c839f1762cb..fc3bb0288d67 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -114,7 +114,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ContrastColorUtil; -import com.android.internal.util.NewlineNormalizer; +import com.android.internal.util.NotificationBigTextNormalizer; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -1632,6 +1632,10 @@ public class Notification implements Parcelable private Icon mSmallIcon; @UnsupportedAppUsage private Icon mLargeIcon; + private Icon mAppIcon; + + /** Cache for whether the notification was posted by a headless system app. */ + private Boolean mBelongsToHeadlessSystemApp = null; @UnsupportedAppUsage private String mChannelId; @@ -3079,25 +3083,17 @@ public class Notification implements Parcelable return name.toString(); } } - // If not, try getting the app info from extras. + // If not, try getting the name from the app info. if (context == null) { return null; } - final PackageManager pm = context.getPackageManager(); if (TextUtils.isEmpty(name)) { - if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) { - final ApplicationInfo info = extras.getParcelable( - EXTRA_BUILDER_APPLICATION_INFO, - ApplicationInfo.class); - if (info != null) { - name = pm.getApplicationLabel(info); - } + ApplicationInfo info = getApplicationInfo(context); + if (info != null) { + final PackageManager pm = context.getPackageManager(); + name = pm.getApplicationLabel(getApplicationInfo(context)); } } - // If that's still empty, use the one from the context directly. - if (TextUtils.isEmpty(name)) { - name = pm.getApplicationLabel(context.getApplicationInfo()); - } // If there's still nothing, ¯\_(ツ)_/¯ if (TextUtils.isEmpty(name)) { return null; @@ -3109,9 +3105,89 @@ public class Notification implements Parcelable } /** + * Whether this notification was posted by a headless system app. + * + * If we don't have enough information to figure this out, this will return false. Therefore, + * false negatives are possible, but false positives should not be. + * * @hide */ - public int loadHeaderAppIconRes(Context context) { + public boolean belongsToHeadlessSystemApp(Context context) { + Trace.beginSection("Notification#belongsToHeadlessSystemApp"); + + try { + if (mBelongsToHeadlessSystemApp != null) { + return mBelongsToHeadlessSystemApp; + } + + if (context == null) { + // Without a valid context, we don't know exactly. Let's assume it doesn't belong to + // a system app, but not cache the value. + return false; + } + + ApplicationInfo info = getApplicationInfo(context); + if (info != null) { + if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + // It's not a system app at all. + mBelongsToHeadlessSystemApp = false; + } else { + // If there's no launch intent, it's probably a headless app. + final PackageManager pm = context.getPackageManager(); + mBelongsToHeadlessSystemApp = pm.getLaunchIntentForPackage(info.packageName) + == null; + } + } else { + // If for some reason we don't have the app info, we don't know; best assume it's + // not a system app. + return false; + } + return mBelongsToHeadlessSystemApp; + } finally { + Trace.endSection(); + } + } + + /** + * Get the resource ID of the app icon from application info. + * @hide + */ + public int getHeaderAppIconRes(Context context) { + ApplicationInfo info = getApplicationInfo(context); + if (info != null) { + return info.icon; + } + return 0; + } + + /** + * Load the app icon drawable from the package manager. This could result in a binder call. + * @hide + */ + public Drawable loadHeaderAppIcon(Context context) { + Trace.beginSection("Notification#loadHeaderAppIcon"); + + try { + if (context == null) { + Log.e(TAG, "Cannot load the app icon drawable with a null context"); + return null; + } + final PackageManager pm = context.getPackageManager(); + ApplicationInfo info = getApplicationInfo(context); + if (info == null) { + Log.e(TAG, "Cannot load the app icon drawable: no application info"); + return null; + } + return pm.getApplicationIcon(info); + } finally { + Trace.endSection(); + } + } + + /** + * Fetch the application info from the notification, or the context if that isn't available. + */ + private ApplicationInfo getApplicationInfo(Context context) { ApplicationInfo info = null; if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) { info = extras.getParcelable( @@ -3119,12 +3195,12 @@ public class Notification implements Parcelable ApplicationInfo.class); } if (info == null) { + if (context == null) { + return null; + } info = context.getApplicationInfo(); } - if (info != null) { - return info.icon; - } - return 0; + return info; } /** @@ -3186,12 +3262,12 @@ public class Notification implements Parcelable return cs.toString(); } - private static CharSequence cleanUpNewLines(@Nullable CharSequence charSequence) { + private static CharSequence normalizeBigText(@Nullable CharSequence charSequence) { if (charSequence == null) { return charSequence; } - return NewlineNormalizer.normalizeNewlines(charSequence.toString()); + return NotificationBigTextNormalizer.normalizeBigText(charSequence.toString()); } private static CharSequence removeTextSizeSpans(CharSequence charSequence) { @@ -4124,6 +4200,55 @@ public class Notification implements Parcelable } /** + * The colored app icon that can replace the small icon in the notification starting in V. + * + * Before using this value, you should first check whether it's actually being used by the + * notification by calling {@link Notification#shouldUseAppIcon()}. + * + * @hide + */ + public Icon getAppIcon() { + if (mAppIcon != null) { + return mAppIcon; + } + // If the app icon hasn't been loaded yet, check if we can load it without a context. + if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) { + final ApplicationInfo info = extras.getParcelable( + EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo.class); + if (info != null) { + int appIconRes = info.icon; + if (appIconRes == 0) { + Log.w(TAG, "Failed to get the app icon: no icon in application info"); + return null; + } + mAppIcon = Icon.createWithResource(info.packageName, appIconRes); + return mAppIcon; + } else { + Log.e(TAG, "Failed to get the app icon: " + + "there's an EXTRA_BUILDER_APPLICATION_INFO in extras but it's null"); + } + } else { + Log.w(TAG, "Failed to get the app icon: no application info in extras"); + } + return null; + } + + /** + * Whether the notification is using the app icon instead of the small icon. + * @hide + */ + public boolean shouldUseAppIcon() { + if (Flags.notificationsUseAppIconInRow()) { + if (belongsToHeadlessSystemApp(/* context = */ null)) { + return false; + } + return getAppIcon() != null; + } + return false; + } + + /** * The large icon shown in this notification's content view. * @see Builder#getLargeIcon() * @see Builder#setLargeIcon(Icon) @@ -6116,16 +6241,30 @@ public class Notification implements Parcelable if (Flags.notificationsUseAppIcon()) { // Override small icon with app icon mN.mSmallIcon = Icon.createWithResource(mContext, - mN.loadHeaderAppIconRes(mContext)); + mN.getHeaderAppIconRes(mContext)); } else if (mN.mSmallIcon == null && mN.icon != 0) { mN.mSmallIcon = Icon.createWithResource(mContext, mN.icon); } - contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon); + boolean usingAppIcon = false; + if (Flags.notificationsUseAppIconInRow() && !mN.belongsToHeadlessSystemApp(mContext)) { + // Use the app icon in the view + int appIconRes = mN.getHeaderAppIconRes(mContext); + if (appIconRes != 0) { + mN.mAppIcon = Icon.createWithResource(mContext, appIconRes); + contentView.setImageViewIcon(R.id.icon, mN.mAppIcon); + usingAppIcon = true; + } else { + Log.w(TAG, "bindSmallIcon: could not get the app icon"); + } + } + if (!usingAppIcon) { + contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon); + } contentView.setInt(R.id.icon, "setImageLevel", mN.iconLevel); // Don't change color if we're using the app icon. - if (!Flags.notificationsUseAppIcon()) { + if (!Flags.notificationsUseAppIcon() && !usingAppIcon) { processSmallIconColor(mN.mSmallIcon, contentView, p); } } @@ -8427,7 +8566,7 @@ public class Notification implements Parcelable // Replace the text with the big text, but only if the big text is not empty. CharSequence bigTextText = mBuilder.processLegacyText(mBigText); if (Flags.cleanUpSpansAndNewLines()) { - bigTextText = cleanUpNewLines(stripStyling(bigTextText)); + bigTextText = normalizeBigText(stripStyling(bigTextText)); } if (!TextUtils.isEmpty(bigTextText)) { p.text(bigTextText); diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 348d4d8fd809..273a79efb591 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -1969,6 +1969,11 @@ public final class UiAutomation { } @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) { + /* do nothing */ + } + + @Override public void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { /* do nothing */ diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 55c3bb60e9c7..6edae0b60fd9 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -52,14 +52,32 @@ flag { bug: "281044385" } +# vvv Prototypes for using app icons in notifications vvv + flag { name: "notifications_use_app_icon" namespace: "systemui" - description: "Experiment to replace the small icon in a notification with the app icon." + description: "Experiment to replace the small icon in a notification with the app icon. This includes the status bar, AOD, shelf and notification row itself." + bug: "335211019" +} + +flag { + name: "notifications_use_app_icon_in_row" + namespace: "systemui" + description: "Experiment to replace the small icon in a notification row with the app icon." bug: "335211019" } flag { + name: "notifications_use_monochrome_app_icon" + namespace: "systemui" + description: "Experiment to replace the notification icon in the status bar and shelf with the monochrome app icon, if available." + bug: "335211019" +} + +# ^^^ Prototypes for using app icons in notifications ^^^ + +flag { name: "notification_expansion_optional" namespace: "systemui" description: "Experiment to restore the pre-S behavior where standard notifications are not expandable unless they have actions." diff --git a/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java b/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java index 1c8e497edd0a..eb31db18473f 100644 --- a/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java +++ b/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java @@ -27,6 +27,8 @@ import android.view.IWindow; import android.view.InsetsSourceControl; import android.view.InsetsState; +import com.android.internal.annotations.VisibleForTesting; + import java.util.Objects; /** @@ -38,7 +40,9 @@ public class WindowStateInsetsControlChangeItem extends WindowStateTransactionIt private static final String TAG = "WindowStateInsetsControlChangeItem"; private InsetsState mInsetsState; - private InsetsSourceControl.Array mActiveControls; + + @VisibleForTesting + public InsetsSourceControl.Array mActiveControls; @Override public void execute(@NonNull ClientTransactionHandler client, @NonNull IWindow window, @@ -51,6 +55,8 @@ public class WindowStateInsetsControlChangeItem extends WindowStateTransactionIt // An exception could happen if the process is restarted. It is safe to ignore since // the window should no longer exist. Log.w(TAG, "The original window no longer exists in the new process", e); + // Prevent leak + mActiveControls.release(); } Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } @@ -69,7 +75,12 @@ public class WindowStateInsetsControlChangeItem extends WindowStateTransactionIt } instance.setWindow(window); instance.mInsetsState = new InsetsState(insetsState, true /* copySources */); - instance.mActiveControls = new InsetsSourceControl.Array(activeControls); + instance.mActiveControls = new InsetsSourceControl.Array( + activeControls, true /* copyControls */); + // This source control is an extra copy if the client is not local. By setting + // PARCELABLE_WRITE_RETURN_VALUE, the leash will be released at the end of + // SurfaceControl.writeToParcel. + instance.mActiveControls.setParcelableFlags(PARCELABLE_WRITE_RETURN_VALUE); return instance; } diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig index 374be6fd2272..18cfca686107 100644 --- a/core/java/android/appwidget/flags.aconfig +++ b/core/java/android/appwidget/flags.aconfig @@ -40,3 +40,13 @@ flag { description: "Throttle the widget view updates to mitigate transaction exceptions" bug: "326145514" } + +flag { + name: "support_resume_restore_after_reboot" + namespace: "app_widgets" + description: "Enable support for resume restore widget after reboot by persisting intermediate states to disk" + bug: "336976070" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index ed5d66227574..1e7815329f3b 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -64,3 +64,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "virtual_devices" + name: "intent_interception_action_matching_fix" + description: "Do not match intents without actions if the filter has actions" + bug: "343805037" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 270fc32a4e32..bbd0e9ff4738 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2431,6 +2431,7 @@ public class PackageInstaller { statusReceiver, new UserHandle(mUserId)); } catch (ParcelableException e) { e.maybeRethrow(PackageManager.NameNotFoundException.class); + throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2467,6 +2468,7 @@ public class PackageInstaller { } catch (ParcelableException e) { e.maybeRethrow(IOException.class); e.maybeRethrow(PackageManager.NameNotFoundException.class); + throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2499,6 +2501,7 @@ public class PackageInstaller { userActionIntent, new UserHandle(mUserId)); } catch (ParcelableException e) { e.maybeRethrow(PackageManager.NameNotFoundException.class); + throw new RuntimeException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index e3780edcd7da..f54be00c9e69 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -733,7 +733,7 @@ public final class SQLiteDatabase extends SQLiteClosable { * commits, or is rolled back, either explicitly or by a call to * {@link #yieldIfContendedSafely}. */ - // TODO(340874899) Provide an Executor overload + @SuppressLint("ExecutorRegistration") public void beginTransactionWithListener( @Nullable SQLiteTransactionListener transactionListener) { beginTransaction(transactionListener, true); @@ -763,7 +763,7 @@ public final class SQLiteDatabase extends SQLiteClosable { * transaction begins, commits, or is rolled back, either * explicitly or by a call to {@link #yieldIfContendedSafely}. */ - // TODO(340874899) Provide an Executor overload + @SuppressLint("ExecutorRegistration") public void beginTransactionWithListenerNonExclusive( @Nullable SQLiteTransactionListener transactionListener) { beginTransaction(transactionListener, false); @@ -789,7 +789,6 @@ public final class SQLiteDatabase extends SQLiteClosable { * } * </pre> */ - // TODO(340874899) Provide an Executor overload @SuppressLint("ExecutorRegistration") @FlaggedApi(Flags.FLAG_SQLITE_APIS_35) public void beginTransactionWithListenerReadOnly( diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index 60ad8e81fcf4..2d3d25217357 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -523,23 +523,25 @@ public class SystemSensorManager extends SensorManager { Handler mainHandler = new Handler(mContext.getMainLooper()); - for (Map.Entry<DynamicSensorCallback, Handler> entry : - mDynamicSensorCallbacks.entrySet()) { - final DynamicSensorCallback callback = entry.getKey(); - Handler handler = - entry.getValue() == null ? mainHandler : entry.getValue(); - - handler.post(new Runnable() { - @Override - public void run() { - for (Sensor s: addedList) { - callback.onDynamicSensorConnected(s); + synchronized (mDynamicSensorCallbacks) { + for (Map.Entry<DynamicSensorCallback, Handler> entry : + mDynamicSensorCallbacks.entrySet()) { + final DynamicSensorCallback callback = entry.getKey(); + Handler handler = + entry.getValue() == null ? mainHandler : entry.getValue(); + + handler.post(new Runnable() { + @Override + public void run() { + for (Sensor s: addedList) { + callback.onDynamicSensorConnected(s); + } + for (Sensor s: removedList) { + callback.onDynamicSensorDisconnected(s); + } } - for (Sensor s: removedList) { - callback.onDynamicSensorDisconnected(s); - } - } - }); + }); + } } for (Sensor s: removedList) { @@ -658,13 +660,15 @@ public class SystemSensorManager extends SensorManager { if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } - if (mDynamicSensorCallbacks.containsKey(callback)) { - // has been already registered, ignore - return; - } + synchronized (mDynamicSensorCallbacks) { + if (mDynamicSensorCallbacks.containsKey(callback)) { + // has been already registered, ignore + return; + } - setupDynamicSensorBroadcastReceiver(); - mDynamicSensorCallbacks.put(callback, handler); + setupDynamicSensorBroadcastReceiver(); + mDynamicSensorCallbacks.put(callback, handler); + } } /** @hide */ @@ -673,7 +677,9 @@ public class SystemSensorManager extends SensorManager { if (DEBUG_DYNAMIC_SENSOR) { Log.i(TAG, "Removing dynamic sensor listener"); } - mDynamicSensorCallbacks.remove(callback); + synchronized (mDynamicSensorCallbacks) { + mDynamicSensorCallbacks.remove(callback); + } } /* diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig index 4284ad09e251..047d1fa4f49a 100644 --- a/core/java/android/hardware/biometrics/flags.aconfig +++ b/core/java/android/hardware/biometrics/flags.aconfig @@ -32,3 +32,10 @@ flag { description: "Feature flag for adding a custom content view API to BiometricPrompt.Builder." bug: "302735104" } + +flag { + name: "mandatory_biometrics" + namespace: "biometrics_framework" + description: "This flag controls whether LSKF fallback is removed from biometric prompt when the phone is outside trusted locations" + bug: "322081563" +} diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 342479bc159e..3cc87ea9d359 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -1492,10 +1492,12 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri /** * <p>Default flash brightness level for manual flash control in SINGLE mode.</p> * <p>If flash unit is available this will be greater than or equal to 1 and less - * or equal to <code>android.flash.info.singleStrengthMaxLevel</code>. + * or equal to {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}. * Note for devices that do not support the manual flash strength control * feature, this level will always be equal to 1.</p> * <p>This key is available on all devices.</p> + * + * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL */ @PublicKey @NonNull @@ -1511,13 +1513,15 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * otherwise the value will be equal to 1.</p> * <p>Note that this level is just a number of supported levels(the granularity of control). * There is no actual physical power units tied to this level. - * There is no relation between android.flash.info.torchStrengthMaxLevel and - * android.flash.info.singleStrengthMaxLevel i.e. the ratio of - * android.flash.info.torchStrengthMaxLevel:android.flash.info.singleStrengthMaxLevel + * There is no relation between {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} and + * {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} i.e. the ratio of + * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}:{@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} * is not guaranteed to be the ratio of actual brightness.</p> * <p>This key is available on all devices.</p> * * @see CaptureRequest#FLASH_MODE + * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL + * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL */ @PublicKey @NonNull @@ -1528,10 +1532,12 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri /** * <p>Default flash brightness level for manual flash control in TORCH mode</p> * <p>If flash unit is available this will be greater than or equal to 1 and less - * or equal to android.flash.info.torchStrengthMaxLevel. + * or equal to {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}. * Note for the devices that do not support the manual flash strength control feature, * this level will always be equal to 1.</p> * <p>This key is available on all devices.</p> + * + * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL */ @PublicKey @NonNull diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index c82e7e8c12ef..938636f7e8e5 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -2684,35 +2684,39 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <p>Flash strength level to be used when manual flash control is active.</p> * <p>Flash strength level to use in capture mode i.e. when the applications control * flash with either SINGLE or TORCH mode.</p> - * <p>Use android.flash.info.singleStrengthMaxLevel and - * android.flash.info.torchStrengthMaxLevel to check whether the device supports + * <p>Use {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and + * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} to check whether the device supports * flash strength control or not. * If the values of android.flash.info.singleStrengthMaxLevel and - * android.flash.info.torchStrengthMaxLevel are greater than 1, + * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1, * then the device supports manual flash strength control.</p> * <p>If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> TORCH the value must be >= 1 - * and <= android.flash.info.torchStrengthMaxLevel. + * and <= {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}. * If the application doesn't set the key and - * android.flash.info.torchStrengthMaxLevel > 1, + * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} > 1, * then the flash will be fired at the default level set by HAL in - * android.flash.info.torchStrengthDefaultLevel. + * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_DEFAULT_LEVEL android.flash.torchStrengthDefaultLevel}. * If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> SINGLE, then the value must be >= 1 - * and <= android.flash.info.singleStrengthMaxLevel. + * and <= {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}. * If the application does not set this key and - * android.flash.info.singleStrengthMaxLevel > 1, + * {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} > 1, * then the flash will be fired at the default level set by HAL - * in android.flash.info.singleStrengthDefaultLevel. + * in {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL android.flash.singleStrengthDefaultLevel}. * If {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to any of ON_AUTO_FLASH, ON_ALWAYS_FLASH, * ON_AUTO_FLASH_REDEYE, ON_EXTERNAL_FLASH values, then the strengthLevel will be ignored.</p> * <p><b>Range of valid values:</b><br> - * <code>[1-android.flash.info.torchStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is + * <code>[1-{@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is * set to TORCH; - * <code>[1-android.flash.info.singleStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is + * <code>[1-{@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is * set to SINGLE</p> * <p>This key is available on all devices.</p> * * @see CaptureRequest#CONTROL_AE_MODE * @see CaptureRequest#FLASH_MODE + * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL + * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL + * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_DEFAULT_LEVEL + * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL */ @PublicKey @NonNull diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index 1460515c2438..4406a419c317 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -2977,35 +2977,39 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <p>Flash strength level to be used when manual flash control is active.</p> * <p>Flash strength level to use in capture mode i.e. when the applications control * flash with either SINGLE or TORCH mode.</p> - * <p>Use android.flash.info.singleStrengthMaxLevel and - * android.flash.info.torchStrengthMaxLevel to check whether the device supports + * <p>Use {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and + * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} to check whether the device supports * flash strength control or not. * If the values of android.flash.info.singleStrengthMaxLevel and - * android.flash.info.torchStrengthMaxLevel are greater than 1, + * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1, * then the device supports manual flash strength control.</p> * <p>If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> TORCH the value must be >= 1 - * and <= android.flash.info.torchStrengthMaxLevel. + * and <= {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}. * If the application doesn't set the key and - * android.flash.info.torchStrengthMaxLevel > 1, + * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} > 1, * then the flash will be fired at the default level set by HAL in - * android.flash.info.torchStrengthDefaultLevel. + * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_DEFAULT_LEVEL android.flash.torchStrengthDefaultLevel}. * If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> SINGLE, then the value must be >= 1 - * and <= android.flash.info.singleStrengthMaxLevel. + * and <= {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}. * If the application does not set this key and - * android.flash.info.singleStrengthMaxLevel > 1, + * {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} > 1, * then the flash will be fired at the default level set by HAL - * in android.flash.info.singleStrengthDefaultLevel. + * in {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL android.flash.singleStrengthDefaultLevel}. * If {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to any of ON_AUTO_FLASH, ON_ALWAYS_FLASH, * ON_AUTO_FLASH_REDEYE, ON_EXTERNAL_FLASH values, then the strengthLevel will be ignored.</p> * <p><b>Range of valid values:</b><br> - * <code>[1-android.flash.info.torchStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is + * <code>[1-{@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is * set to TORCH; - * <code>[1-android.flash.info.singleStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is + * <code>[1-{@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is * set to SINGLE</p> * <p>This key is available on all devices.</p> * * @see CaptureRequest#CONTROL_AE_MODE * @see CaptureRequest#FLASH_MODE + * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL + * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL + * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_DEFAULT_LEVEL + * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL */ @PublicKey @NonNull diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index 91c2965c2505..c9f207cf26e8 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -305,15 +305,28 @@ public interface IBinder { /** * Interface for receiving a callback when the process hosting an IBinder * has gone away. - * + * * @see #linkToDeath */ public interface DeathRecipient { public void binderDied(); /** - * Interface for receiving a callback when the process hosting an IBinder + * The function called when the process hosting an IBinder * has gone away. + * + * This callback will be called from any binder thread like any other binder + * transaction. If the process receiving this notification is multithreaded + * then synchronization may be required because other threads may be executing + * at the same time. + * + * No locks are held in libbinder when {@link binderDied} is called. + * + * There is no need to call {@link unlinkToDeath} in the binderDied callback. + * The binder is already dead so {@link unlinkToDeath} is a no-op. + * It will be unlinked when the last local reference of that binder proxy is + * dropped. + * * @param who The IBinder that has become invalid */ default void binderDied(@NonNull IBinder who) { diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 71066ac7ac39..3f9c819cd62f 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1276,13 +1276,22 @@ public class DreamService extends Service implements Window.Callback { }); } + /** + * Whether or not wake requests will be redirected. + * + * @hide + */ + public boolean getRedirectWake() { + return mOverlayConnection != null && mRedirectWake; + } + private void wakeUp(boolean fromSystem) { if (mDebug) { Slog.v(mTag, "wakeUp(): fromSystem=" + fromSystem + ", mWaking=" + mWaking + ", mFinished=" + mFinished); } - if (!fromSystem && mOverlayConnection != null && mRedirectWake) { + if (!fromSystem && getRedirectWake()) { mOverlayConnection.addConsumer(overlay -> { try { overlay.onWakeRequested(); diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index c674968bba8a..0dec13ff0c02 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -18,9 +18,10 @@ package android.text; import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; -import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; +import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; +import android.annotation.ColorInt; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; @@ -398,6 +399,20 @@ public abstract class Layout { mUseBoundsForWidth = useBoundsForWidth; mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang; mMinimumFontMetrics = minimumFontMetrics; + + initSpanColors(); + } + + private void initSpanColors() { + if (mSpannedText && Flags.highContrastTextSmallTextRect()) { + if (mSpanColors == null) { + mSpanColors = new SpanColors(); + } else { + mSpanColors.recycle(); + } + } else { + mSpanColors = null; + } } /** @@ -417,6 +432,7 @@ public abstract class Layout { mSpacingMult = spacingmult; mSpacingAdd = spacingadd; mSpannedText = text instanceof Spanned; + initSpanColors(); } /** @@ -643,20 +659,20 @@ public abstract class Layout { return null; } - return isHighContrastTextDark() ? BlendMode.MULTIPLY : BlendMode.DIFFERENCE; + return isHighContrastTextDark(mPaint.getColor()) ? BlendMode.MULTIPLY + : BlendMode.DIFFERENCE; } - private boolean isHighContrastTextDark() { + private boolean isHighContrastTextDark(@ColorInt int color) { // High-contrast text mode // Determine if the text is black-on-white or white-on-black, so we know what blendmode will // give the highest contrast and most realistic text color. // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h if (highContrastTextLuminance()) { var lab = new double[3]; - ColorUtils.colorToLAB(mPaint.getColor(), lab); - return lab[0] < 0.5; + ColorUtils.colorToLAB(color, lab); + return lab[0] < 50.0; } else { - var color = mPaint.getColor(); int channelSum = Color.red(color) + Color.green(color) + Color.blue(color); return channelSum < (128 * 3); } @@ -1010,15 +1026,22 @@ public abstract class Layout { var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX, mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR); + var originalTextColor = mPaint.getColor(); var bgPaint = mWorkPlainPaint; bgPaint.reset(); - bgPaint.setColor(isHighContrastTextDark() ? Color.WHITE : Color.BLACK); + bgPaint.setColor(isHighContrastTextDark(originalTextColor) ? Color.WHITE : Color.BLACK); bgPaint.setStyle(Paint.Style.FILL); int start = getLineStart(firstLine); int end = getLineEnd(lastLine); // Draw a separate background rectangle for each line of text, that only surrounds the - // characters on that line. + // characters on that line. But we also have to check the text color for each character, and + // make sure we are drawing the correct contrasting background. This is because Spans can + // change colors throughout the text and we'll need to match our backgrounds. + if (mSpannedText && mSpanColors != null) { + mSpanColors.init(mWorkPaint, ((Spanned) mText), start, end); + } + forEachCharacterBounds( start, end, @@ -1028,13 +1051,24 @@ public abstract class Layout { int mLastLineNum = -1; final RectF mLineBackground = new RectF(); + @ColorInt int mLastColor = originalTextColor; + @Override public void onCharacterBounds(int index, int lineNum, float left, float top, float right, float bottom) { - if (lineNum != mLastLineNum) { + + var newBackground = determineContrastingBackgroundColor(index); + var hasBgColorChanged = newBackground != bgPaint.getColor(); + + if (lineNum != mLastLineNum || hasBgColorChanged) { + // Draw what we have so far, then reset the rect and update its color drawRect(); mLineBackground.set(left, top, right, bottom); mLastLineNum = lineNum; + + if (hasBgColorChanged) { + bgPaint.setColor(newBackground); + } } else { mLineBackground.union(left, top, right, bottom); } @@ -1051,8 +1085,36 @@ public abstract class Layout { canvas.drawRect(mLineBackground, bgPaint); } } + + private int determineContrastingBackgroundColor(int index) { + if (!mSpannedText || mSpanColors == null) { + // The text is not Spanned. it's all one color. + return bgPaint.getColor(); + } + + // Sometimes the color will change, but not enough to warrant a background + // color change. e.g. from black to dark grey still gets clamped to black, + // so the background stays white and we don't need to draw a fresh + // background. + var textColor = mSpanColors.getColorAt(index); + if (textColor == SpanColors.NO_COLOR_FOUND) { + textColor = originalTextColor; + } + var hasColorChanged = textColor != mLastColor; + if (hasColorChanged) { + mLastColor = textColor; + + return isHighContrastTextDark(textColor) ? Color.WHITE : Color.BLACK; + } + + return bgPaint.getColor(); + } } ); + + if (mSpanColors != null) { + mSpanColors.recycle(); + } } /** @@ -3580,6 +3642,7 @@ public abstract class Layout { private float mSpacingAdd; private static final Rect sTempRect = new Rect(); private boolean mSpannedText; + @Nullable private SpanColors mSpanColors; private TextDirectionHeuristic mTextDir; private SpanSet<LineBackgroundSpan> mLineBackgroundSpans; private boolean mIncludePad; diff --git a/core/java/android/text/SpanColors.java b/core/java/android/text/SpanColors.java new file mode 100644 index 000000000000..fcd242b62700 --- /dev/null +++ b/core/java/android/text/SpanColors.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import android.annotation.ColorInt; +import android.annotation.Nullable; +import android.graphics.Color; +import android.text.style.CharacterStyle; + +/** + * Finds the foreground text color for the given Spanned text so you can iterate through each color + * change. + * + * @hide + */ +public class SpanColors { + public static final @ColorInt int NO_COLOR_FOUND = Color.TRANSPARENT; + + private final SpanSet<CharacterStyle> mCharacterStyleSpanSet = + new SpanSet<>(CharacterStyle.class); + @Nullable private TextPaint mWorkPaint; + + public SpanColors() {} + + /** + * Init for the given text + * + * @param workPaint A temporary TextPaint object that will be used to calculate the colors. The + * paint properties will be mutated on calls to {@link #getColorAt(int)} so + * make sure to reset it before you use it for something else. + * @param spanned the text to examine + * @param start index to start at + * @param end index of the end + */ + public void init(TextPaint workPaint, Spanned spanned, int start, int end) { + mWorkPaint = workPaint; + mCharacterStyleSpanSet.init(spanned, start, end); + } + + /** + * Removes all internal references to the spans to avoid memory leaks. + */ + public void recycle() { + mWorkPaint = null; + mCharacterStyleSpanSet.recycle(); + } + + /** + * Calculates the foreground color of the text at the given character index. + * + * <p>You must call {@link #init(TextPaint, Spanned, int, int)} before calling this + */ + public @ColorInt int getColorAt(int index) { + var finalColor = NO_COLOR_FOUND; + // Reset the paint so if we get a CharacterStyle that doesn't actually specify color, + // (like UnderlineSpan), we still return no color found. + mWorkPaint.setColor(finalColor); + for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { + if ((index >= mCharacterStyleSpanSet.spanStarts[k]) + && (index <= mCharacterStyleSpanSet.spanEnds[k])) { + final CharacterStyle span = mCharacterStyleSpanSet.spans[k]; + span.updateDrawState(mWorkPaint); + + finalColor = calculateFinalColor(mWorkPaint); + } + } + return finalColor; + } + + private @ColorInt int calculateFinalColor(TextPaint workPaint) { + // TODO: can we figure out what the getColorFilter() will do? + // if so, we also need to reset colorFilter before the loop in getColorAt() + return workPaint.getColor(); + } +} diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java index 4e5cb58a00b5..487214c5c33a 100644 --- a/core/java/android/view/InsetsSourceControl.java +++ b/core/java/android/view/InsetsSourceControl.java @@ -269,22 +269,66 @@ public class InsetsSourceControl implements Parcelable { public Array() { } - public Array(@NonNull Array other) { - mControls = other.mControls; + /** + * @param copyControls whether or not to make a copy of the each {@link InsetsSourceControl} + */ + public Array(@NonNull Array other, boolean copyControls) { + setTo(other, copyControls); } - public Array(Parcel in) { + public Array(@NonNull Parcel in) { readFromParcel(in); } - public void set(@Nullable InsetsSourceControl[] controls) { - mControls = controls; + /** Updates the current Array to the given Array. */ + public void setTo(@NonNull Array other, boolean copyControls) { + set(other.mControls, copyControls); } + /** Updates the current controls to the given controls. */ + public void set(@Nullable InsetsSourceControl[] controls, boolean copyControls) { + if (controls == null || !copyControls) { + mControls = controls; + return; + } + // Make a copy of the array. + mControls = new InsetsSourceControl[controls.length]; + for (int i = mControls.length - 1; i >= 0; i--) { + if (controls[i] != null) { + mControls[i] = new InsetsSourceControl(controls[i]); + } + } + } + + /** Gets the controls. */ public @Nullable InsetsSourceControl[] get() { return mControls; } + /** Cleanup {@link SurfaceControl} stored in controls to prevent leak. */ + public void release() { + if (mControls == null) { + return; + } + for (InsetsSourceControl control : mControls) { + if (control != null) { + control.release(SurfaceControl::release); + } + } + } + + /** Sets the given flags to all controls. */ + public void setParcelableFlags(int parcelableFlags) { + if (mControls == null) { + return; + } + for (InsetsSourceControl control : mControls) { + if (control != null) { + control.setParcelableFlags(parcelableFlags); + } + } + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1cb276568244..95c9d7bb72c5 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -23705,12 +23705,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; - // // For VRR to vote the preferred frame rate - if (sToolkitSetFrameRateReadOnlyFlagValue - && sToolkitFrameRateViewEnablingReadOnlyFlagValue) { - votePreferredFrameRate(); - } - mPrivateFlags4 |= PFLAG4_HAS_DRAWN; // Fast path for layouts with no backgrounds @@ -23727,6 +23721,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, draw(canvas); } } + + // For VRR to vote the preferred frame rate + if (sToolkitSetFrameRateReadOnlyFlagValue + && sToolkitFrameRateViewEnablingReadOnlyFlagValue) { + votePreferredFrameRate(); + } } finally { renderNode.endRecording(); setDisplayListProperties(renderNode); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 139285a44817..496e8992fc41 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2276,6 +2276,29 @@ public final class ViewRootImpl implements ViewParent, requestLayout(); } + /** Handles messages {@link #MSG_INSETS_CONTROL_CHANGED}. */ + private void handleInsetsControlChanged(@NonNull InsetsState insetsState, + @NonNull InsetsSourceControl.Array activeControls) { + final InsetsSourceControl[] controls = activeControls.get(); + + if (mTranslator != null) { + mTranslator.translateInsetsStateInScreenToAppWindow(insetsState); + mTranslator.translateSourceControlsInScreenToAppWindow(controls); + } + + // Deliver state change before control change, such that: + // a) When gaining control, controller can compare with server state to evaluate + // whether it needs to run animation. + // b) When loosing control, controller can restore server state by taking last + // dispatched state as truth. + mInsetsController.onStateChanged(insetsState); + if (mAdded) { + mInsetsController.onControlsChanged(controls); + } else { + activeControls.release(); + } + } + private final DisplayListener mDisplayListener = new DisplayListener() { @Override public void onDisplayChanged(int displayId) { @@ -6591,24 +6614,11 @@ public final class ViewRootImpl implements ViewParent, break; } case MSG_INSETS_CONTROL_CHANGED: { - SomeArgs args = (SomeArgs) msg.obj; - - // Deliver state change before control change, such that: - // a) When gaining control, controller can compare with server state to evaluate - // whether it needs to run animation. - // b) When loosing control, controller can restore server state by taking last - // dispatched state as truth. - mInsetsController.onStateChanged((InsetsState) args.arg1); - InsetsSourceControl[] controls = (InsetsSourceControl[]) args.arg2; - if (mAdded) { - mInsetsController.onControlsChanged(controls); - } else if (controls != null) { - for (InsetsSourceControl control : controls) { - if (control != null) { - control.release(SurfaceControl::release); - } - } - } + final SomeArgs args = (SomeArgs) msg.obj; + final InsetsState insetsState = (InsetsState) args.arg1; + final InsetsSourceControl.Array activeControls = + (InsetsSourceControl.Array) args.arg2; + handleInsetsControlChanged(insetsState, activeControls); args.recycle(); break; } @@ -9828,25 +9838,9 @@ public final class ViewRootImpl implements ViewParent, mHandler.sendMessage(msg); } - private void dispatchInsetsControlChanged(InsetsState insetsState, - InsetsSourceControl[] activeControls) { - if (Binder.getCallingPid() == android.os.Process.myPid()) { - insetsState = new InsetsState(insetsState, true /* copySource */); - if (activeControls != null) { - for (int i = activeControls.length - 1; i >= 0; i--) { - activeControls[i] = new InsetsSourceControl(activeControls[i]); - } - } - } - if (mTranslator != null) { - mTranslator.translateInsetsStateInScreenToAppWindow(insetsState); - mTranslator.translateSourceControlsInScreenToAppWindow(activeControls); - } - if (insetsState != null && insetsState.isSourceOrDefaultVisible(ID_IME, Type.ime())) { - ImeTracing.getInstance().triggerClientDump("ViewRootImpl#dispatchInsetsControlChanged", - getInsetsController().getHost().getInputMethodManager(), null /* icProto */); - } - SomeArgs args = SomeArgs.obtain(); + private void dispatchInsetsControlChanged(@NonNull InsetsState insetsState, + @NonNull InsetsSourceControl.Array activeControls) { + final SomeArgs args = SomeArgs.obtain(); args.arg1 = insetsState; args.arg2 = activeControls; mHandler.obtainMessage(MSG_INSETS_CONTROL_CHANGED, args).sendToTarget(); @@ -11289,9 +11283,9 @@ public final class ViewRootImpl implements ViewParent, return; } // The the parameters from WindowStateResizeItem are already copied. - final boolean needCopy = + final boolean needsCopy = !isFromResizeItem && (Binder.getCallingPid() == Process.myPid()); - if (needCopy) { + if (needsCopy) { insetsState = new InsetsState(insetsState, true /* copySource */); frames = new ClientWindowFrames(frames); mergedConfiguration = new MergedConfiguration(mergedConfiguration); @@ -11307,10 +11301,35 @@ public final class ViewRootImpl implements ViewParent, final boolean isFromInsetsControlChangeItem = mIsFromTransactionItem; mIsFromTransactionItem = false; final ViewRootImpl viewAncestor = mViewAncestor.get(); - if (viewAncestor != null) { - viewAncestor.dispatchInsetsControlChanged(insetsState, activeControls.get()); + if (viewAncestor == null) { + if (isFromInsetsControlChangeItem) { + activeControls.release(); + } + return; + } + if (insetsState.isSourceOrDefaultVisible(ID_IME, Type.ime())) { + ImeTracing.getInstance().triggerClientDump( + "ViewRootImpl#dispatchInsetsControlChanged", + viewAncestor.getInsetsController().getHost().getInputMethodManager(), + null /* icProto */); + } + // If the UI thread is the same as the current thread that is dispatching + // WindowStateInsetsControlChangeItem, then it can run directly. + if (isFromInsetsControlChangeItem && viewAncestor.mHandler.getLooper() + == ActivityThread.currentActivityThread().getLooper()) { + viewAncestor.handleInsetsControlChanged(insetsState, activeControls); + return; } - // TODO(b/339380439): no need to post if the call is from InsetsControlChangeItem + // The parameters from WindowStateInsetsControlChangeItem are already copied. + final boolean needsCopy = + !isFromInsetsControlChangeItem && (Binder.getCallingPid() == Process.myPid()); + if (needsCopy) { + insetsState = new InsetsState(insetsState, true /* copySource */); + activeControls = new InsetsSourceControl.Array( + activeControls, true /* copyControls */); + } + + viewAncestor.dispatchInsetsControlChanged(insetsState, activeControls); } @Override diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java index 86e5bea46882..1af9387e6fbd 100644 --- a/core/java/android/view/ViewStructure.java +++ b/core/java/android/view/ViewStructure.java @@ -90,6 +90,19 @@ public abstract class ViewStructure { public static final String EXTRA_VIRTUAL_STRUCTURE_TYPE = "android.view.ViewStructure.extra.VIRTUAL_STRUCTURE_TYPE"; + + /** + * Key used for specifying the version of the view that generated the virtual structure for + * itself and its children + * + * For example, if the virtual structure is generated by a webview of version "104.0.5112.69", + * then the value should be "104.0.5112.69" + * + * @hide + */ + public static final String EXTRA_VIRTUAL_STRUCTURE_VERSION_NUMBER = + "android.view.ViewStructure.extra.VIRTUAL_STRUCTURE_VERSION_NUMBER"; + /** * Set the identifier for this view. * diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java index 12e08148a651..1fe8180aa7b2 100644 --- a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java +++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java @@ -302,6 +302,10 @@ public abstract class AccessibilityDisplayProxy { } @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) { + } + + @Override public void onMagnificationChanged(int displayId, @NonNull Region region, MagnificationConfig config) { } diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index ab7b2261dc17..edf33875b765 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -169,3 +169,13 @@ flag { description: "Feature flag for declaring system pinch zoom opt-out apis" bug: "315089687" } + +flag { + name: "wait_magnification_system_ui_connection_to_notify_service_connected" + namespace: "accessibility" + description: "Decide whether AccessibilityService needs to wait until magnification system ui connection is ready to trigger onServiceConnected" + bug: "337800504" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/widget/RemoteViewsService.java b/core/java/android/widget/RemoteViewsService.java index 07d6acbe38a8..c79eac605e64 100644 --- a/core/java/android/widget/RemoteViewsService.java +++ b/core/java/android/widget/RemoteViewsService.java @@ -132,7 +132,8 @@ public abstract class RemoteViewsService extends Service { RemoteViews.RemoteCollectionItems items = new RemoteViews.RemoteCollectionItems .Builder().build(); Parcel capSizeTestParcel = Parcel.obtain(); - capSizeTestParcel.allowSquashing(); + // restore allowSquashing to reduce the noise in error messages + boolean prevAllowSquashing = capSizeTestParcel.allowSquashing(); try { RemoteViews.RemoteCollectionItems.Builder itemsBuilder = @@ -154,6 +155,7 @@ public abstract class RemoteViewsService extends Service { items = itemsBuilder.build(); } finally { + capSizeTestParcel.restoreAllowSquashing(prevAllowSquashing); // Recycle the parcel capSizeTestParcel.recycle(); } diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java index 1bd921b339f6..d5398e6268dc 100644 --- a/core/java/android/window/ClientWindowFrames.java +++ b/core/java/android/window/ClientWindowFrames.java @@ -56,7 +56,16 @@ public class ClientWindowFrames implements Parcelable { public ClientWindowFrames() { } - public ClientWindowFrames(ClientWindowFrames other) { + public ClientWindowFrames(@NonNull ClientWindowFrames other) { + setTo(other); + } + + private ClientWindowFrames(@NonNull Parcel in) { + readFromParcel(in); + } + + /** Updates the current frames to the given frames. */ + public void setTo(@NonNull ClientWindowFrames other) { frame.set(other.frame); displayFrame.set(other.displayFrame); parentFrame.set(other.parentFrame); @@ -67,10 +76,6 @@ public class ClientWindowFrames implements Parcelable { compatScale = other.compatScale; } - private ClientWindowFrames(Parcel in) { - readFromParcel(in); - } - /** Needed for AIDL out parameters. */ public void readFromParcel(Parcel in) { frame.readFromParcel(in); diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java index f928f509bdb6..4c8bad6d0aff 100644 --- a/core/java/android/window/SnapshotDrawerUtils.java +++ b/core/java/android/window/SnapshotDrawerUtils.java @@ -77,6 +77,14 @@ public class SnapshotDrawerUtils { private static final String TAG = "SnapshotDrawerUtils"; /** + * Used to check if toolkitSetFrameRateReadOnly flag is enabled + * + * @hide + */ + private static boolean sToolkitSetFrameRateReadOnlyFlagValue = + android.view.flags.Flags.toolkitSetFrameRateReadOnly(); + + /** * When creating the starting window, we use the exact same layout flags such that we end up * with a window with the exact same dimensions etc. However, these flags are not used in layout * and might cause other side effects so we exclude them. @@ -439,6 +447,9 @@ public class SnapshotDrawerUtils { layoutParams.setFitInsetsTypes(attrs.getFitInsetsTypes()); layoutParams.setFitInsetsSides(attrs.getFitInsetsSides()); layoutParams.setFitInsetsIgnoringVisibility(attrs.isFitInsetsIgnoringVisibility()); + if (sToolkitSetFrameRateReadOnlyFlagValue) { + layoutParams.setFrameRatePowerSavingsBalanced(false); + } layoutParams.setTitle(title); layoutParams.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL; diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index 983f46c58c4b..5b99ff9703e0 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -73,6 +73,16 @@ flag { } flag { + name: "immersive_app_repositioning" + namespace: "large_screen_experiences_app_compat" + description: "Fix immersive apps changing size when repositioning" + bug: "334076352" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "camera_compat_for_freeform" namespace: "large_screen_experiences_app_compat" description: "Whether to apply Camera Compat treatment to fixed-orientation apps in freeform windowing mode" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index 21aa4800237c..9e69f8910bab 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -158,4 +158,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + namespace: "windowing_sdk" + name: "move_animation_options_to_change" + description: "Move AnimationOptions from TransitionInfo to each Change" + bug: "327332488" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 55c6ad1d0581..c14a6c1ca5ba 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -812,7 +812,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind final MenuHelper helper; final boolean isPopup = !Float.isNaN(x) && !Float.isNaN(y); if (isPopup) { - helper = mWindow.mContextMenu.showPopup(getContext(), originalView, x, y); + helper = mWindow.mContextMenu.showPopup(originalView.getContext(), originalView, x, y); } else { helper = mWindow.mContextMenu.showDialog(originalView, originalView.getWindowToken()); } diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java index 2f09a5550fd4..66b2a9c8a424 100644 --- a/core/java/com/android/internal/policy/TransitionAnimation.java +++ b/core/java/com/android/internal/policy/TransitionAnimation.java @@ -1299,6 +1299,21 @@ public class TransitionAnimation { == HardwareBuffer.USAGE_PROTECTED_CONTENT; } + /** + * Returns the luminance in 0~1. The surface control is the source of the hardware buffer, + * which will be used if the buffer is protected from reading. + */ + public static float getBorderLuma(@NonNull HardwareBuffer hwBuffer, + @NonNull ColorSpace colorSpace, @NonNull SurfaceControl sourceSurfaceControl) { + if (hasProtectedContent(hwBuffer)) { + // The buffer cannot be read. Capture another buffer which excludes protected content + // from the source surface. + return getBorderLuma(sourceSurfaceControl, hwBuffer.getWidth(), hwBuffer.getHeight()); + } + // Use the existing buffer directly. + return getBorderLuma(hwBuffer, colorSpace); + } + /** Returns the luminance in 0~1. */ public static float getBorderLuma(SurfaceControl surfaceControl, int w, int h) { final ScreenCapture.ScreenshotHardwareBuffer buffer = diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 37b72880dd0c..42fa6ac0407d 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -135,7 +135,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { new DataSourceParams.Builder() .setBufferExhaustedPolicy( DataSourceParams - .PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT) + .PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP) .build(); mDataSource.register(params); this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; diff --git a/core/java/com/android/internal/util/NotificationBigTextNormalizer.java b/core/java/com/android/internal/util/NotificationBigTextNormalizer.java new file mode 100644 index 000000000000..80d409500ef0 --- /dev/null +++ b/core/java/com/android/internal/util/NotificationBigTextNormalizer.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + + +import android.annotation.NonNull; +import android.os.Trace; + +import java.util.regex.Pattern; + +/** + * Utility class that normalizes BigText style Notification content. + * @hide + */ +public class NotificationBigTextNormalizer { + + private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?"); + private static final Pattern HORIZONTAL_WHITESPACES = Pattern.compile("\\h+"); + + // Private constructor to prevent instantiation + private NotificationBigTextNormalizer() {} + + /** + * Normalizes the given text by collapsing consecutive new lines into single one and cleaning + * up each line by removing zero-width characters, invisible formatting characters, and + * collapsing consecutive whitespace into single space. + */ + @NonNull + public static String normalizeBigText(@NonNull String text) { + try { + Trace.beginSection("NotifBigTextNormalizer#normalizeBigText"); + text = MULTIPLE_NEWLINES.matcher(text).replaceAll("\n"); + text = HORIZONTAL_WHITESPACES.matcher(text).replaceAll(" "); + text = normalizeLines(text); + return text; + } finally { + Trace.endSection(); + } + } + + /** + * Normalizes lines in a text by removing zero-width characters, invisible formatting + * characters, and collapsing consecutive whitespace into single space. + * + * <p> + * This method processes the input text line by line. It eliminates zero-width + * characters (U+200B to U+200D, U+FEFF, U+034F), invisible formatting + * characters (U+2060 to U+2065, U+206A to U+206F, U+FFF9 to U+FFFB), + * and replaces any sequence of consecutive whitespace characters with a single space. + * </p> + * + * <p> + * Additionally, the method trims trailing whitespace from each line and removes any + * resulting empty lines. + * </p> + */ + @NonNull + private static String normalizeLines(@NonNull String text) { + String[] lines = text.split("\n"); + final StringBuilder textSB = new StringBuilder(text.length()); + for (int i = 0; i < lines.length; i++) { + final String line = lines[i]; + final StringBuilder lineSB = new StringBuilder(line.length()); + boolean spaceSeen = false; + for (int j = 0; j < line.length(); j++) { + final char character = line.charAt(j); + + // Skip ZERO WIDTH characters + if ((character >= '\u200B' && character <= '\u200D') + || character == '\uFEFF' || character == '\u034F') { + continue; + } + // Skip INVISIBLE_FORMATTING_CHARACTERS + if ((character >= '\u2060' && character <= '\u2065') + || (character >= '\u206A' && character <= '\u206F') + || (character >= '\uFFF9' && character <= '\uFFFB')) { + continue; + } + + if (isSpace(character)) { + // eliminate consecutive spaces.... + if (!spaceSeen) { + lineSB.append(" "); + } + spaceSeen = true; + } else { + spaceSeen = false; + lineSB.append(character); + } + } + // trim line. + final String currentLine = lineSB.toString().trim(); + + // don't add empty lines after trim. + if (currentLine.length() > 0) { + if (textSB.length() > 0) { + textSB.append("\n"); + } + textSB.append(currentLine); + } + } + + return textSB.toString(); + } + + private static boolean isSpace(char ch) { + return ch != '\n' && Character.isSpaceChar(ch); + } +} diff --git a/core/java/com/android/internal/widget/NotificationRowIconView.java b/core/java/com/android/internal/widget/NotificationRowIconView.java index 0f4615a12ea2..58bddaecd3e7 100644 --- a/core/java/com/android/internal/widget/NotificationRowIconView.java +++ b/core/java/com/android/internal/widget/NotificationRowIconView.java @@ -59,7 +59,7 @@ public class NotificationRowIconView extends CachingIconView { @Override protected void onFinishInflate() { // If showing the app icon, we don't need background or padding. - if (Flags.notificationsUseAppIcon()) { + if (Flags.notificationsUseAppIcon() || Flags.notificationsUseAppIconInRow()) { setPadding(0, 0, 0, 0); setBackground(null); } diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index d48cdc4645c6..eaff7608ce3b 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -713,6 +713,19 @@ android_media_AudioSystem_getForceUse(JNIEnv *env, jobject thiz, jint usage) AudioSystem::getForceUse(static_cast<audio_policy_force_use_t>(usage))); } +static jint android_media_AudioSystem_setDeviceAbsoluteVolumeEnabled(JNIEnv *env, jobject thiz, + jint device, jstring address, + jboolean enabled, + jint stream) { + const char *c_address = env->GetStringUTFChars(address, nullptr); + int state = check_AudioSystem_Command( + AudioSystem::setDeviceAbsoluteVolumeEnabled(static_cast<audio_devices_t>(device), + c_address, enabled, + static_cast<audio_stream_type_t>(stream))); + env->ReleaseStringUTFChars(address, c_address); + return state; +} + static jint android_media_AudioSystem_initStreamVolume(JNIEnv *env, jobject thiz, jint stream, jint indexMin, jint indexMax) { @@ -3373,6 +3386,7 @@ static const JNINativeMethod gMethods[] = MAKE_AUDIO_SYSTEM_METHOD(setPhoneState), MAKE_AUDIO_SYSTEM_METHOD(setForceUse), MAKE_AUDIO_SYSTEM_METHOD(getForceUse), + MAKE_AUDIO_SYSTEM_METHOD(setDeviceAbsoluteVolumeEnabled), MAKE_AUDIO_SYSTEM_METHOD(initStreamVolume), MAKE_AUDIO_SYSTEM_METHOD(setStreamVolumeIndex), MAKE_AUDIO_SYSTEM_METHOD(getStreamVolumeIndex), diff --git a/core/jni/com_android_internal_content_FileSystemUtils.cpp b/core/jni/com_android_internal_content_FileSystemUtils.cpp index 31f4e641b69e..d426f1240a7f 100644 --- a/core/jni/com_android_internal_content_FileSystemUtils.cpp +++ b/core/jni/com_android_internal_content_FileSystemUtils.cpp @@ -88,7 +88,7 @@ bool punchHoles(const char *filePath, const uint64_t offset, ALOGD("Total number of LOAD segments %zu", programHeaders.size()); ALOGD("Size before punching holes st_blocks: %" PRIu64 - ", st_blksize: %ld, st_size: %" PRIu64 "", + ", st_blksize: %d, st_size: %" PRIu64 "", beforePunch.st_blocks, beforePunch.st_blksize, static_cast<uint64_t>(beforePunch.st_size)); } @@ -193,7 +193,7 @@ bool punchHoles(const char *filePath, const uint64_t offset, ALOGD("lstat64 failed for filePath %s, error:%d", filePath, errno); return false; } - ALOGD("Size after punching holes st_blocks: %" PRIu64 ", st_blksize: %ld, st_size: %" PRIu64 + ALOGD("Size after punching holes st_blocks: %" PRIu64 ", st_blksize: %d, st_size: %" PRIu64 "", afterPunch.st_blocks, afterPunch.st_blksize, static_cast<uint64_t>(afterPunch.st_size)); @@ -271,7 +271,7 @@ bool punchHolesInZip(const char *filePath, uint64_t offset, uint16_t extraFieldL uint64_t blockSize = beforePunch.st_blksize; IF_ALOGD() { ALOGD("Extra field length: %hu, Size before punching holes st_blocks: %" PRIu64 - ", st_blksize: %ld, st_size: %" PRIu64 "", + ", st_blksize: %d, st_size: %" PRIu64 "", extraFieldLen, beforePunch.st_blocks, beforePunch.st_blksize, static_cast<uint64_t>(beforePunch.st_size)); } @@ -346,7 +346,7 @@ bool punchHolesInZip(const char *filePath, uint64_t offset, uint16_t extraFieldL return false; } ALOGD("punchHolesInApk:: Size after punching holes st_blocks: %" PRIu64 - ", st_blksize: %ld, st_size: %" PRIu64 "", + ", st_blksize: %d, st_size: %" PRIu64 "", afterPunch.st_blocks, afterPunch.st_blksize, static_cast<uint64_t>(afterPunch.st_size)); } diff --git a/core/proto/android/app/appexitinfo.proto b/core/proto/android/app/appexitinfo.proto index 3abc462671a2..e560a944b94b 100644 --- a/core/proto/android/app/appexitinfo.proto +++ b/core/proto/android/app/appexitinfo.proto @@ -20,7 +20,7 @@ option java_multiple_files = true; package android.app; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/app_enums.proto"; /** * An android.app.ApplicationExitInfo object. diff --git a/core/proto/android/app/appstartinfo.proto b/core/proto/android/app/appstartinfo.proto index d9ed911515ba..c13753343ba8 100644 --- a/core/proto/android/app/appstartinfo.proto +++ b/core/proto/android/app/appstartinfo.proto @@ -20,7 +20,7 @@ option java_multiple_files = true; package android.app; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/app_enums.proto"; /** * An android.app.ApplicationStartInfo object. diff --git a/core/proto/android/os/batterystats.proto b/core/proto/android/os/batterystats.proto index 4c84944a7382..97f81484b84d 100644 --- a/core/proto/android/os/batterystats.proto +++ b/core/proto/android/os/batterystats.proto @@ -21,7 +21,7 @@ package android.os; import "frameworks/base/core/proto/android/os/powermanager.proto"; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/job/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/job/job_enums.proto"; import "frameworks/proto_logging/stats/enums/telephony/enums.proto"; message BatteryStatsProto { diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto index e3a438da5abc..921c41c8e52b 100644 --- a/core/proto/android/server/activitymanagerservice.proto +++ b/core/proto/android/server/activitymanagerservice.proto @@ -35,7 +35,7 @@ import "frameworks/base/core/proto/android/server/intentresolver.proto"; import "frameworks/base/core/proto/android/server/windowmanagerservice.proto"; import "frameworks/base/core/proto/android/util/common.proto"; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/app_enums.proto"; option java_multiple_files = true; diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto index 00127c134ce6..a1e3dc1b9b4e 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -31,7 +31,7 @@ import "frameworks/base/core/proto/android/server/appstatetracker.proto"; import "frameworks/base/core/proto/android/server/statlogger.proto"; import "frameworks/base/core/proto/android/privacy.proto"; import "frameworks/base/core/proto/android/util/quotatracker.proto"; -import "frameworks/proto_logging/stats/enums/app/job/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/job/job_enums.proto"; import "frameworks/proto_logging/stats/enums/server/job/enums.proto"; message JobSchedulerServiceDumpProto { diff --git a/core/proto/android/server/powermanagerservice.proto b/core/proto/android/server/powermanagerservice.proto index 2f865afd28c7..593bbc6f5d0d 100644 --- a/core/proto/android/server/powermanagerservice.proto +++ b/core/proto/android/server/powermanagerservice.proto @@ -26,7 +26,7 @@ import "frameworks/base/core/proto/android/os/worksource.proto"; import "frameworks/base/core/proto/android/providers/settings.proto"; import "frameworks/base/core/proto/android/server/wirelesschargerdetector.proto"; import "frameworks/base/core/proto/android/privacy.proto"; -import "frameworks/proto_logging/stats/enums/app/enums.proto"; +import "frameworks/proto_logging/stats/enums/app/app_enums.proto"; import "frameworks/proto_logging/stats/enums/os/enums.proto"; import "frameworks/proto_logging/stats/enums/view/enums.proto"; diff --git a/core/tests/coretests/OWNERS b/core/tests/coretests/OWNERS index b7e008b196ff..b669e3bc4f30 100644 --- a/core/tests/coretests/OWNERS +++ b/core/tests/coretests/OWNERS @@ -3,3 +3,4 @@ include platform/frameworks/base:/services/core/java/com/android/server/am/OWNER per-file BinderTest.java = file:platform/frameworks/native:/libs/binder/OWNERS per-file ParcelTest.java = file:platform/frameworks/native:/libs/binder/OWNERS per-file SurfaceControlRegistryTests.java = file:/services/core/java/com/android/server/wm/OWNERS +per-file VintfObjectTest.java = file:platform/system/libvintf:/OWNERS diff --git a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java index 89c2b3cecfef..b972882e68e6 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java @@ -128,7 +128,8 @@ public class ActivityManagerTest extends AndroidTestCase { 0x222222, // colorBackground 0x333333, // statusBarColor 0x444444, // navigationBarColor - 0, // statusBarAppearance + 0x555555, // systemBarsAppeareance + 0x666666, // topOpaqueSystemBarsAppeareance true, // ensureStatusBarContrastWhenTransparent true, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_RESIZEABLE, // resizeMode @@ -153,7 +154,8 @@ public class ActivityManagerTest extends AndroidTestCase { 0x222222, // colorBackground 0x333333, // statusBarColor 0x444444, // navigationBarColor - 0, // statusBarAppearance + 0x555555, // systemBarsAppeareance + 0x666666, // topOpaqueSystemBarsAppeareance false, // ensureStatusBarContrastWhenTransparent false, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_UNRESIZEABLE, // resizeMode @@ -169,7 +171,8 @@ public class ActivityManagerTest extends AndroidTestCase { 0x2222222, // colorBackground 0x3333332, // statusBarColor 0x4444442, // navigationBarColor - 0, // statusBarAppearance + 0x5555552, // systemBarsAppeareance + 0x6666662, // topOpaqueSystemBarsAppeareance true, // ensureStatusBarContrastWhenTransparent true, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_RESIZEABLE, // resizeMode @@ -200,7 +203,8 @@ public class ActivityManagerTest extends AndroidTestCase { 0x222222, // colorBackground 0x333333, // statusBarColor 0x444444, // navigationBarColor - 0, // statusBarAppearance + 0x555555, // systemBarsAppeareance + 0x666666, // topOpaqueSystemBarsAppeareance false, // ensureStatusBarContrastWhenTransparent false, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_UNRESIZEABLE, // resizeMode @@ -223,7 +227,8 @@ public class ActivityManagerTest extends AndroidTestCase { 0x222222, // colorBackground 0x333333, // statusBarColor 0x444444, // navigationBarColor - 0, // statusBarAppearance + 0x555555, // systemBarsAppeareance + 0x666666, // topOpaqueSystemBarsAppeareance false, // ensureStatusBarContrastWhenTransparent false, // ensureNavigationBarContrastWhenTransparent RESIZE_MODE_UNRESIZEABLE, // resizeMode @@ -256,6 +261,8 @@ public class ActivityManagerTest extends AndroidTestCase { assertEquals(td1.getStatusBarColor(), td2.getStatusBarColor()); assertEquals(td1.getNavigationBarColor(), td2.getNavigationBarColor()); assertEquals(td1.getSystemBarsAppearance(), td2.getSystemBarsAppearance()); + assertEquals(td1.getTopOpaqueSystemBarsAppearance(), + td2.getTopOpaqueSystemBarsAppearance()); assertEquals(td1.getResizeMode(), td2.getResizeMode()); assertEquals(td1.getMinWidth(), td2.getMinWidth()); assertEquals(td1.getMinHeight(), td2.getMinHeight()); diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java index 3735274c1a6c..c7060adc1ca1 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java @@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.Activity; @@ -39,7 +41,6 @@ import android.view.InsetsSourceControl; import android.view.InsetsState; import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; -import android.window.WindowContext; import android.window.WindowContextInfo; import androidx.test.filters.SmallTest; @@ -73,8 +74,6 @@ public class ClientTransactionItemTest { @Mock private IBinder mWindowClientToken; @Mock - private WindowContext mWindowContext; - @Mock private IWindow mWindow; // Can't mock final class. @@ -176,4 +175,17 @@ public class ClientTransactionItemTest { verify(mWindow).insetsControlChanged(mInsetsState, mActiveControls); } + + @Test + public void testWindowStateInsetsControlChangeItem_executeError() throws RemoteException { + doThrow(new RemoteException()).when(mWindow).insetsControlChanged(any(), any()); + + mActiveControls = spy(mActiveControls); + final WindowStateInsetsControlChangeItem item = WindowStateInsetsControlChangeItem.obtain( + mWindow, mInsetsState, mActiveControls); + item.mActiveControls = mActiveControls; + item.execute(mHandler, mPendingActions); + + verify(mActiveControls).release(); + } } diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java index 1c1236279b61..98f8b7fc897c 100644 --- a/core/tests/coretests/src/android/text/LayoutTest.java +++ b/core/tests/coretests/src/android/text/LayoutTest.java @@ -39,6 +39,7 @@ import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.text.Layout.Alignment; +import android.text.style.ForegroundColorSpan; import android.text.style.StrikethroughSpan; import androidx.test.filters.SmallTest; @@ -933,6 +934,83 @@ public class LayoutTest { expect.that(numBackgroundsFound).isEqualTo(backgroundRectsDrawn); } + @Test + @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void highContrastTextEnabled_testDrawMulticolorText_drawsBlackAndWhiteBackgrounds() { + /* + Here's what the final render should look like: + + Text | Background + ======================== + al | BW + w | WW + ei | WW + \t; | WW + s | BB + df | BB + s | BB + df | BB + @ | BB + ------------------------ + */ + + mTextPaint.setColor(Color.WHITE); + + mSpannedText.setSpan( + // Can't use DKGREY because it is right on the cusp of clamping white + new ForegroundColorSpan(0xFF332211), + /* start= */ 1, + /* end= */ 6, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ); + mSpannedText.setSpan( + new ForegroundColorSpan(Color.LTGRAY), + /* start= */ 8, + /* end= */ 11, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ); + Layout layout = new StaticLayout(mSpannedText, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); + + final int width = 256; + final int height = 256; + MockCanvas c = new MockCanvas(width, height); + c.setHighContrastTextEnabled(true); + layout.draw( + c, + /* highlightPaths= */ null, + /* highlightPaints= */ null, + /* selectionPath= */ null, + /* selectionPaint= */ null, + /* cursorOffsetVertical= */ 0 + ); + List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); + var highlightsDrawn = 0; + var numColorChangesWithinOneLine = 1; + var textsDrawn = STATIC_LINE_COUNT + numColorChangesWithinOneLine; + var backgroundRectsDrawn = STATIC_LINE_COUNT + numColorChangesWithinOneLine; + expect.withMessage("wrong number of drawCommands: " + drawCommands) + .that(drawCommands.size()) + .isEqualTo(textsDrawn + backgroundRectsDrawn + highlightsDrawn); + + var backgroundCommands = drawCommands.stream() + .filter(it -> it.rect != null) + .toList(); + + expect.that(backgroundCommands.get(0).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(1).paint.getColor()).isEqualTo(Color.WHITE); + expect.that(backgroundCommands.get(2).paint.getColor()).isEqualTo(Color.WHITE); + expect.that(backgroundCommands.get(3).paint.getColor()).isEqualTo(Color.WHITE); + expect.that(backgroundCommands.get(4).paint.getColor()).isEqualTo(Color.WHITE); + expect.that(backgroundCommands.get(5).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(6).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(7).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(8).paint.getColor()).isEqualTo(Color.BLACK); + expect.that(backgroundCommands.get(9).paint.getColor()).isEqualTo(Color.BLACK); + + expect.that(backgroundCommands.size()).isEqualTo(backgroundRectsDrawn); + } + private static final class MockCanvas extends Canvas { static class DrawCommand { diff --git a/core/tests/coretests/src/android/text/SpanColorsTest.java b/core/tests/coretests/src/android/text/SpanColorsTest.java new file mode 100644 index 000000000000..3d8d8f9c126d --- /dev/null +++ b/core/tests/coretests/src/android/text/SpanColorsTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; +import android.graphics.drawable.ShapeDrawable; +import android.platform.test.annotations.Presubmit; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.UnderlineSpan; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SpanColorsTest { + private final TextPaint mWorkPaint = new TextPaint(); + private SpanColors mSpanColors; + private SpannableString mSpannedText; + + @Before + public void setup() { + mSpanColors = new SpanColors(); + mSpannedText = new SpannableString("Hello world! This is a test."); + mSpannedText.setSpan(new ForegroundColorSpan(Color.RED), 0, 4, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + mSpannedText.setSpan(new ForegroundColorSpan(Color.GREEN), 6, 11, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + mSpannedText.setSpan(new UnderlineSpan(), 5, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + mSpannedText.setSpan(new ImageSpan(new ShapeDrawable()), 1, 2, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + mSpannedText.setSpan(new ForegroundColorSpan(Color.BLUE), 12, 16, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void testNoColorFound() { + mSpanColors.init(mWorkPaint, mSpannedText, 25, 30); // Beyond the spans + assertThat(mSpanColors.getColorAt(27)).isEqualTo(SpanColors.NO_COLOR_FOUND); + } + + @Test + public void testSingleColorSpan() { + mSpanColors.init(mWorkPaint, mSpannedText, 1, 4); + assertThat(mSpanColors.getColorAt(3)).isEqualTo(Color.RED); + } + + @Test + public void testMultipleColorSpans() { + mSpanColors.init(mWorkPaint, mSpannedText, 0, mSpannedText.length()); + assertThat(mSpanColors.getColorAt(2)).isEqualTo(Color.RED); + assertThat(mSpanColors.getColorAt(5)).isEqualTo(SpanColors.NO_COLOR_FOUND); + assertThat(mSpanColors.getColorAt(8)).isEqualTo(Color.GREEN); + assertThat(mSpanColors.getColorAt(13)).isEqualTo(Color.BLUE); + } +} diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index a7f817665f23..94e187aed698 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -1287,6 +1287,31 @@ public class ViewRootImplTest { } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY) + public void votePreferredFrameRate_velocityVotedAfterOnDraw() throws Throwable { + mView = new View(sContext); + double delta = 0.1; + float pixelsPerSecond = 1000_000; + float expectedFrameRate = 120; + attachViewToWindow(mView); + sInstrumentation.waitForIdleSync(); + ViewRootImpl viewRoot = mView.getViewRootImpl(); + waitForFrameRateCategoryToSettle(mView); + + sInstrumentation.runOnMainSync(() -> { + mView.setFrameContentVelocity(pixelsPerSecond); + mView.invalidate(); + assertEquals(0, viewRoot.getPreferredFrameRate(), delta); + assertEquals(0, viewRoot.getLastPreferredFrameRate(), delta); + runAfterDraw(() -> { + assertEquals(expectedFrameRate, viewRoot.getPreferredFrameRate(), delta); + assertEquals(expectedFrameRate, viewRoot.getLastPreferredFrameRate(), delta); + }); + }); + waitForAfterDraw(); + } + + @Test public void forceInvertOffDarkThemeOff_forceDarkModeDisabled() { mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR); ShellIdentityUtils.invokeWithShellPermissions(() -> { diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java index b5c264c4ae5e..5a6824bf0d7e 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java @@ -146,6 +146,10 @@ public class AccessibilityServiceConnectionImpl extends IAccessibilityServiceCon public void setMagnificationCallbackEnabled(int displayId, boolean enabled) {} + public boolean isMagnificationSystemUIConnected() { + return false; + } + public boolean setSoftKeyboardShowMode(int showMode) { return false; } diff --git a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java deleted file mode 100644 index bcdac610a49d..000000000000 --- a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.util; - -import static junit.framework.Assert.assertEquals; - - -import android.platform.test.annotations.DisabledOnRavenwood; -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test for {@link NewlineNormalizer} - * @hide - */ -@DisabledOnRavenwood(blockedBy = NewlineNormalizer.class) -@RunWith(AndroidJUnit4.class) -public class NewlineNormalizerTest { - - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule(); - - @Test - public void testEmptyInput() { - assertEquals("", NewlineNormalizer.normalizeNewlines("")); - } - - @Test - public void testSingleNewline() { - assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n")); - } - - @Test - public void testMultipleConsecutiveNewlines() { - assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n\n\n\n\n")); - } - - @Test - public void testNewlinesWithSpacesAndTabs() { - String input = "Line 1\n \n \t \n\tLine 2"; - // Adjusted expected output to include the tab character - String expected = "Line 1\n\tLine 2"; - assertEquals(expected, NewlineNormalizer.normalizeNewlines(input)); - } - - @Test - public void testMixedNewlineCharacters() { - String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6"; - String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6"; - assertEquals(expected, NewlineNormalizer.normalizeNewlines(input)); - } -} diff --git a/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java new file mode 100644 index 000000000000..1f2e24aa8c68 --- /dev/null +++ b/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + +import static junit.framework.Assert.assertEquals; + + +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test for {@link NotificationBigTextNormalizer} + * @hide + */ +@DisabledOnRavenwood(blockedBy = NotificationBigTextNormalizer.class) +@RunWith(AndroidJUnit4.class) +public class NotificationBigTextNormalizerTest { + + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + + @Test + public void testEmptyInput() { + assertEquals("", NotificationBigTextNormalizer.normalizeBigText("")); + } + + @Test + public void testSingleNewline() { + assertEquals("", NotificationBigTextNormalizer.normalizeBigText("\n")); + } + + @Test + public void testMultipleConsecutiveNewlines() { + assertEquals("", NotificationBigTextNormalizer.normalizeBigText("\n\n\n\n\n")); + } + + @Test + public void testNewlinesWithSpacesAndTabs() { + String input = "Line 1\n \n \t \n\tLine 2"; + // Adjusted expected output to include the tab character + String expected = "Line 1\nLine 2"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + + @Test + public void testMixedNewlineCharacters() { + String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6"; + String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + + @Test + public void testConsecutiveSpaces() { + // Only spaces + assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This" + + " is a test.")); + // Zero width characters bw spaces. + assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This" + + "\u200B \u200B \u200B \u200B \u200B \u200B \u200B \u200Bis\uFEFF \uFEFF \uFEFF" + + " \uFEFFa \u034F \u034F \u034F \u034F \u034F \u034Ftest.")); + + // Invisible formatting characters bw spaces. + assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This" + + "\u2061 \u2061 \u2061 \u2061 \u2061 \u2061 \u2061 \u2061is\u206E \u206E \u206E" + + " \u206Ea \uFFFB \uFFFB \uFFFB \uFFFB \uFFFB \uFFFBtest.")); + // Non breakable spaces + assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This" + + "\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0is\u2005 \u2005 \u2005" + + " \u2005a\u2005\u2005\u2005 \u2005\u2005\u2005test.")); + } + + @Test + public void testZeroWidthCharRemoval() { + // Test each character individually + char[] zeroWidthChars = { '\u200B', '\u200C', '\u200D', '\uFEFF', '\u034F' }; + + for (char c : zeroWidthChars) { + String input = "Test" + c + "string"; + String expected = "Teststring"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + } + + @Test + public void testWhitespaceReplacement() { + assertEquals("This text has horizontal whitespace.", + NotificationBigTextNormalizer.normalizeBigText( + "This\ttext\thas\thorizontal\twhitespace.")); + assertEquals("This text has mixed whitespace.", + NotificationBigTextNormalizer.normalizeBigText( + "This text has \u00A0 mixed\u2009whitespace.")); + assertEquals("This text has leading and trailing whitespace.", + NotificationBigTextNormalizer.normalizeBigText( + "\t This text has leading and trailing whitespace. \n")); + } + + @Test + public void testInvisibleFormattingCharacterRemoval() { + // Test each character individually + char[] invisibleFormattingChars = { + '\u2060', '\u2061', '\u2062', '\u2063', '\u2064', '\u2065', + '\u206A', '\u206B', '\u206C', '\u206D', '\u206E', '\u206F', + '\uFFF9', '\uFFFA', '\uFFFB' + }; + + for (char c : invisibleFormattingChars) { + String input = "Test " + c + "string"; + String expected = "Test string"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + } + @Test + public void testNonBreakSpaceReplacement() { + // Test each character individually + char[] nonBreakSpaces = { + '\u00A0', '\u1680', '\u2000', '\u2001', '\u2002', + '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', + '\u2008', '\u2009', '\u200A', '\u202F', '\u205F', '\u3000' + }; + + for (char c : nonBreakSpaces) { + String input = "Test" + c + "string"; + String expected = "Test string"; + assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input)); + } + } +} diff --git a/data/etc/core.protolog.pb b/data/etc/core.protolog.pb Binary files differindex ddb706e93ffc..a105ba756d51 100644 --- a/data/etc/core.protolog.pb +++ b/data/etc/core.protolog.pb diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 80f143c63333..db68f9528151 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -2035,6 +2035,24 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/PhysicalDisplaySwitchTransitionLauncher.java" }, + "-1640401313436844534": { + "message": "Resetting frozen recents task list reason=app touch win=%s x=%d y=%d insetFrame=%s", + "level": "INFO", + "group": "WM_DEBUG_TASKS", + "at": "com\/android\/server\/wm\/RecentTasks.java" + }, + "-8803811426486764449": { + "message": "Setting frozen recents task list", + "level": "INFO", + "group": "WM_DEBUG_TASKS", + "at": "com\/android\/server\/wm\/RecentTasks.java" + }, + "4040735335719974079": { + "message": "Resetting frozen recents task list reason=timeout", + "level": "INFO", + "group": "WM_DEBUG_TASKS", + "at": "com\/android\/server\/wm\/RecentTasks.java" + }, "3308140128142966415": { "message": "remove RecentTask %s when finishing user %d", "level": "INFO", diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java index d359a9050a0f..3cff91597939 100644 --- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java +++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java @@ -1149,6 +1149,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu /** * Sets the serial number used for the certificate of the generated key pair. + * To ensure compatibility with devices and certificate parsers, the value + * should be 20 bytes or shorter (see RFC 5280 section 4.1.2.2). * * <p>By default, the serial number is {@code 1}. */ diff --git a/ktfmt_includes.txt b/ktfmt_includes.txt deleted file mode 100644 index 0ac6265ecf27..000000000000 --- a/ktfmt_includes.txt +++ /dev/null @@ -1,740 +0,0 @@ -+services/permission -+packages/SystemUI --packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt --packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt --packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt --packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt --packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt --packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt --packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/View.kt --packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt --packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt --packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt --packages/SystemUI/shared/src/com/android/systemui/flags/FlagSerializer.kt --packages/SystemUI/shared/src/com/android/systemui/flags/FlagSettingsHelper.kt --packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt --packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionDarkness.kt --packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt --packages/SystemUI/shared/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerManager.kt --packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt --packages/SystemUI/shared/src/com/android/systemui/unfold/system/ActivityManagerActivityTypeProvider.kt --packages/SystemUI/shared/src/com/android/systemui/unfold/system/DeviceStateManagerFoldProvider.kt --packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt --packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt --packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt --packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt --packages/SystemUI/src/com/android/keyguard/BouncerPanelExpansionCalculator.kt --packages/SystemUI/src/com/android/keyguard/ClockEventController.kt --packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt --packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt --packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherAnchor.kt --packages/SystemUI/src/com/android/keyguard/clock/ClockPalette.kt --packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt --packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt --packages/SystemUI/src/com/android/systemui/BootCompleteCache.kt --packages/SystemUI/src/com/android/systemui/BootCompleteCacheImpl.kt --packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt --packages/SystemUI/src/com/android/systemui/ChooserSelector.kt --packages/SystemUI/src/com/android/systemui/DarkReceiverImpl.kt --packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt --packages/SystemUI/src/com/android/systemui/DualToneHandler.kt --packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt --packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt --packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt --packages/SystemUI/src/com/android/systemui/SystemUIInitializerFactory.kt --packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt --packages/SystemUI/src/com/android/systemui/assist/AssistLogger.kt --packages/SystemUI/src/com/android/systemui/assist/AssistantInvocationEvent.kt --packages/SystemUI/src/com/android/systemui/assist/AssistantSessionEvent.kt --packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt --packages/SystemUI/src/com/android/systemui/biometrics/AlternateUdfpsTouchProvider.kt --packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt --packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.kt --packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt --packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt --packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt --packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.kt --packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt --packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt --packages/SystemUI/src/com/android/systemui/biometrics/BiometricDisplayListener.kt --packages/SystemUI/src/com/android/systemui/biometrics/DwellRippleShader.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpDrawable.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt --packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt --packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt --packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt --packages/SystemUI/src/com/android/systemui/broadcast/ActionReceiver.kt --packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt --packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt --packages/SystemUI/src/com/android/systemui/broadcast/BroadcastSender.kt --packages/SystemUI/src/com/android/systemui/broadcast/PendingRemovalStore.kt --packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt --packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt --packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt --packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt --packages/SystemUI/src/com/android/systemui/controls/ControlStatus.kt --packages/SystemUI/src/com/android/systemui/controls/ControlsMetricsLogger.kt --packages/SystemUI/src/com/android/systemui/controls/ControlsMetricsLoggerImpl.kt --packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt --packages/SystemUI/src/com/android/systemui/controls/CustomIconCache.kt --packages/SystemUI/src/com/android/systemui/controls/TooltipManager.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ControlInfo.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ControlsTileResourceConfiguration.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ControlsTileResourceConfigurationImpl.kt --packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt --packages/SystemUI/src/com/android/systemui/controls/controller/StatefulControlSubscriber.kt --packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt --packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsFeatureEnabled.kt --packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt --packages/SystemUI/src/com/android/systemui/controls/management/AllModel.kt --packages/SystemUI/src/com/android/systemui/controls/management/AppAdapter.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt --packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt --packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt --packages/SystemUI/src/com/android/systemui/controls/management/ManagementPageIndicator.kt --packages/SystemUI/src/com/android/systemui/controls/management/StructureAdapter.kt --packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt --packages/SystemUI/src/com/android/systemui/controls/ui/Behavior.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ChallengeDialogs.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinator.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ControlWithState.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt --packages/SystemUI/src/com/android/systemui/controls/ui/CornerDrawable.kt --packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt --packages/SystemUI/src/com/android/systemui/controls/ui/RenderInfo.kt --packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt --packages/SystemUI/src/com/android/systemui/controls/ui/TemperatureControlBehavior.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ThumbnailBehavior.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ToggleBehavior.kt --packages/SystemUI/src/com/android/systemui/controls/ui/ToggleRangeBehavior.kt --packages/SystemUI/src/com/android/systemui/controls/ui/TouchBehavior.kt --packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt --packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderFactory.kt --packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt --packages/SystemUI/src/com/android/systemui/decor/DecorProvider.kt --packages/SystemUI/src/com/android/systemui/decor/DecorProviderFactory.kt --packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt --packages/SystemUI/src/com/android/systemui/decor/OverlayWindow.kt --packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt --packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderFactory.kt --packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderImpl.kt --packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt --packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt --packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt --packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt --packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt --packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt --packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt --packages/SystemUI/src/com/android/systemui/dump/DumpsysTableLogger.kt --packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt --packages/SystemUI/src/com/android/systemui/dump/LogBufferFreezer.kt --packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt --packages/SystemUI/src/com/android/systemui/flags/Flags.kt --packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt --packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt --packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt --packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt --packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt --packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartable.kt --packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt --packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt --packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt --packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt --packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt --packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt --packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt --packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt --packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt --packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt --packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt --packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt --packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt --packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt --packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt --packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt --packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt --packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt --packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLogger.kt --packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt --packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt --packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt --packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt --packages/SystemUI/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolver.kt --packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt --packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt --packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt --packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEventLogger.kt --packages/SystemUI/src/com/android/systemui/power/BatteryStateSnapshot.kt --packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt --packages/SystemUI/src/com/android/systemui/privacy/MediaProjectionPrivacyItemMonitor.kt --packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt --packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt --packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt --packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt --packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt --packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogController.kt --packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt --packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt --packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt --packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt --packages/SystemUI/src/com/android/systemui/qs/AutoAddTracker.kt --packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt --packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt --packages/SystemUI/src/com/android/systemui/qs/QSEvents.kt --packages/SystemUI/src/com/android/systemui/qs/QSExpansionPathInterpolator.kt --packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt --packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt --packages/SystemUI/src/com/android/systemui/qs/QSUtils.kt --packages/SystemUI/src/com/android/systemui/qs/SideLabelTileLayout.kt --packages/SystemUI/src/com/android/systemui/qs/VisibilityChangedDispatcher.kt --packages/SystemUI/src/com/android/systemui/qs/carrier/CellSignalState.kt --packages/SystemUI/src/com/android/systemui/qs/customize/CustomizeTileView.kt --packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt --packages/SystemUI/src/com/android/systemui/qs/external/QSExternalModule.kt --packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt --packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialogEventLogger.kt --packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt --packages/SystemUI/src/com/android/systemui/qs/tileimpl/HeightOverrideable.kt --packages/SystemUI/src/com/android/systemui/qs/tileimpl/IgnorableChildLinearLayout.kt --packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt --packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt --packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt --packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogFactory.kt --packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt --packages/SystemUI/src/com/android/systemui/screenshot/ImageCaptureImpl.kt --packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt --packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt --packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt --packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt --packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseDialog.kt --packages/SystemUI/src/com/android/systemui/sensorprivacy/SensorUseStartedActivity.kt --packages/SystemUI/src/com/android/systemui/settings/UserContentResolverProvider.kt --packages/SystemUI/src/com/android/systemui/settings/UserContextProvider.kt --packages/SystemUI/src/com/android/systemui/settings/UserFileManager.kt --packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt --packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt --packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt --packages/SystemUI/src/com/android/systemui/settings/brightness/MirroredBrightnessController.kt --packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManager.kt --packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt --packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt --packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt --packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt --packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt --packages/SystemUI/src/com/android/systemui/shade/ShadeHeightLogger.kt --packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt --packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt --packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt --packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt --packages/SystemUI/src/com/android/systemui/smartspace/SmartspacePrecondition.kt --packages/SystemUI/src/com/android/systemui/smartspace/SmartspaceTargetFilter.kt --packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt --packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt --packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenAndDreamTargetFilter.kt --packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt --packages/SystemUI/src/com/android/systemui/statusbar/AbstractLockscreenShadeTransitionController.kt --packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt --packages/SystemUI/src/com/android/systemui/statusbar/LockScreenShadeOverScroller.kt --packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt --packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionController.kt --packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt --packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt --packages/SystemUI/src/com/android/systemui/statusbar/NotificationClickNotifier.kt --packages/SystemUI/src/com/android/systemui/statusbar/NotificationInteractionTracker.kt --packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt --packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt --packages/SystemUI/src/com/android/systemui/statusbar/SingleShadeLockScreenOverScroller.kt --packages/SystemUI/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScroller.kt --packages/SystemUI/src/com/android/systemui/statusbar/commandline/CommandRegistry.kt --packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointController.kt --packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityState.kt --packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt --packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt --packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileStatusTrackerFactory.kt --packages/SystemUI/src/com/android/systemui/statusbar/connectivity/SignalCallback.kt --packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiState.kt --packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiStatusTrackerFactory.kt --packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt --packages/SystemUI/src/com/android/systemui/statusbar/dagger/StartCentralSurfacesModule.kt --packages/SystemUI/src/com/android/systemui/statusbar/disableflags/DisableFlagsLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt --packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt --packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt --packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt --packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt --packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt --packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventDetector.kt --packages/SystemUI/src/com/android/systemui/statusbar/gesture/GenericGestureDetector.kt --packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt --packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/gesture/TapGestureDetector.kt --packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/FeedbackIcon.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/ViewGroupFadeHelper.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreImpl.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipelineChoreographer.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineDumper.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/SuppressedAttachState.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolver.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DebugModeCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SmartspaceDedupingCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ViewConfigCoordinator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/dagger/CoordinatorsModule.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/BindEventManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/BindEventManagerImpl.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/NotifSection.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifStabilityManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifEvent.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/MediaContainerController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGroupController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGutsViewListener.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifGutsViewManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifRowController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifShadeEventSource.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewBarn.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProvider.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderExtensions.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RootNodeController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/people/NotificationPersonExtractor.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/people/ViewPipeline.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/row/dagger/RemoteInputViewModule.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCallTemplateViewWrapper.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationPriorityBucket.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt --packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaViewController.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocationPublisher.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarIconBlocklist.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometer.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallFlags.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplyState.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplyViewHolder.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputQuickSettingsDisabler.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateView.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/WalletController.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/WalletControllerImpl.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/RemoteInput.kt --packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/SmartRepliesInflationModule.kt --packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowModule.kt --packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt --packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt --packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt --packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt --packages/SystemUI/src/com/android/systemui/toast/ToastDefaultAnimation.kt --packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt --packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt --packages/SystemUI/src/com/android/systemui/unfold/FoldStateLogger.kt --packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt --packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt --packages/SystemUI/src/com/android/systemui/unfold/UnfoldProgressProvider.kt --packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt --packages/SystemUI/src/com/android/systemui/user/UserSwitcherRootView.kt --packages/SystemUI/src/com/android/systemui/util/AsyncActivityLauncher.kt --packages/SystemUI/src/com/android/systemui/util/ColorUtil.kt --packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt --packages/SystemUI/src/com/android/systemui/util/DelayableMarqueeTextView.kt --packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt --packages/SystemUI/src/com/android/systemui/util/InitializationChecker.kt --packages/SystemUI/src/com/android/systemui/util/LargeScreenUtils.kt --packages/SystemUI/src/com/android/systemui/util/ListenerSet.kt --packages/SystemUI/src/com/android/systemui/util/NeverExactlyLinearLayout.kt --packages/SystemUI/src/com/android/systemui/util/NoRemeasureMotionLayout.kt --packages/SystemUI/src/com/android/systemui/util/PluralMessageFormater.kt --packages/SystemUI/src/com/android/systemui/util/RingerModeTracker.kt --packages/SystemUI/src/com/android/systemui/util/RingerModeTrackerImpl.kt --packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt --packages/SystemUI/src/com/android/systemui/util/SafeMarqueeTextView.kt --packages/SystemUI/src/com/android/systemui/util/SparseArrayUtils.kt --packages/SystemUI/src/com/android/systemui/util/TraceUtils.kt --packages/SystemUI/src/com/android/systemui/util/UserAwareController.kt --packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt --packages/SystemUI/src/com/android/systemui/util/animation/AnimationUtil.kt --packages/SystemUI/src/com/android/systemui/util/animation/MeasurementInput.kt --packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt --packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt --packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt --packages/SystemUI/src/com/android/systemui/util/concurrency/Execution.kt --packages/SystemUI/src/com/android/systemui/util/concurrency/PendingTasksContainer.kt --packages/SystemUI/src/com/android/systemui/util/drawable/DrawableSize.kt --packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt --packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt --packages/SystemUI/src/com/android/systemui/util/kotlin/IpcSerializer.kt --packages/SystemUI/src/com/android/systemui/util/kotlin/nullability.kt --packages/SystemUI/src/com/android/systemui/util/recycler/HorizontalSpacerItemDecoration.kt --packages/SystemUI/src/com/android/systemui/util/view/ViewUtil.kt --packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt --packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt --packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt --packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt --packages/SystemUI/tests/src/com/android/keyguard/BouncerPanelExpansionCalculatorTest.kt --packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt --packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt --packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt --packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt --packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt --packages/SystemUI/tests/src/com/android/keyguard/KeyguardUserSwitcherAnchorTest.kt --packages/SystemUI/tests/src/com/android/keyguard/clock/ClockPaletteTest.kt --packages/SystemUI/tests/src/com/android/keyguard/clock/ViewPreviewerTest.kt --packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/BootCompleteCacheTest.kt --packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt --packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt --packages/SystemUI/tests/src/com/android/systemui/InstanceIdSequenceFake.kt --packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt --packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/animation/TextInterpolatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt --packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt --packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt --packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt --packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt --packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt --packages/SystemUI/tests/src/com/android/systemui/broadcast/ActionReceiverTest.kt --packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt --packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastSenderTest.kt --packages/SystemUI/tests/src/com/android/systemui/broadcast/PendingRemovalStoreTest.kt --packages/SystemUI/tests/src/com/android/systemui/broadcast/UserBroadcastDispatcherTest.kt --packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt --packages/SystemUI/tests/src/com/android/systemui/camera/CameraIntentsTest.kt --packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/CustomIconCacheTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapperTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsTileResourceConfigurationImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/controller/StatefulControlSubscriberTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/management/AllModelTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlViewHolderTest.kt --packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt --packages/SystemUI/tests/src/com/android/systemui/decor/CutoutDecorProviderFactoryTest.kt --packages/SystemUI/tests/src/com/android/systemui/decor/OverlayWindowTest.kt --packages/SystemUI/tests/src/com/android/systemui/decor/PrivacyDotDecorProviderFactoryTest.kt --packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerDecorProviderFactoryTest.kt --packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt --packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt --packages/SystemUI/tests/src/com/android/systemui/dump/DumpsysTableLoggerTest.kt --packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferFreezerTest.kt --packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt --packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt --packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt --packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt --packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt --packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt --packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt --packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt --packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt --packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLoggerTest.kt --packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt --packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitorTest.kt --packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt --packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt --packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogTest.kt --packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/SettingObserverTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/carrier/CellSignalStateTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogEventLoggerTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/TilesStatesTextTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt --packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordDialogTest.kt --packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt --packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt --packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt --packages/SystemUI/tests/src/com/android/systemui/shared/navigationbar/RegionSamplingHelperTest.kt --packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenAndDreamTargetFilterTest.kt --packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/BlurUtilsTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/DragDownHelperTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/SingleShadeLockScreenOverScrollerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateEventTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/CommandRegistryTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/DisableFlagsLoggerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/DisableStateTrackerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/gesture/GenericGestureDetectorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifPipelineChoreographerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/GroupCountCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SmartspaceDedupingCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ViewConfigCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTrackerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtenderTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProviderTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/MediaContainerViewTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FoldStateListenerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryStateNotifierTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputQuickSettingsDisablerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/WalletControllerImplTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/window/StatusBarWindowStateControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateLoggingProviderTest.kt --packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt --packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfigTest.kt --packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt --packages/SystemUI/tests/src/com/android/systemui/unfold/util/FoldableTestUtils.kt --packages/SystemUI/tests/src/com/android/systemui/unfold/util/ScaleAwareUnfoldProgressProviderTest.kt --packages/SystemUI/tests/src/com/android/systemui/unfold/util/TestFoldStateProvider.kt --packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt --packages/SystemUI/tests/src/com/android/systemui/user/UserCreatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/util/FakeSharedPreferencesTest.kt --packages/SystemUI/tests/src/com/android/systemui/util/FloatingContentCoordinatorTest.kt --packages/SystemUI/tests/src/com/android/systemui/util/ListenerSetTest.kt --packages/SystemUI/tests/src/com/android/systemui/util/RingerModeLiveDataTest.kt --packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/util/animation/AnimationUtilTest.kt --packages/SystemUI/tests/src/com/android/systemui/util/drawable/DrawableSizeTest.kt --packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt --packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt --packages/SystemUI/tests/src/com/android/systemui/util/kotlin/SuspendUtilTests.kt --packages/SystemUI/tests/src/com/android/systemui/util/view/ViewUtilTest.kt --packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt --packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt --packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/compat/ScreenSizeFoldProvider.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/compat/SizeScreenStatusProvider.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/config/ResourceUnfoldTransitionConfig.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/UnfoldMain.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateProvider.kt --packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index 6b957114c00b..774b2129497d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -200,6 +200,10 @@ class DividerPresenter implements View.OnTouchListener { } // At this point, a divider is required. + final TaskFragmentContainer primaryContainer = + topSplitContainer.getPrimaryContainer(); + final TaskFragmentContainer secondaryContainer = + topSplitContainer.getSecondaryContainer(); // Create the decor surface if one is not available yet. final SurfaceControl decorSurface = parentInfo.getDecorSurface(); @@ -207,41 +211,44 @@ class DividerPresenter implements View.OnTouchListener { // Clean up when the decor surface is currently unavailable. removeDivider(); // Request to create the decor surface - createOrMoveDecorSurfaceLocked(wct, topSplitContainer.getPrimaryContainer()); + createOrMoveDecorSurfaceLocked(wct, primaryContainer); return; } // Update the decor surface owner if needed. boolean isDraggableExpandType = SplitAttributesHelper.isDraggableExpandType(splitAttributes); - final TaskFragmentContainer decorSurfaceOwnerContainer = isDraggableExpandType - ? topSplitContainer.getSecondaryContainer() - : topSplitContainer.getPrimaryContainer(); + final TaskFragmentContainer decorSurfaceOwnerContainer = + isDraggableExpandType ? secondaryContainer : primaryContainer; if (!Objects.equals( mDecorSurfaceOwner, decorSurfaceOwnerContainer.getTaskFragmentToken())) { createOrMoveDecorSurfaceLocked(wct, decorSurfaceOwnerContainer); } - final boolean isVerticalSplit = isVerticalSplit(topSplitContainer); - final boolean isReversedLayout = isReversedLayout( - topSplitContainer.getCurrentSplitAttributes(), - parentInfo.getConfiguration()); + + final Configuration parentConfiguration = parentInfo.getConfiguration(); + final Rect taskBounds = parentConfiguration.windowConfiguration.getBounds(); + final boolean isVerticalSplit = isVerticalSplit(splitAttributes); + final boolean isReversedLayout = isReversedLayout(splitAttributes, parentConfiguration); + final int dividerWidthPx = getDividerWidthPx(dividerAttributes); updateProperties( new Properties( - parentInfo.getConfiguration(), + parentConfiguration, dividerAttributes, decorSurface, getInitialDividerPosition( - topSplitContainer, isVerticalSplit, isReversedLayout), + primaryContainer, secondaryContainer, taskBounds, + dividerWidthPx, isDraggableExpandType, isVerticalSplit, + isReversedLayout), isVerticalSplit, isReversedLayout, parentInfo.getDisplayId(), isDraggableExpandType, - getContainerBackgroundColor(topSplitContainer.getPrimaryContainer(), - DEFAULT_PRIMARY_VEIL_COLOR), - getContainerBackgroundColor(topSplitContainer.getSecondaryContainer(), - DEFAULT_SECONDARY_VEIL_COLOR) + getContainerBackgroundColor( + primaryContainer, DEFAULT_PRIMARY_VEIL_COLOR), + getContainerBackgroundColor( + secondaryContainer, DEFAULT_SECONDARY_VEIL_COLOR) )); } } @@ -338,32 +345,31 @@ class DividerPresenter implements View.OnTouchListener { @VisibleForTesting static int getInitialDividerPosition( - @NonNull SplitContainer splitContainer, + @NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer, + @NonNull Rect taskBounds, + int dividerWidthPx, + boolean isDraggableExpandType, boolean isVerticalSplit, boolean isReversedLayout) { - final Rect primaryBounds = - splitContainer.getPrimaryContainer().getLastRequestedBounds(); - final Rect secondaryBounds = - splitContainer.getSecondaryContainer().getLastRequestedBounds(); - final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes(); - - if (SplitAttributesHelper.isDraggableExpandType(splitAttributes)) { - // If the container is fully expanded by dragging the divider, we display the divider - // on the edge. - final int dividerWidth = getDividerWidthPx(splitAttributes.getDividerAttributes()); + if (isDraggableExpandType) { + // If the secondary container is fully expanded by dragging the divider, we display the + // divider on the edge. final int fullyExpandedPosition = isVerticalSplit - ? primaryBounds.right - dividerWidth - : primaryBounds.bottom - dividerWidth; + ? taskBounds.width() - dividerWidthPx + : taskBounds.height() - dividerWidthPx; return isReversedLayout ? fullyExpandedPosition : 0; } else { + final Rect primaryBounds = primaryContainer.getLastRequestedBounds(); + final Rect secondaryBounds = secondaryContainer.getLastRequestedBounds(); return isVerticalSplit ? Math.min(primaryBounds.right, secondaryBounds.right) : Math.min(primaryBounds.bottom, secondaryBounds.bottom); } } - private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) { - final int layoutDirection = splitContainer.getCurrentSplitAttributes().getLayoutDirection(); + private static boolean isVerticalSplit(@NonNull SplitAttributes splitAttributes) { + final int layoutDirection = splitAttributes.getLayoutDirection(); switch (layoutDirection) { case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: @@ -510,7 +516,7 @@ class DividerPresenter implements View.OnTouchListener { if (mProperties != null && mRenderer != null) { final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); mDividerPosition = calculateDividerPosition( - event, taskBounds, mRenderer.mDividerWidthPx, + event, taskBounds, mProperties.mDividerWidthPx, mProperties.mDividerAttributes, mProperties.mIsVerticalSplit, calculateMinPosition(), calculateMaxPosition()); mRenderer.setDividerPosition(mDividerPosition); @@ -676,8 +682,8 @@ class DividerPresenter implements View.OnTouchListener { final int minPosition = calculateMinPosition(); final int maxPosition = calculateMaxPosition(); final int fullyExpandedPosition = mProperties.mIsVerticalSplit - ? taskBounds.right - mRenderer.mDividerWidthPx - : taskBounds.bottom - mRenderer.mDividerWidthPx; + ? taskBounds.width() - mProperties.mDividerWidthPx + : taskBounds.height() - mProperties.mDividerWidthPx; if (isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)) { final float displayDensity = getDisplayDensity(); @@ -782,7 +788,7 @@ class DividerPresenter implements View.OnTouchListener { private int calculateMinPosition() { return calculateMinPosition( mProperties.mConfiguration.windowConfiguration.getBounds(), - mRenderer.mDividerWidthPx, mProperties.mDividerAttributes, + mProperties.mDividerWidthPx, mProperties.mDividerAttributes, mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); } @@ -790,7 +796,7 @@ class DividerPresenter implements View.OnTouchListener { private int calculateMaxPosition() { return calculateMaxPosition( mProperties.mConfiguration.windowConfiguration.getBounds(), - mRenderer.mDividerWidthPx, mProperties.mDividerAttributes, + mProperties.mDividerWidthPx, mProperties.mDividerAttributes, mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); } @@ -828,13 +834,12 @@ class DividerPresenter implements View.OnTouchListener { * Returns the new split ratio of the {@link SplitContainer} based on the current divider * position. */ - float calculateNewSplitRatio(@NonNull SplitContainer topSplitContainer) { + float calculateNewSplitRatio() { synchronized (mLock) { return calculateNewSplitRatio( - topSplitContainer, mDividerPosition, mProperties.mConfiguration.windowConfiguration.getBounds(), - mRenderer.mDividerWidthPx, + mProperties.mDividerWidthPx, mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout, calculateMinPosition(), @@ -846,21 +851,20 @@ class DividerPresenter implements View.OnTouchListener { private static boolean isDraggingToFullscreenAllowed( @NonNull DividerAttributes dividerAttributes) { // TODO(b/293654166) Use DividerAttributes.isDraggingToFullscreenAllowed when extension is - // updated. - return true; + // updated to v7. + return false; } /** * Returns the new split ratio of the {@link SplitContainer} based on the current divider * position. * - * @param topSplitContainer the {@link SplitContainer} for which to compute the split ratio. * @param dividerPosition the divider position. See {@link #mDividerPosition}. * @param taskBounds the task bounds * @param dividerWidthPx the width of the divider in pixels. * @param isVerticalSplit if {@code true}, the split is a vertical split. If {@code false}, the * split is a horizontal split. See - * {@link #isVerticalSplit(SplitContainer)}. + * {@link #isVerticalSplit(SplitAttributes)}. * @param isReversedLayout if {@code true}, the split layout is reversed, i.e. right-to-left or * bottom-to-top. If {@code false}, the split is not reversed, i.e. * left-to-right or top-to-bottom. See @@ -871,7 +875,6 @@ class DividerPresenter implements View.OnTouchListener { */ @VisibleForTesting static float calculateNewSplitRatio( - @NonNull SplitContainer topSplitContainer, int dividerPosition, @NonNull Rect taskBounds, int dividerWidthPx, @@ -896,8 +899,6 @@ class DividerPresenter implements View.OnTouchListener { dividerPosition = Math.clamp(dividerPosition, minPosition, maxPosition); } - final TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer(); - final Rect origPrimaryBounds = primaryContainer.getLastRequestedBounds(); final int usableSize = isVerticalSplit ? taskBounds.width() - dividerWidthPx : taskBounds.height() - dividerWidthPx; @@ -905,13 +906,13 @@ class DividerPresenter implements View.OnTouchListener { final float newRatio; if (isVerticalSplit) { final int newPrimaryWidth = isReversedLayout - ? (origPrimaryBounds.right - (dividerPosition + dividerWidthPx)) - : (dividerPosition - origPrimaryBounds.left); + ? taskBounds.width() - (dividerPosition + dividerWidthPx) + : dividerPosition; newRatio = 1.0f * newPrimaryWidth / usableSize; } else { final int newPrimaryHeight = isReversedLayout - ? (origPrimaryBounds.bottom - (dividerPosition + dividerWidthPx)) - : (dividerPosition - origPrimaryBounds.top); + ? taskBounds.height() - (dividerPosition + dividerWidthPx) + : dividerPosition; newRatio = 1.0f * newPrimaryHeight / usableSize; } return newRatio; @@ -966,6 +967,7 @@ class DividerPresenter implements View.OnTouchListener { private final boolean mIsDraggableExpandType; private final Color mPrimaryVeilColor; private final Color mSecondaryVeilColor; + private final int mDividerWidthPx; @VisibleForTesting Properties( @@ -989,6 +991,7 @@ class DividerPresenter implements View.OnTouchListener { mIsDraggableExpandType = isDraggableExpandType; mPrimaryVeilColor = primaryVeilColor; mSecondaryVeilColor = secondaryVeilColor; + mDividerWidthPx = getDividerWidthPx(dividerAttributes); } /** @@ -1055,7 +1058,6 @@ class DividerPresenter implements View.OnTouchListener { private final View.OnTouchListener mListener; @NonNull private Properties mProperties; - private int mDividerWidthPx; private int mHandleWidthPx; @Nullable private SurfaceControl mPrimaryVeil; @@ -1095,7 +1097,6 @@ class DividerPresenter implements View.OnTouchListener { /** Updates the divider when initializing or when properties are changed */ @VisibleForTesting void update() { - mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes); mDividerPosition = mProperties.mInitialDividerPosition; mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration); @@ -1161,15 +1162,17 @@ class DividerPresenter implements View.OnTouchListener { // When the divider drag handle width is larger than the divider width, the position // of the divider surface is adjusted so that it is large enough to host both the // divider line and the divider drag handle. - mDividerSurfaceWidthPx = Math.max(mDividerWidthPx, mHandleWidthPx); + mDividerSurfaceWidthPx = Math.max(mProperties.mDividerWidthPx, mHandleWidthPx); + dividerSurfacePosition = mProperties.mIsReversedLayout + ? mDividerPosition + : mDividerPosition + mProperties.mDividerWidthPx - mDividerSurfaceWidthPx; dividerSurfacePosition = - mProperties.mIsReversedLayout - ? mDividerPosition - : mDividerPosition + mDividerWidthPx - mDividerSurfaceWidthPx; - dividerSurfacePosition = Math.clamp(dividerSurfacePosition, 0, - mProperties.mIsVerticalSplit ? taskBounds.width() : taskBounds.height()); + Math.clamp(dividerSurfacePosition, 0, + mProperties.mIsVerticalSplit + ? taskBounds.width() - mDividerSurfaceWidthPx + : taskBounds.height() - mDividerSurfaceWidthPx); } else { - mDividerSurfaceWidthPx = mDividerWidthPx; + mDividerSurfaceWidthPx = mProperties.mDividerWidthPx; dividerSurfacePosition = mDividerPosition; } @@ -1182,16 +1185,9 @@ class DividerPresenter implements View.OnTouchListener { } // Update divider line position in the surface - if (!mProperties.mIsReversedLayout) { - final int offset = mDividerPosition - dividerSurfacePosition; - mDividerLine.setX(mProperties.mIsVerticalSplit ? offset : 0); - mDividerLine.setY(mProperties.mIsVerticalSplit ? 0 : offset); - } else { - // For reversed layout, the divider line is always at the start of the divider - // surface. - mDividerLine.setX(0); - mDividerLine.setY(0); - } + final int offset = mDividerPosition - dividerSurfacePosition; + mDividerLine.setX(mProperties.mIsVerticalSplit ? offset : 0); + mDividerLine.setY(mProperties.mIsVerticalSplit ? 0 : offset); if (mIsDragging) { updateVeils(t); @@ -1241,8 +1237,10 @@ class DividerPresenter implements View.OnTouchListener { final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); mDividerLine.setLayoutParams( mProperties.mIsVerticalSplit - ? new FrameLayout.LayoutParams(mDividerWidthPx, taskBounds.height()) - : new FrameLayout.LayoutParams(taskBounds.width(), mDividerWidthPx) + ? new FrameLayout.LayoutParams( + mProperties.mDividerWidthPx, taskBounds.height()) + : new FrameLayout.LayoutParams( + taskBounds.width(), mProperties.mDividerWidthPx) ); if (mProperties.mDividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { @@ -1352,13 +1350,14 @@ class DividerPresenter implements View.OnTouchListener { Rect secondaryBounds; if (mProperties.mIsVerticalSplit) { final Rect boundsLeft = new Rect(0, 0, mDividerPosition, taskBounds.height()); - final Rect boundsRight = new Rect(mDividerPosition + mDividerWidthPx, 0, + final Rect boundsRight = new Rect(mDividerPosition + mProperties.mDividerWidthPx, 0, taskBounds.width(), taskBounds.height()); primaryBounds = mProperties.mIsReversedLayout ? boundsRight : boundsLeft; secondaryBounds = mProperties.mIsReversedLayout ? boundsLeft : boundsRight; } else { final Rect boundsTop = new Rect(0, 0, taskBounds.width(), mDividerPosition); - final Rect boundsBottom = new Rect(0, mDividerPosition + mDividerWidthPx, + final Rect boundsBottom = new Rect( + 0, mDividerPosition + mProperties.mDividerWidthPx, taskBounds.width(), taskBounds.height()); primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop; secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index c708da97d908..ee00c4cd67eb 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -510,7 +510,7 @@ class TaskContainer { return; } final TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer(); - final float newRatio = dividerPresenter.calculateNewSplitRatio(topSplitContainer); + final float newRatio = dividerPresenter.calculateNewSplitRatio(); // If the primary container is fully expanded, we should finish all the associated // secondary containers. diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java index 20626c79714e..4515187f231e 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -144,6 +144,7 @@ public class DividerPresenterTest { new SplitAttributes.Builder() .setDividerAttributes(DEFAULT_DIVIDER_ATTRIBUTES) .build()); + final Rect mockTaskBounds = new Rect(0, 0, 2000, 1000); final TaskFragmentContainer mockPrimaryContainer = createMockTaskFragmentContainer( mPrimaryContainerToken, new Rect(0, 0, 950, 1000)); @@ -158,7 +159,9 @@ public class DividerPresenterTest { DEFAULT_DIVIDER_ATTRIBUTES, mSurfaceControl, getInitialDividerPosition( - mSplitContainer, true /* isVerticalSplit */, false /* isReversedLayout */), + mockPrimaryContainer, mockSecondaryContainer, mockTaskBounds, + 50 /* divideWidthPx */, false /* isDraggableExpandType */, + true /* isVerticalSplit */, false /* isReversedLayout */), true /* isVerticalSplit */, false /* isReversedLayout */, Display.DEFAULT_DISPLAY, @@ -502,7 +505,6 @@ public class DividerPresenterTest { assertEquals( 0.3f, // Primary is 300px after dragging. DividerPresenter.calculateNewSplitRatio( - mSplitContainer, dividerPosition, taskBounds, dividerWidthPx, @@ -518,7 +520,6 @@ public class DividerPresenterTest { assertEquals( DividerPresenter.RATIO_EXPANDED_SECONDARY, DividerPresenter.calculateNewSplitRatio( - mSplitContainer, dividerPosition, taskBounds, dividerWidthPx, @@ -535,7 +536,6 @@ public class DividerPresenterTest { assertEquals( 0.2f, // Adjusted to the minPosition 200 DividerPresenter.calculateNewSplitRatio( - mSplitContainer, dividerPosition, taskBounds, dividerWidthPx, @@ -569,7 +569,6 @@ public class DividerPresenterTest { // After dragging, secondary is [0, 0, 2000, 300]. Primary is [0, 400, 2000, 1100]. 0.7f, DividerPresenter.calculateNewSplitRatio( - mSplitContainer, dividerPosition, taskBounds, dividerWidthPx, @@ -587,7 +586,6 @@ public class DividerPresenterTest { // The primary (bottom) container is expanded DividerPresenter.RATIO_EXPANDED_PRIMARY, DividerPresenter.calculateNewSplitRatio( - mSplitContainer, dividerPosition, taskBounds, dividerWidthPx, @@ -605,7 +603,6 @@ public class DividerPresenterTest { // Adjusted to minPosition 200, so the primary (bottom) container is 800. 0.8f, DividerPresenter.calculateNewSplitRatio( - mSplitContainer, dividerPosition, taskBounds, dividerWidthPx, diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index 6ca6517abbb0..dc022b4afd3b 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -69,7 +69,7 @@ public class TransitionUtil { /** Returns {@code true} if the transition is opening or closing mode. */ public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) { - return isOpeningMode(mode) || mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK; + return isOpeningMode(mode) || isClosingMode(mode); } /** Returns {@code true} if the transition is opening mode. */ @@ -77,6 +77,11 @@ public class TransitionUtil { return mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT; } + /** Returns {@code true} if the transition is closing mode. */ + public static boolean isClosingMode(@TransitionInfo.TransitionMode int mode) { + return mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK; + } + /** Returns {@code true} if the transition has a display change. */ public static boolean hasDisplayChange(@NonNull TransitionInfo info) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { 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 6fcea1fe5560..b52b0d8dee74 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 @@ -489,6 +489,14 @@ public class PipTransition extends PipTransitionController { // activity windowing mode, and set the task bounds to the final bounds wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); wct.setBounds(taskInfo.token, destinationBounds); + // If the animation is only used to apply destination bounds immediately and + // invisibly, then reshow it until the pip is drawn with the bounds. + final PipAnimationController.PipTransitionAnimator<?> animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.getEndValue().equals(0f)) { + tx.addTransactionCommittedListener(mTransitions.getMainExecutor(), + () -> fadeExistingPip(true /* show */)); + } } else { wct.setBounds(taskInfo.token, null /* bounds */); } @@ -1026,6 +1034,7 @@ public class PipTransition extends PipTransitionController { } startTransaction.apply(); + int animationDuration = mEnterExitAnimationDuration; PipAnimationController.PipTransitionAnimator animator; if (enterAnimationType == ANIM_TYPE_BOUNDS) { animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds, @@ -1057,8 +1066,17 @@ public class PipTransition extends PipTransitionController { } } } else if (enterAnimationType == ANIM_TYPE_ALPHA) { + // In case augmentRequest() is unable to apply the entering bounds (e.g. the request + // info only contains display change), keep the animation invisible (alpha 0) and + // duration 0 to apply the destination bounds. The actual fade-in animation will be + // done in onFinishResize() after the bounds are applied. + final boolean fadeInAfterOnFinishResize = rotationDelta != Surface.ROTATION_0 + && mFixedRotationState == FIXED_ROTATION_CALLBACK; animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds, - 0f, 1f); + 0f, fadeInAfterOnFinishResize ? 0f : 1f); + if (fadeInAfterOnFinishResize) { + animationDuration = 0; + } mSurfaceTransactionHelper .crop(finishTransaction, leash, destinationBounds) .round(finishTransaction, leash, true /* applyCornerRadius */); @@ -1068,7 +1086,7 @@ public class PipTransition extends PipTransitionController { mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), currentBounds); animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) - .setDuration(mEnterExitAnimationDuration); + .setDuration(animationDuration); if (rotationDelta != Surface.ROTATION_0 && mFixedRotationState == FIXED_ROTATION_TRANSITION) { // For fixed rotation, the animation destination bounds is in old rotation coordinates. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index 9ce22094d56b..e196254628d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -167,7 +167,7 @@ class ScreenRotationAnimation { t.show(mScreenshotLayer); if (!isCustomRotate()) { mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, - screenshotBuffer.getColorSpace()); + screenshotBuffer.getColorSpace(), mSurfaceControl); } hardwareBuffer.close(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 6224543516fa..6ade81c0f3a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -1591,7 +1591,7 @@ public class Transitions implements RemoteCallable<Transitions>, public void setHomeTransitionListener(IHomeTransitionListener listener) { executeRemoteCallWithTaskPermission(mTransitions, "setHomeTransitionListener", (transitions) -> { - transitions.mHomeTransitionObserver.setHomeTransitionListener(mTransitions, + transitions.mHomeTransitionObserver.setHomeTransitionListener(transitions, listener); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index e85cb6400000..95e0d79c212e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -255,6 +255,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final WindowContainerToken mTaskToken; private final DragPositioningCallback mDragPositioningCallback; private final DragDetector mDragDetector; + private final int mDisplayId; private int mDragPointerId = -1; private boolean mIsDragging; @@ -266,6 +267,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mTaskToken = taskInfo.token; mDragPositioningCallback = dragPositioningCallback; mDragDetector = new DragDetector(this); + mDisplayId = taskInfo.displayId; } @Override @@ -274,7 +276,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (id == R.id.close_window) { mTaskOperations.closeTask(mTaskToken); } else if (id == R.id.back_button) { - mTaskOperations.injectBackKey(); + mTaskOperations.injectBackKey(mDisplayId); } else if (id == R.id.minimize_window) { mTaskOperations.minimizeTask(mTaskToken); } else if (id == R.id.maximize_window) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 74b091fc4964..37cdbb47bfe8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -386,6 +386,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final DragPositioningCallback mDragPositioningCallback; private final DragDetector mDragDetector; private final GestureDetector mGestureDetector; + private final int mDisplayId; /** * Whether to pilfer the next motion event to send cancellations to the windows below. @@ -407,6 +408,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragPositioningCallback = dragPositioningCallback; mDragDetector = new DragDetector(this); mGestureDetector = new GestureDetector(mContext, this); + mDisplayId = taskInfo.displayId; mCloseMaximizeWindowRunnable = () -> { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); if (decoration == null) return; @@ -432,7 +434,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mTaskOperations.closeTask(mTaskToken, wct); } } else if (id == R.id.back_button) { - mTaskOperations.injectBackKey(); + mTaskOperations.injectBackKey(mDisplayId); } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { moveTaskToFront(decoration.mTaskInfo); @@ -581,17 +583,20 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (ev.getAction() == ACTION_HOVER_MOVE && MaximizeMenu.Companion.isMaximizeMenuView(id)) { decoration.onMaximizeMenuHoverMove(id, ev); + mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); } else if (ev.getAction() == ACTION_HOVER_EXIT) { if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { decoration.onMaximizeWindowHoverExit(); - } else if (id == R.id.maximize_window || id == R.id.maximize_menu) { + } else if (id == R.id.maximize_window + || MaximizeMenu.Companion.isMaximizeMenuView(id)) { // Close menu if not hovering over maximize menu or maximize button after a // delay to give user a chance to re-enter view or to move from one maximize // menu view to another. mMainHandler.postDelayed(mCloseMaximizeWindowRunnable, CLOSE_MAXIMIZE_MENU_DELAY_MS); - } else if (MaximizeMenu.Companion.isMaximizeMenuView(id)) { - decoration.onMaximizeMenuHoverExit(id, ev); + if (id != R.id.maximize_window) { + decoration.onMaximizeMenuHoverExit(id, ev); + } } return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt index 78f0ef7af45c..4f049015af99 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt @@ -88,7 +88,7 @@ class MaximizeButtonView( } fun cancelHoverAnimation() { - hoverProgressAnimatorSet.removeAllListeners() + hoverProgressAnimatorSet.childAnimations.forEach { it.removeAllListeners() } hoverProgressAnimatorSet.cancel() progressBar.visibility = View.INVISIBLE } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java deleted file mode 100644 index 93e2a21c6b02..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java +++ /dev/null @@ -1,451 +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.wm.shell.windowdecor; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.ColorRes; -import android.annotation.NonNull; -import android.app.ActivityManager.RunningTaskInfo; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.PixelFormat; -import android.graphics.PointF; -import android.graphics.Rect; -import android.os.Trace; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; -import android.view.View; -import android.view.WindowManager; -import android.view.WindowlessWindowManager; -import android.widget.ImageView; -import android.window.TaskConstants; - -import com.android.wm.shell.R; -import com.android.wm.shell.common.DisplayController; - -import java.util.function.Supplier; - -/** - * Creates and updates a veil that covers task contents on resize. - */ -public class ResizeVeil { - private static final String TAG = "ResizeVeil"; - private static final int RESIZE_ALPHA_DURATION = 100; - - private static final int VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL; - /** The background is a child of the veil container layer and goes at the bottom. */ - private static final int VEIL_BACKGROUND_LAYER = 0; - /** The icon is a child of the veil container layer and goes in front of the background. */ - private static final int VEIL_ICON_LAYER = 1; - - private final Context mContext; - private final DisplayController mDisplayController; - private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; - private final SurfaceControlBuilderFactory mSurfaceControlBuilderFactory; - private final WindowDecoration.SurfaceControlViewHostFactory mSurfaceControlViewHostFactory; - private final SurfaceSession mSurfaceSession = new SurfaceSession(); - private final Bitmap mAppIcon; - private ImageView mIconView; - private int mIconSize; - private SurfaceControl mParentSurface; - - /** A container surface to host the veil background and icon child surfaces. */ - private SurfaceControl mVeilSurface; - /** A color surface for the veil background. */ - private SurfaceControl mBackgroundSurface; - /** A surface that hosts a windowless window with the app icon. */ - private SurfaceControl mIconSurface; - - private final RunningTaskInfo mTaskInfo; - private SurfaceControlViewHost mViewHost; - private Display mDisplay; - private ValueAnimator mVeilAnimator; - - private boolean mIsShowing = false; - - private final DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener = - new DisplayController.OnDisplaysChangedListener() { - @Override - public void onDisplayAdded(int displayId) { - if (mTaskInfo.displayId != displayId) { - return; - } - mDisplayController.removeDisplayWindowListener(this); - setupResizeVeil(); - } - }; - - public ResizeVeil(Context context, - @NonNull DisplayController displayController, - Bitmap appIcon, RunningTaskInfo taskInfo, - SurfaceControl taskSurface, - Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) { - this(context, - displayController, - appIcon, - taskInfo, - taskSurface, - surfaceControlTransactionSupplier, - new SurfaceControlBuilderFactory() {}, - new WindowDecoration.SurfaceControlViewHostFactory() {}); - } - - public ResizeVeil(Context context, - @NonNull DisplayController displayController, - Bitmap appIcon, RunningTaskInfo taskInfo, - SurfaceControl taskSurface, - Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, - SurfaceControlBuilderFactory surfaceControlBuilderFactory, - WindowDecoration.SurfaceControlViewHostFactory surfaceControlViewHostFactory) { - mContext = context; - mDisplayController = displayController; - mAppIcon = appIcon; - mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; - mTaskInfo = taskInfo; - mParentSurface = taskSurface; - mSurfaceControlBuilderFactory = surfaceControlBuilderFactory; - mSurfaceControlViewHostFactory = surfaceControlViewHostFactory; - setupResizeVeil(); - } - /** - * Create the veil in its default invisible state. - */ - private void setupResizeVeil() { - if (!obtainDisplayOrRegisterListener()) { - // Display may not be available yet, skip this until then. - return; - } - Trace.beginSection("ResizeVeil#setupResizeVeil"); - mVeilSurface = mSurfaceControlBuilderFactory - .create("Resize veil of Task=" + mTaskInfo.taskId) - .setContainerLayer() - .setHidden(true) - .setParent(mParentSurface) - .setCallsite("ResizeVeil#setupResizeVeil") - .build(); - mBackgroundSurface = mSurfaceControlBuilderFactory - .create("Resize veil background of Task=" + mTaskInfo.taskId, mSurfaceSession) - .setColorLayer() - .setHidden(true) - .setParent(mVeilSurface) - .setCallsite("ResizeVeil#setupResizeVeil") - .build(); - mIconSurface = mSurfaceControlBuilderFactory - .create("Resize veil icon of Task=" + mTaskInfo.taskId) - .setContainerLayer() - .setHidden(true) - .setParent(mVeilSurface) - .setCallsite("ResizeVeil#setupResizeVeil") - .build(); - - mIconSize = mContext.getResources() - .getDimensionPixelSize(R.dimen.desktop_mode_resize_veil_icon_size); - final View root = LayoutInflater.from(mContext) - .inflate(R.layout.desktop_mode_resize_veil, null /* root */); - mIconView = root.findViewById(R.id.veil_application_icon); - mIconView.setImageBitmap(mAppIcon); - - final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams( - mIconSize, - mIconSize, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSPARENT); - lp.setTitle("Resize veil icon window of Task=" + mTaskInfo.taskId); - lp.setTrustedOverlay(); - - final WindowlessWindowManager wwm = new WindowlessWindowManager(mTaskInfo.configuration, - mIconSurface, null /* hostInputToken */); - - mViewHost = mSurfaceControlViewHostFactory.create(mContext, mDisplay, wwm, "ResizeVeil"); - mViewHost.setView(root, lp); - Trace.endSection(); - } - - private boolean obtainDisplayOrRegisterListener() { - mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); - if (mDisplay == null) { - mDisplayController.addDisplayWindowListener(mOnDisplaysChangedListener); - return false; - } - return true; - } - - /** - * Shows the veil surface/view. - * - * @param t the transaction to apply in sync with the veil draw - * @param parentSurface the surface that the veil should be a child of - * @param taskBounds the bounds of the task that owns the veil - * @param fadeIn if true, the veil will fade-in with an animation, if false, it will be shown - * immediately - */ - public void showVeil(SurfaceControl.Transaction t, SurfaceControl parentSurface, - Rect taskBounds, boolean fadeIn) { - if (!isReady() || isVisible()) { - t.apply(); - return; - } - mIsShowing = true; - - // Parent surface can change, ensure it is up to date. - if (!parentSurface.equals(mParentSurface)) { - t.reparent(mVeilSurface, parentSurface); - mParentSurface = parentSurface; - } - - t.show(mVeilSurface); - t.setLayer(mVeilSurface, VEIL_CONTAINER_LAYER); - t.setLayer(mIconSurface, VEIL_ICON_LAYER); - t.setLayer(mBackgroundSurface, VEIL_BACKGROUND_LAYER); - t.setColor(mBackgroundSurface, - Color.valueOf(mContext.getColor(getBackgroundColorId())).getComponents()); - - relayout(taskBounds, t); - if (fadeIn) { - cancelAnimation(); - final SurfaceControl.Transaction veilAnimT = mSurfaceControlTransactionSupplier.get(); - mVeilAnimator = new ValueAnimator(); - mVeilAnimator.setFloatValues(0f, 1f); - mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); - mVeilAnimator.addUpdateListener(animation -> { - veilAnimT.setAlpha(mBackgroundSurface, mVeilAnimator.getAnimatedFraction()); - veilAnimT.apply(); - }); - mVeilAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - veilAnimT.show(mBackgroundSurface) - .setAlpha(mBackgroundSurface, 0) - .apply(); - } - - @Override - public void onAnimationEnd(Animator animation) { - veilAnimT.setAlpha(mBackgroundSurface, 1).apply(); - } - }); - - final SurfaceControl.Transaction iconAnimT = mSurfaceControlTransactionSupplier.get(); - final ValueAnimator iconAnimator = new ValueAnimator(); - iconAnimator.setFloatValues(0f, 1f); - iconAnimator.setDuration(RESIZE_ALPHA_DURATION); - iconAnimator.addUpdateListener(animation -> { - iconAnimT.setAlpha(mIconSurface, animation.getAnimatedFraction()); - iconAnimT.apply(); - }); - iconAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - iconAnimT.show(mIconSurface) - .setAlpha(mIconSurface, 0) - .apply(); - } - - @Override - public void onAnimationEnd(Animator animation) { - iconAnimT.setAlpha(mIconSurface, 1).apply(); - } - }); - // Let the animators show it with the correct alpha value once the animation starts. - t.hide(mIconSurface); - t.hide(mBackgroundSurface); - t.apply(); - - mVeilAnimator.start(); - iconAnimator.start(); - } else { - // Show the veil immediately. - t.show(mIconSurface); - t.show(mBackgroundSurface); - t.setAlpha(mIconSurface, 1); - t.setAlpha(mBackgroundSurface, 1); - t.apply(); - } - } - - /** - * Animate veil's alpha to 1, fading it in. - */ - public void showVeil(SurfaceControl parentSurface, Rect taskBounds) { - if (!isReady() || isVisible()) { - return; - } - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - showVeil(t, parentSurface, taskBounds, true /* fadeIn */); - } - - /** - * Update veil bounds to match bounds changes. - * @param newBounds bounds to update veil to. - */ - private void relayout(Rect newBounds, SurfaceControl.Transaction t) { - t.setWindowCrop(mVeilSurface, newBounds.width(), newBounds.height()); - final PointF iconPosition = calculateAppIconPosition(newBounds); - t.setPosition(mIconSurface, iconPosition.x, iconPosition.y); - t.setPosition(mParentSurface, newBounds.left, newBounds.top); - t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height()); - } - - /** - * Calls relayout to update task and veil bounds. - * @param newBounds bounds to update veil to. - */ - public void updateResizeVeil(Rect newBounds) { - if (!isVisible()) { - return; - } - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - updateResizeVeil(t, newBounds); - } - - /** - * Calls relayout to update task and veil bounds. - * Finishes veil fade in if animation is currently running; this is to prevent empty space - * being visible behind the transparent veil during a fast resize. - * - * @param t a transaction to be applied in sync with the veil draw. - * @param newBounds bounds to update veil to. - */ - public void updateResizeVeil(SurfaceControl.Transaction t, Rect newBounds) { - if (!isVisible()) { - t.apply(); - return; - } - if (mVeilAnimator != null && mVeilAnimator.isStarted()) { - mVeilAnimator.removeAllUpdateListeners(); - mVeilAnimator.end(); - } - relayout(newBounds, t); - t.apply(); - } - - /** - * Animate veil's alpha to 0, fading it out. - */ - public void hideVeil() { - if (!isVisible()) { - return; - } - cancelAnimation(); - mVeilAnimator = new ValueAnimator(); - mVeilAnimator.setFloatValues(1, 0); - mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); - mVeilAnimator.addUpdateListener(animation -> { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.setAlpha(mBackgroundSurface, 1 - mVeilAnimator.getAnimatedFraction()); - t.setAlpha(mIconSurface, 1 - mVeilAnimator.getAnimatedFraction()); - t.apply(); - }); - mVeilAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.hide(mBackgroundSurface); - t.hide(mIconSurface); - t.apply(); - } - }); - mVeilAnimator.start(); - mIsShowing = false; - } - - @ColorRes - private int getBackgroundColorId() { - Configuration configuration = mContext.getResources().getConfiguration(); - if ((configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK) - == Configuration.UI_MODE_NIGHT_YES) { - return R.color.desktop_mode_resize_veil_dark; - } else { - return R.color.desktop_mode_resize_veil_light; - } - } - - private PointF calculateAppIconPosition(Rect parentBounds) { - return new PointF((float) parentBounds.width() / 2 - (float) mIconSize / 2, - (float) parentBounds.height() / 2 - (float) mIconSize / 2); - } - - private void cancelAnimation() { - if (mVeilAnimator != null) { - mVeilAnimator.removeAllUpdateListeners(); - mVeilAnimator.cancel(); - } - } - - /** - * Whether the resize veil is currently visible. - * - * Note: when animating a {@link ResizeVeil#hideVeil()}, the veil is considered visible as soon - * as the animation starts. - */ - private boolean isVisible() { - return mIsShowing; - } - - /** Whether the resize veil is ready to be shown. */ - private boolean isReady() { - return mViewHost != null; - } - - /** - * Dispose of veil when it is no longer needed, likely on close of its container decor. - */ - void dispose() { - cancelAnimation(); - mIsShowing = false; - mVeilAnimator = null; - - if (mViewHost != null) { - mViewHost.release(); - mViewHost = null; - } - final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - if (mBackgroundSurface != null) { - t.remove(mBackgroundSurface); - mBackgroundSurface = null; - } - if (mIconSurface != null) { - t.remove(mIconSurface); - mIconSurface = null; - } - if (mVeilSurface != null) { - t.remove(mVeilSurface); - mVeilSurface = null; - } - t.apply(); - mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener); - } - - interface SurfaceControlBuilderFactory { - default SurfaceControl.Builder create(@NonNull String name) { - return new SurfaceControl.Builder().setName(name); - } - default SurfaceControl.Builder create(@NonNull String name, - @NonNull SurfaceSession surfaceSession) { - return new SurfaceControl.Builder(surfaceSession).setName(name); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt new file mode 100644 index 000000000000..4f2d945e49f9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.PixelFormat +import android.graphics.PointF +import android.graphics.Rect +import android.os.Trace +import android.view.Display +import android.view.LayoutInflater +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.SurfaceSession +import android.view.WindowManager +import android.view.WindowlessWindowManager +import android.widget.ImageView +import android.window.TaskConstants +import com.android.wm.shell.R +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener +import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory +import java.util.function.Supplier + +/** + * Creates and updates a veil that covers task contents on resize. + */ +class ResizeVeil @JvmOverloads constructor( + private val context: Context, + private val displayController: DisplayController, + private val appIcon: Bitmap, + private val taskInfo: RunningTaskInfo, + private var parentSurface: SurfaceControl, + private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>, + private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory = + object : SurfaceControlBuilderFactory {}, + private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = + object : SurfaceControlViewHostFactory {} +) { + private val surfaceSession = SurfaceSession() + private lateinit var iconView: ImageView + private var iconSize = 0 + + /** A container surface to host the veil background and icon child surfaces. */ + private var veilSurface: SurfaceControl? = null + /** A color surface for the veil background. */ + private var backgroundSurface: SurfaceControl? = null + /** A surface that hosts a windowless window with the app icon. */ + private var iconSurface: SurfaceControl? = null + private var viewHost: SurfaceControlViewHost? = null + private var display: Display? = null + private var veilAnimator: ValueAnimator? = null + + /** + * Whether the resize veil is currently visible. + * + * Note: when animating a [ResizeVeil.hideVeil], the veil is considered visible as soon + * as the animation starts. + */ + private var isVisible = false + + private val onDisplaysChangedListener: OnDisplaysChangedListener = + object : OnDisplaysChangedListener { + override fun onDisplayAdded(displayId: Int) { + if (taskInfo.displayId != displayId) { + return + } + displayController.removeDisplayWindowListener(this) + setupResizeVeil() + } + } + + private val backgroundColorId: Int + get() { + val configuration = context.resources.configuration + return if (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + == Configuration.UI_MODE_NIGHT_YES) { + R.color.desktop_mode_resize_veil_dark + } else { + R.color.desktop_mode_resize_veil_light + } + } + + /** + * Whether the resize veil is ready to be shown. + */ + private val isReady: Boolean + get() = viewHost != null + + init { + setupResizeVeil() + } + + /** + * Create the veil in its default invisible state. + */ + private fun setupResizeVeil() { + if (!obtainDisplayOrRegisterListener()) { + // Display may not be available yet, skip this until then. + return + } + Trace.beginSection("ResizeVeil#setupResizeVeil") + veilSurface = surfaceControlBuilderFactory + .create("Resize veil of Task=" + taskInfo.taskId) + .setContainerLayer() + .setHidden(true) + .setParent(parentSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build() + backgroundSurface = surfaceControlBuilderFactory + .create("Resize veil background of Task=" + taskInfo.taskId, surfaceSession) + .setColorLayer() + .setHidden(true) + .setParent(veilSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build() + iconSurface = surfaceControlBuilderFactory + .create("Resize veil icon of Task=" + taskInfo.taskId) + .setContainerLayer() + .setHidden(true) + .setParent(veilSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build() + iconSize = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_resize_veil_icon_size) + val root = LayoutInflater.from(context) + .inflate(R.layout.desktop_mode_resize_veil, null /* root */) + iconView = root.requireViewById(R.id.veil_application_icon) + iconView.setImageBitmap(appIcon) + val lp = WindowManager.LayoutParams( + iconSize, + iconSize, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT) + lp.title = "Resize veil icon window of Task=" + taskInfo.taskId + lp.setTrustedOverlay() + val wwm = WindowlessWindowManager(taskInfo.configuration, + iconSurface, null /* hostInputToken */) + viewHost = surfaceControlViewHostFactory.create(context, display, wwm, "ResizeVeil") + viewHost?.setView(root, lp) + Trace.endSection() + } + + private fun obtainDisplayOrRegisterListener(): Boolean { + display = displayController.getDisplay(taskInfo.displayId) + if (display == null) { + displayController.addDisplayWindowListener(onDisplaysChangedListener) + return false + } + return true + } + + /** + * Shows the veil surface/view. + * + * @param t the transaction to apply in sync with the veil draw + * @param parent the surface that the veil should be a child of + * @param taskBounds the bounds of the task that owns the veil + * @param fadeIn if true, the veil will fade-in with an animation, if false, it will be shown + * immediately + */ + fun showVeil( + t: SurfaceControl.Transaction, + parent: SurfaceControl, + taskBounds: Rect, + fadeIn: Boolean + ) { + if (!isReady || isVisible) { + t.apply() + return + } + isVisible = true + val background = backgroundSurface + val icon = iconSurface + val veil = veilSurface + if (background == null || icon == null || veil == null) return + + // Parent surface can change, ensure it is up to date. + if (parent != parentSurface) { + t.reparent(veil, parent) + parentSurface = parent + } + + + t.show(veil) + .setLayer(veil, VEIL_CONTAINER_LAYER) + .setLayer(icon, VEIL_ICON_LAYER) + .setLayer(background, VEIL_BACKGROUND_LAYER) + .setColor(background, + Color.valueOf(context.getColor(backgroundColorId)).components) + relayout(taskBounds, t) + if (fadeIn) { + cancelAnimation() + val veilAnimT = surfaceControlTransactionSupplier.get() + val iconAnimT = surfaceControlTransactionSupplier.get() + veilAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = RESIZE_ALPHA_DURATION + addUpdateListener { + veilAnimT.setAlpha(background, animatedValue as Float) + .apply() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + veilAnimT.show(background) + .setAlpha(background, 0f) + .apply() + } + + override fun onAnimationEnd(animation: Animator) { + veilAnimT.setAlpha(background, 1f).apply() + } + }) + } + val iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = RESIZE_ALPHA_DURATION + addUpdateListener { + iconAnimT.setAlpha(icon, animatedValue as Float) + .apply() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + iconAnimT.show(icon) + .setAlpha(icon, 0f) + .apply() + } + + override fun onAnimationEnd(animation: Animator) { + iconAnimT.setAlpha(icon, 1f).apply() + } + }) + } + + // Let the animators show it with the correct alpha value once the animation starts. + t.hide(icon) + .hide(background) + .apply() + veilAnimator?.start() + iconAnimator.start() + } else { + // Show the veil immediately. + t.show(icon) + .show(background) + .setAlpha(icon, 1f) + .setAlpha(background, 1f) + .apply() + } + } + + /** + * Animate veil's alpha to 1, fading it in. + */ + fun showVeil(parentSurface: SurfaceControl, taskBounds: Rect) { + if (!isReady || isVisible) { + return + } + val t = surfaceControlTransactionSupplier.get() + showVeil(t, parentSurface, taskBounds, true /* fadeIn */) + } + + /** + * Update veil bounds to match bounds changes. + * @param newBounds bounds to update veil to. + */ + private fun relayout(newBounds: Rect, t: SurfaceControl.Transaction) { + val iconPosition = calculateAppIconPosition(newBounds) + val veil = veilSurface + val icon = iconSurface + if (veil == null || icon == null) return + t.setWindowCrop(veil, newBounds.width(), newBounds.height()) + .setPosition(icon, iconPosition.x, iconPosition.y) + .setPosition(parentSurface, newBounds.left.toFloat(), newBounds.top.toFloat()) + .setWindowCrop(parentSurface, newBounds.width(), newBounds.height()) + } + + /** + * Calls relayout to update task and veil bounds. + * @param newBounds bounds to update veil to. + */ + fun updateResizeVeil(newBounds: Rect) { + if (!isVisible) { + return + } + val t = surfaceControlTransactionSupplier.get() + updateResizeVeil(t, newBounds) + } + + /** + * Calls relayout to update task and veil bounds. + * Finishes veil fade in if animation is currently running; this is to prevent empty space + * being visible behind the transparent veil during a fast resize. + * + * @param t a transaction to be applied in sync with the veil draw. + * @param newBounds bounds to update veil to. + */ + fun updateResizeVeil(t: SurfaceControl.Transaction, newBounds: Rect) { + if (!isVisible) { + t.apply() + return + } + veilAnimator?.let { animator -> + if (animator.isStarted) { + animator.removeAllUpdateListeners() + animator.end() + } + } + relayout(newBounds, t) + t.apply() + } + + /** + * Animate veil's alpha to 0, fading it out. + */ + fun hideVeil() { + if (!isVisible) { + return + } + cancelAnimation() + val background = backgroundSurface + val icon = iconSurface + if (background == null || icon == null) return + + veilAnimator = ValueAnimator.ofFloat(1f, 0f).apply { + duration = RESIZE_ALPHA_DURATION + addUpdateListener { + surfaceControlTransactionSupplier.get() + .setAlpha(background, animatedValue as Float) + .setAlpha(icon, animatedValue as Float) + .apply() + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + surfaceControlTransactionSupplier.get() + .hide(background) + .hide(icon) + .apply() + } + }) + } + veilAnimator?.start() + isVisible = false + } + + private fun calculateAppIconPosition(parentBounds: Rect): PointF { + return PointF(parentBounds.width().toFloat() / 2 - iconSize.toFloat() / 2, + parentBounds.height().toFloat() / 2 - iconSize.toFloat() / 2) + } + + private fun cancelAnimation() { + veilAnimator?.removeAllUpdateListeners() + veilAnimator?.cancel() + } + + /** + * Dispose of veil when it is no longer needed, likely on close of its container decor. + */ + fun dispose() { + cancelAnimation() + veilAnimator = null + isVisible = false + + viewHost?.release() + viewHost = null + + val t: SurfaceControl.Transaction = surfaceControlTransactionSupplier.get() + backgroundSurface?.let { background -> t.remove(background) } + backgroundSurface = null + iconSurface?.let { icon -> t.remove(icon) } + iconSurface = null + veilSurface?.let { veil -> t.remove(veil) } + veilSurface = null + t.apply() + displayController.removeDisplayWindowListener(onDisplaysChangedListener) + } + + interface SurfaceControlBuilderFactory { + fun create(name: String): SurfaceControl.Builder { + return SurfaceControl.Builder().setName(name) + } + + fun create(name: String, surfaceSession: SurfaceSession): SurfaceControl.Builder { + return SurfaceControl.Builder(surfaceSession).setName(name) + } + } + + companion object { + private const val TAG = "ResizeVeil" + private const val RESIZE_ALPHA_DURATION = 100L + private const val VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL + + /** The background is a child of the veil container layer and goes at the bottom. */ + private const val VEIL_BACKGROUND_LAYER = 0 + + /** The icon is a child of the veil container layer and goes in front of the background. */ + private const val VEIL_ICON_LAYER = 1 + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java index 53d4e2701849..ad238c35dd83 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java @@ -52,19 +52,19 @@ class TaskOperations { mSyncQueue = syncQueue; } - void injectBackKey() { - sendBackEvent(KeyEvent.ACTION_DOWN); - sendBackEvent(KeyEvent.ACTION_UP); + void injectBackKey(int displayId) { + sendBackEvent(KeyEvent.ACTION_DOWN, displayId); + sendBackEvent(KeyEvent.ACTION_UP, displayId); } - private void sendBackEvent(int action) { + private void sendBackEvent(int action, int displayId) { final long when = SystemClock.uptimeMillis(); final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, InputDevice.SOURCE_KEYBOARD); - ev.setDisplayId(mContext.getDisplay().getDisplayId()); + ev.setDisplayId(displayId); if (!mContext.getSystemService(InputManager.class) .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) { Log.e(TAG, "Inject input event fail"); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 0dc512835d65..2ae3cb9ef3c0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -51,6 +51,7 @@ import android.window.TaskConstants; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.shared.DesktopModeStatus; @@ -687,7 +688,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } } - interface SurfaceControlViewHostFactory { + @VisibleForTesting + public interface SurfaceControlViewHostFactory { default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { return new SurfaceControlViewHost(c, d, wmm, "WindowDecoration"); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt index ec204714c341..7ade9876d28a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt @@ -23,13 +23,13 @@ import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BA val TaskInfo.isTransparentCaptionBarAppearance: Boolean get() { - val appearance = taskDescription?.systemBarsAppearance ?: 0 + val appearance = taskDescription?.topOpaqueSystemBarsAppearance ?: 0 return (appearance and APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND) != 0 } val TaskInfo.isLightCaptionBarAppearance: Boolean get() { - val appearance = taskDescription?.systemBarsAppearance ?: 0 + val appearance = taskDescription?.topOpaqueSystemBarsAppearance ?: 0 return (appearance and APPEARANCE_LIGHT_CAPTION_BARS) != 0 } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt index 285e5b6a04a5..51b291c0b7a4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt @@ -39,7 +39,7 @@ import org.junit.runner.RunWith class DesktopModeUiEventLoggerTest : ShellTestCase() { private lateinit var uiEventLoggerFake: UiEventLoggerFake private lateinit var logger: DesktopModeUiEventLogger - private val instanceIdSequence = InstanceIdSequence(10) + private val instanceIdSequence = InstanceIdSequence(/* instanceIdMax */ 1 shl 20) @Before diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index aa2cee79fcfc..9c1dc22bcef2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -26,6 +26,7 @@ import android.content.Context import android.graphics.Rect import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay +import android.hardware.input.InputManager import android.os.Handler import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsEnabled @@ -42,8 +43,10 @@ import android.view.InputChannel import android.view.InputMonitor import android.view.InsetsSource import android.view.InsetsState +import android.view.KeyEvent import android.view.SurfaceControl import android.view.SurfaceView +import android.view.View import android.view.WindowInsets.Type.navigationBars import android.view.WindowInsets.Type.statusBars import androidx.test.filters.SmallTest @@ -51,6 +54,7 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.window.flags.Flags +import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase @@ -61,6 +65,7 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopTasksController +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.DesktopModeStatus import com.android.wm.shell.sysui.KeyguardChangeListener import com.android.wm.shell.sysui.ShellCommandHandler @@ -70,6 +75,7 @@ import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener import java.util.Optional import java.util.function.Supplier +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test @@ -279,6 +285,41 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + fun testBackEventHasRightDisplayId() { + val secondaryDisplay = createVirtualDisplay() ?: return + val secondaryDisplayId = secondaryDisplay.display.displayId + val task = createTask( + displayId = secondaryDisplayId, + windowingMode = WINDOWING_MODE_FREEFORM + ) + val windowDecor = setUpMockDecorationForTask(task) + + onTaskOpening(task) + val onClickListenerCaptor = argumentCaptor<View.OnClickListener>() + verify(windowDecor).setCaptionListeners( + onClickListenerCaptor.capture(), any(), any(), any()) + + val onClickListener = onClickListenerCaptor.firstValue + val view = mock(View::class.java) + whenever(view.id).thenReturn(R.id.back_button) + + val inputManager = mock(InputManager::class.java) + mContext.addMockSystemService(InputManager::class.java, inputManager) + + val freeformTaskTransitionStarter = mock(FreeformTaskTransitionStarter::class.java) + desktopModeWindowDecorViewModel + .setFreeformTaskTransitionStarter(freeformTaskTransitionStarter) + + onClickListener.onClick(view) + + val eventCaptor = argumentCaptor<KeyEvent>() + verify(inputManager, times(2)).injectInputEvent(eventCaptor.capture(), anyInt()) + + assertEquals(secondaryDisplayId, eventCaptor.firstValue.displayId) + assertEquals(secondaryDisplayId, eventCaptor.secondValue.displayId) + } + + @Test fun testCaptionIsNotCreatedWhenKeyguardIsVisible() { val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) val keyguardListenerCaptor = argumentCaptor<KeyguardChangeListener>() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 3ca9b57e03fd..a731e5394bdf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -228,7 +228,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); - taskInfo.taskDescription.setSystemBarsAppearance( + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance( APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND); final RelayoutParams relayoutParams = new RelayoutParams(); @@ -246,7 +246,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { public void updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); - taskInfo.taskDescription.setSystemBarsAppearance(0); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0); final RelayoutParams relayoutParams = new RelayoutParams(); DesktopModeWindowDecoration.updateRelayoutParams( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt index 87425915fbf7..5da57c50e6c1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt @@ -33,6 +33,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Spy import org.mockito.kotlin.any @@ -102,6 +104,14 @@ class ResizeVeilTest : ShellTestCase() { .create("Resize veil icon of Task=" + taskInfo.taskId)) .thenReturn(spyIconSurfaceBuilder) doReturn(mockIconSurface).whenever(spyIconSurfaceBuilder).build() + + doReturn(mockTransaction).whenever(mockTransaction).setLayer(any(), anyInt()) + doReturn(mockTransaction).whenever(mockTransaction).setAlpha(any(), anyFloat()) + doReturn(mockTransaction).whenever(mockTransaction).show(any()) + doReturn(mockTransaction).whenever(mockTransaction).hide(any()) + doReturn(mockTransaction).whenever(mockTransaction) + .setPosition(any(), anyFloat(), anyFloat()) + doReturn(mockTransaction).whenever(mockTransaction).setWindowCrop(any(), anyInt(), anyInt()) } @Test @@ -139,52 +149,48 @@ class ResizeVeilTest : ShellTestCase() { @Test fun showVeil() { val veil = createResizeVeil() - val tx = mock<SurfaceControl.Transaction>() - veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) - verify(tx).show(mockResizeVeilSurface) - verify(tx).show(mockBackgroundSurface) - verify(tx).show(mockIconSurface) - verify(tx).apply() + verify(mockTransaction).show(mockResizeVeilSurface) + verify(mockTransaction).show(mockBackgroundSurface) + verify(mockTransaction).show(mockIconSurface) + verify(mockTransaction).apply() } @Test fun showVeil_displayUnavailable_doesNotShow() { val veil = createResizeVeil(withDisplayAvailable = false) - val tx = mock<SurfaceControl.Transaction>() - veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) - verify(tx, never()).show(mockResizeVeilSurface) - verify(tx, never()).show(mockBackgroundSurface) - verify(tx, never()).show(mockIconSurface) - verify(tx).apply() + verify(mockTransaction, never()).show(mockResizeVeilSurface) + verify(mockTransaction, never()).show(mockBackgroundSurface) + verify(mockTransaction, never()).show(mockIconSurface) + verify(mockTransaction).apply() } @Test fun showVeil_alreadyVisible_doesNotShowAgain() { val veil = createResizeVeil() - val tx = mock<SurfaceControl.Transaction>() - veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) - veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */) - verify(tx, times(1)).show(mockResizeVeilSurface) - verify(tx, times(1)).show(mockBackgroundSurface) - verify(tx, times(1)).show(mockIconSurface) - verify(tx, times(2)).apply() + verify(mockTransaction, times(1)).show(mockResizeVeilSurface) + verify(mockTransaction, times(1)).show(mockBackgroundSurface) + verify(mockTransaction, times(1)).show(mockIconSurface) + verify(mockTransaction, times(2)).apply() } @Test fun showVeil_reparentsVeilToNewParent() { val veil = createResizeVeil(parent = mock()) - val tx = mock<SurfaceControl.Transaction>() val newParent = mock<SurfaceControl>() - veil.showVeil(tx, newParent, Rect(0, 0, 100, 100), false /* fadeIn */) + veil.showVeil(mockTransaction, newParent, Rect(0, 0, 100, 100), false /* fadeIn */) - verify(tx).reparent(mockResizeVeilSurface, newParent) + verify(mockTransaction).reparent(mockResizeVeilSurface, newParent) } @Test diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp index f1ee3256dbee..eecc741a3bbb 100644 --- a/libs/input/MouseCursorController.cpp +++ b/libs/input/MouseCursorController.cpp @@ -165,6 +165,15 @@ void MouseCursorController::setStylusHoverMode(bool stylusHoverMode) { } } +void MouseCursorController::setSkipScreenshot(bool skip) { + std::scoped_lock lock(mLock); + if (mLocked.skipScreenshot == skip) { + return; + } + mLocked.skipScreenshot = skip; + updatePointerLocked(); +} + void MouseCursorController::reloadPointerResources(bool getAdditionalMouseResources) { std::scoped_lock lock(mLock); @@ -352,6 +361,7 @@ void MouseCursorController::updatePointerLocked() REQUIRES(mLock) { mLocked.pointerSprite->setLayer(Sprite::BASE_LAYER_POINTER); mLocked.pointerSprite->setPosition(mLocked.pointerX, mLocked.pointerY); mLocked.pointerSprite->setDisplayId(mLocked.viewport.displayId); + mLocked.pointerSprite->setSkipScreenshot(mLocked.skipScreenshot); if (mLocked.pointerAlpha > 0) { mLocked.pointerSprite->setAlpha(mLocked.pointerAlpha); diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h index dc7e8ca16c8a..78f6413ff111 100644 --- a/libs/input/MouseCursorController.h +++ b/libs/input/MouseCursorController.h @@ -53,6 +53,9 @@ public: void setDisplayViewport(const DisplayViewport& viewport, bool getAdditionalMouseResources); void setStylusHoverMode(bool stylusHoverMode); + // Set/Unset flag to hide the mouse cursor on the mirrored display + void setSkipScreenshot(bool skip); + void updatePointerIcon(PointerIconStyle iconId); void setCustomPointerIcon(const SpriteIcon& icon); void reloadPointerResources(bool getAdditionalMouseResources); @@ -94,6 +97,7 @@ private: PointerIconStyle requestedPointerType; PointerIconStyle resolvedPointerType; + bool skipScreenshot{false}; bool animating{false}; } mLocked GUARDED_BY(mLock); diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index cca1b07c3118..11b27a214984 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -286,13 +286,16 @@ void PointerController::setCustomPointerIcon(const SpriteIcon& icon) { mCursorController.setCustomPointerIcon(icon); } -void PointerController::setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) { +void PointerController::setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) { std::scoped_lock lock(getLock()); - if (skip) { - mLocked.displaysToSkipScreenshot.insert(displayId); - } else { - mLocked.displaysToSkipScreenshot.erase(displayId); - } + mLocked.displaysToSkipScreenshot.insert(displayId); + mCursorController.setSkipScreenshot(true); +} + +void PointerController::clearSkipScreenshotFlags() { + std::scoped_lock lock(getLock()); + mLocked.displaysToSkipScreenshot.clear(); + mCursorController.setSkipScreenshot(false); } void PointerController::doInactivityTimeout() { diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index c6430f7f36ff..4d1e1d733cc1 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -66,7 +66,8 @@ public: void clearSpots() override; void updatePointerIcon(PointerIconStyle iconId) override; void setCustomPointerIcon(const SpriteIcon& icon) override; - void setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) override; + void setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) override; + void clearSkipScreenshotFlags() override; virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout); void doInactivityTimeout(); diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index 2dcb1f1d1650..cbef68e2eb8f 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -183,12 +183,16 @@ private: MyLooper() : Looper(false) {} ~MyLooper() = default; }; - sp<MyLooper> mLooper; std::thread mThread; + +protected: + sp<MyLooper> mLooper; }; -PointerControllerTest::PointerControllerTest() : mPointerSprite(new NiceMock<MockSprite>), - mLooper(new MyLooper), mThread(&PointerControllerTest::loopThread, this) { +PointerControllerTest::PointerControllerTest() + : mPointerSprite(new NiceMock<MockSprite>), + mThread(&PointerControllerTest::loopThread, this), + mLooper(new MyLooper) { mSpriteController.reset(new NiceMock<MockSpriteController>(mLooper)); mPolicy = new MockPointerControllerPolicyInterface(); @@ -339,7 +343,7 @@ TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) { testing::Mock::VerifyAndClearExpectations(testSpotSprite.get()); // Marking the display to skip screenshot should update sprite as well - mPointerController->setSkipScreenshot(ui::LogicalDisplayId::DEFAULT, true); + mPointerController->setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId::DEFAULT); EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(true)); // Update spots to sync state with sprite @@ -348,13 +352,53 @@ TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) { testing::Mock::VerifyAndClearExpectations(testSpotSprite.get()); // Reset flag and verify again - mPointerController->setSkipScreenshot(ui::LogicalDisplayId::DEFAULT, false); + mPointerController->clearSkipScreenshotFlags(); EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false)); mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits, ui::LogicalDisplayId::DEFAULT); testing::Mock::VerifyAndClearExpectations(testSpotSprite.get()); } +class PointerControllerSkipScreenshotFlagTest + : public PointerControllerTest, + public testing::WithParamInterface<PointerControllerInterface::ControllerType> {}; + +TEST_P(PointerControllerSkipScreenshotFlagTest, updatesSkipScreenshotFlag) { + sp<MockSprite> testPointerSprite(new NiceMock<MockSprite>); + EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testPointerSprite)); + + // Create a pointer controller + mPointerController = + PointerController::create(mPolicy, mLooper, *mSpriteController, GetParam()); + ensureDisplayViewportIsSet(ui::LogicalDisplayId::DEFAULT); + + // By default skip screenshot flag is not set for the sprite + EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(false)); + + // Update pointer to sync state with sprite + mPointerController->setPosition(100, 100); + testing::Mock::VerifyAndClearExpectations(testPointerSprite.get()); + + // Marking the controller to skip screenshot should update pointer sprite + mPointerController->setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId::DEFAULT); + EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(true)); + + // Update pointer to sync state with sprite + mPointerController->move(10, 10); + testing::Mock::VerifyAndClearExpectations(testPointerSprite.get()); + + // Reset flag and verify again + mPointerController->clearSkipScreenshotFlags(); + EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(false)); + mPointerController->move(10, 10); + testing::Mock::VerifyAndClearExpectations(testPointerSprite.get()); +} + +INSTANTIATE_TEST_SUITE_P(PointerControllerSkipScreenshotFlagTest, + PointerControllerSkipScreenshotFlagTest, + testing::Values(PointerControllerInterface::ControllerType::MOUSE, + PointerControllerInterface::ControllerType::STYLUS)); + class PointerControllerWindowInfoListenerTest : public Test {}; TEST_F(PointerControllerWindowInfoListenerTest, diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 293c561f166c..d148afd3c15d 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -1764,6 +1764,10 @@ public class AudioSystem public static native int getForceUse(int usage); /** @hide */ @UnsupportedAppUsage + public static native int setDeviceAbsoluteVolumeEnabled(int nativeDeviceType, + @NonNull String address, boolean enabled, int streamToDriveAbs); + /** @hide */ + @UnsupportedAppUsage public static native int initStreamVolume(int stream, int indexMin, int indexMax); @UnsupportedAppUsage private static native int setStreamVolumeIndex(int stream, int index, int device); diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java index 70462effaa54..442ccdcddb2b 100644 --- a/media/java/android/media/session/MediaController.java +++ b/media/java/android/media/session/MediaController.java @@ -141,10 +141,9 @@ public final class MediaController { } try { return mSessionBinder.sendMediaButton(mContext.getPackageName(), keyEvent); - } catch (RemoteException e) { - // System is dead. =( + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } - return false; } /** @@ -155,9 +154,8 @@ public final class MediaController { public @Nullable PlaybackState getPlaybackState() { try { return mSessionBinder.getPlaybackState(); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling getPlaybackState.", e); - return null; + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -169,9 +167,8 @@ public final class MediaController { public @Nullable MediaMetadata getMetadata() { try { return mSessionBinder.getMetadata(); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling getMetadata.", e); - return null; + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -185,10 +182,9 @@ public final class MediaController { try { ParceledListSlice list = mSessionBinder.getQueue(); return list == null ? null : list.getList(); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling getQueue.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } - return null; } /** @@ -197,10 +193,9 @@ public final class MediaController { public @Nullable CharSequence getQueueTitle() { try { return mSessionBinder.getQueueTitle(); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling getQueueTitle", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } - return null; } /** @@ -209,10 +204,9 @@ public final class MediaController { public @Nullable Bundle getExtras() { try { return mSessionBinder.getExtras(); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling getExtras", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } - return null; } /** @@ -232,9 +226,8 @@ public final class MediaController { public int getRatingType() { try { return mSessionBinder.getRatingType(); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling getRatingType.", e); - return Rating.RATING_NONE; + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -246,10 +239,9 @@ public final class MediaController { public long getFlags() { try { return mSessionBinder.getFlags(); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling getFlags.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } - return 0; } /** Returns the current playback info for this session. */ @@ -271,10 +263,9 @@ public final class MediaController { public @Nullable PendingIntent getSessionActivity() { try { return mSessionBinder.getLaunchPendingIntent(); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling getPendingIntent.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } - return null; } /** @@ -304,8 +295,8 @@ public final class MediaController { // AppOpsManager usages. mSessionBinder.setVolumeTo(mContext.getPackageName(), mContext.getOpPackageName(), value, flags); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling setVolumeTo.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -329,8 +320,8 @@ public final class MediaController { // AppOpsManager usages. mSessionBinder.adjustVolume(mContext.getPackageName(), mContext.getOpPackageName(), direction, flags); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling adjustVolumeBy.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -395,8 +386,8 @@ public final class MediaController { } try { mSessionBinder.sendCommand(mContext.getPackageName(), command, args, cb); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in sendCommand.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -409,8 +400,8 @@ public final class MediaController { if (mPackageName == null) { try { mPackageName = mSessionBinder.getPackageName(); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in getPackageName.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } return mPackageName; @@ -430,8 +421,8 @@ public final class MediaController { // Get info from the connected session. try { mSessionInfo = mSessionBinder.getSessionInfo(); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in getSessionInfo.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } if (mSessionInfo == null) { @@ -454,8 +445,8 @@ public final class MediaController { if (mTag == null) { try { mTag = mSessionBinder.getTag(); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in getTag.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } return mTag; @@ -485,8 +476,8 @@ public final class MediaController { try { mSessionBinder.registerCallback(mContext.getPackageName(), mCbStub); mCbRegistered = true; - } catch (RemoteException e) { - Log.e(TAG, "Dead object in registerCallback", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } } @@ -504,8 +495,8 @@ public final class MediaController { if (mCbRegistered && mCallbacks.size() == 0) { try { mSessionBinder.unregisterCallback(mCbStub); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in removeCallbackLocked"); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } mCbRegistered = false; } @@ -641,8 +632,8 @@ public final class MediaController { public void prepare() { try { mSessionBinder.prepare(mContext.getPackageName()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling prepare.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -665,8 +656,8 @@ public final class MediaController { } try { mSessionBinder.prepareFromMediaId(mContext.getPackageName(), mediaId, extras); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling prepare(" + mediaId + ").", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -691,8 +682,8 @@ public final class MediaController { } try { mSessionBinder.prepareFromSearch(mContext.getPackageName(), query, extras); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling prepare(" + query + ").", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -715,8 +706,8 @@ public final class MediaController { } try { mSessionBinder.prepareFromUri(mContext.getPackageName(), uri, extras); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling prepare(" + uri + ").", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -726,8 +717,8 @@ public final class MediaController { public void play() { try { mSessionBinder.play(mContext.getPackageName()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling play.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -745,8 +736,8 @@ public final class MediaController { } try { mSessionBinder.playFromMediaId(mContext.getPackageName(), mediaId, extras); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling play(" + mediaId + ").", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -767,8 +758,8 @@ public final class MediaController { } try { mSessionBinder.playFromSearch(mContext.getPackageName(), query, extras); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling play(" + query + ").", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -786,8 +777,8 @@ public final class MediaController { } try { mSessionBinder.playFromUri(mContext.getPackageName(), uri, extras); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling play(" + uri + ").", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -798,8 +789,8 @@ public final class MediaController { public void skipToQueueItem(long id) { try { mSessionBinder.skipToQueueItem(mContext.getPackageName(), id); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -810,8 +801,8 @@ public final class MediaController { public void pause() { try { mSessionBinder.pause(mContext.getPackageName()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling pause.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -822,8 +813,8 @@ public final class MediaController { public void stop() { try { mSessionBinder.stop(mContext.getPackageName()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling stop.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -835,8 +826,8 @@ public final class MediaController { public void seekTo(long pos) { try { mSessionBinder.seekTo(mContext.getPackageName(), pos); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling seekTo.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -847,8 +838,8 @@ public final class MediaController { public void fastForward() { try { mSessionBinder.fastForward(mContext.getPackageName()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling fastForward.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -858,8 +849,8 @@ public final class MediaController { public void skipToNext() { try { mSessionBinder.next(mContext.getPackageName()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling next.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -870,8 +861,8 @@ public final class MediaController { public void rewind() { try { mSessionBinder.rewind(mContext.getPackageName()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling rewind.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -881,8 +872,8 @@ public final class MediaController { public void skipToPrevious() { try { mSessionBinder.previous(mContext.getPackageName()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling previous.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -896,8 +887,8 @@ public final class MediaController { public void setRating(Rating rating) { try { mSessionBinder.rate(mContext.getPackageName(), rating); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling rate.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -914,8 +905,8 @@ public final class MediaController { } try { mSessionBinder.setPlaybackSpeed(mContext.getPackageName(), speed); - } catch (RemoteException e) { - Log.wtf(TAG, "Error calling setPlaybackSpeed.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } @@ -949,8 +940,8 @@ public final class MediaController { } try { mSessionBinder.sendCustomAction(mContext.getPackageName(), action, args); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in sendCustomAction.", e); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); } } } diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index a33e2252019b..055ccbced58a 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -27,6 +27,7 @@ package android.nfc { field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC = "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC"; field @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.SHOW_CUSTOMIZED_RESOLVER) public static final String ACTION_SHOW_NFC_RESOLVER = "android.nfc.action.SHOW_NFC_RESOLVER"; field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String EXTRA_RESOLVE_INFOS = "android.nfc.extra.RESOLVE_INFOS"; + field @FlaggedApi("android.nfc.nfc_set_default_disc_tech") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static final int FLAG_SET_DEFAULT_TECH = 1073741824; // 0x40000000 field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int MESSAGE_TYPE_COMMAND = 1; // 0x1 field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_FAILED = 3; // 0x3 field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_MESSAGE_CORRUPTED = 2; // 0x2 diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 698df28129be..1dfc81e2108e 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -340,7 +340,8 @@ public final class NfcAdapter { public static final int FLAG_READER_NFC_BARCODE = 0x10; /** @hide */ - @IntDef(flag = true, prefix = {"FLAG_READER_"}, value = { + @IntDef(flag = true, value = { + FLAG_SET_DEFAULT_TECH, FLAG_READER_KEEP, FLAG_READER_DISABLE, FLAG_READER_NFC_A, @@ -438,7 +439,8 @@ public final class NfcAdapter { public static final int FLAG_USE_ALL_TECH = 0xff; /** @hide */ - @IntDef(flag = true, prefix = {"FLAG_LISTEN_"}, value = { + @IntDef(flag = true, value = { + FLAG_SET_DEFAULT_TECH, FLAG_LISTEN_KEEP, FLAG_LISTEN_DISABLE, FLAG_LISTEN_NFC_PASSIVE_A, @@ -449,6 +451,18 @@ public final class NfcAdapter { public @interface ListenTechnology {} /** + * Flag used in {@link #setDiscoveryTechnology(Activity, int, int)}. + * <p> + * Setting this flag changes the default listen or poll tech. + * Only available to privileged apps. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_NFC_SET_DEFAULT_DISC_TECH) + @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) + public static final int FLAG_SET_DEFAULT_TECH = 0x40000000; + + /** * @hide * @removed */ @@ -1874,14 +1888,6 @@ public final class NfcAdapter { public void setDiscoveryTechnology(@NonNull Activity activity, @PollTechnology int pollTechnology, @ListenTechnology int listenTechnology) { - // A special treatment of the _KEEP flags - if ((listenTechnology & FLAG_LISTEN_KEEP) != 0) { - listenTechnology = -1; - } - if ((pollTechnology & FLAG_READER_KEEP) != 0) { - pollTechnology = -1; - } - if (listenTechnology == FLAG_LISTEN_DISABLE) { synchronized (sLock) { if (!sHasNfcFeature) { @@ -1901,7 +1907,25 @@ public final class NfcAdapter { } } } - mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology); + /* + * Privileged FLAG to set technology mask for all data processed by NFC controller + * Note: Use with caution! The app is responsible for ensuring that the discovery + * technology mask is returned to default. + * Note: FLAG_USE_ALL_TECH used with _KEEP flags will reset the technolody to android default + */ + if (Flags.nfcSetDefaultDiscTech() + && ((pollTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH + || (listenTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH)) { + Binder token = new Binder(); + try { + NfcAdapter.sService.updateDiscoveryTechnology(token, + pollTechnology, listenTechnology); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } else { + mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology); + } } /** diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig index cb2a48c2913f..b242a76ffae4 100644 --- a/nfc/java/android/nfc/flags.aconfig +++ b/nfc/java/android/nfc/flags.aconfig @@ -101,3 +101,12 @@ flag { description: "Enable nfc state change API" bug: "319934052" } + +flag { + name: "nfc_set_default_disc_tech" + is_exported: true + namespace: "nfc" + description: "Flag for NFC set default disc tech API" + bug: "321311407" +} + diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt index b43b5f318cf1..373b3e8b068d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt @@ -38,7 +38,6 @@ import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.creation.CreateOptionInfo import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.model.get.ProviderInfo -import java.lang.Exception /** * Aggregates common display information used for the Biometric Flow. @@ -121,11 +120,11 @@ fun runBiometricFlowForGet( getBiometricCancellationSignal: () -> CancellationSignal, getRequestDisplayInfo: RequestDisplayInfo? = null, getProviderInfoList: List<ProviderInfo>? = null, - getProviderDisplayInfo: ProviderDisplayInfo? = null, -) { + getProviderDisplayInfo: ProviderDisplayInfo? = null +): Boolean { if (getBiometricPromptState() != BiometricPromptState.INACTIVE) { // Screen is already up, do not re-launch - return + return false } onBiometricPromptStateChange(BiometricPromptState.PENDING) val biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo( @@ -137,7 +136,7 @@ fun runBiometricFlowForGet( if (biometricDisplayInfo == null) { onBiometricFailureFallback(BiometricFlowType.GET) - return + return false } val callback: BiometricPrompt.AuthenticationCallback = @@ -146,7 +145,7 @@ fun runBiometricFlowForGet( getBiometricPromptState) Log.d(TAG, "The BiometricPrompt API call begins for Get.") - runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, + return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, onBiometricFailureFallback, BiometricFlowType.GET, onCancelFlowAndFinish, getBiometricCancellationSignal) } @@ -169,11 +168,11 @@ fun runBiometricFlowForCreate( getBiometricCancellationSignal: () -> CancellationSignal, createRequestDisplayInfo: com.android.credentialmanager.createflow .RequestDisplayInfo? = null, - createProviderInfo: EnabledProviderInfo? = null, -) { + createProviderInfo: EnabledProviderInfo? = null +): Boolean { if (getBiometricPromptState() != BiometricPromptState.INACTIVE) { // Screen is already up, do not re-launch - return + return false } onBiometricPromptStateChange(BiometricPromptState.PENDING) val biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo( @@ -184,7 +183,7 @@ fun runBiometricFlowForCreate( if (biometricDisplayInfo == null) { onBiometricFailureFallback(BiometricFlowType.CREATE) - return + return false } val callback: BiometricPrompt.AuthenticationCallback = @@ -193,7 +192,7 @@ fun runBiometricFlowForCreate( getBiometricPromptState) Log.d(TAG, "The BiometricPrompt API call begins for Create.") - runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, + return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage, onBiometricFailureFallback, BiometricFlowType.CREATE, onCancelFlowAndFinish, getBiometricCancellationSignal) } @@ -206,19 +205,19 @@ fun runBiometricFlowForCreate( * only device credentials are requested. */ private fun runBiometricFlow( - context: Context, - biometricDisplayInfo: BiometricDisplayInfo, - callback: BiometricPrompt.AuthenticationCallback, - openMoreOptionsPage: () -> Unit, - onBiometricFailureFallback: (BiometricFlowType) -> Unit, - biometricFlowType: BiometricFlowType, - onCancelFlowAndFinish: () -> Unit, - getBiometricCancellationSignal: () -> CancellationSignal, -) { + context: Context, + biometricDisplayInfo: BiometricDisplayInfo, + callback: BiometricPrompt.AuthenticationCallback, + openMoreOptionsPage: () -> Unit, + onBiometricFailureFallback: (BiometricFlowType) -> Unit, + biometricFlowType: BiometricFlowType, + onCancelFlowAndFinish: () -> Unit, + getBiometricCancellationSignal: () -> CancellationSignal +): Boolean { try { if (!canCallBiometricPrompt(biometricDisplayInfo, context)) { onBiometricFailureFallback(biometricFlowType) - return + return false } val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, @@ -239,7 +238,9 @@ private fun runBiometricFlow( } catch (e: IllegalArgumentException) { Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n") onBiometricFailureFallback(biometricFlowType) + return false } + return true } private fun getCryptoOpId(biometricDisplayInfo: BiometricDisplayInfo): Int? { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 7d61f73a525b..4993a1fa0672 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -123,7 +123,8 @@ fun CreateCredentialScreen( onBiometricPromptStateChange = viewModel::onBiometricPromptStateChange, getBiometricCancellationSignal = - viewModel::getBiometricCancellationSignal + viewModel::getBiometricCancellationSignal, + onLog = { viewModel.logUiEvent(it) }, ) CreateScreenState.MORE_OPTIONS_SELECTION_ONLY -> MoreOptionsSelectionCard( requestDisplayInfo = createCredentialUiState.requestDisplayInfo, @@ -642,12 +643,13 @@ internal fun BiometricSelectionPage( getBiometricPromptState: () -> BiometricPromptState, onBiometricPromptStateChange: (BiometricPromptState) -> Unit, getBiometricCancellationSignal: () -> CancellationSignal, + onLog: @Composable (UiEventEnum) -> Unit ) { if (biometricEntry == null) { fallbackToOriginalFlow(BiometricFlowType.CREATE) return } - runBiometricFlowForCreate( + val biometricFlowCalled = runBiometricFlowForCreate( biometricEntry = biometricEntry, context = LocalContext.current, openMoreOptionsPage = onMoreOptionSelected, @@ -659,6 +661,9 @@ internal fun BiometricSelectionPage( createProviderInfo = enabledProviderInfo, onBiometricFailureFallback = fallbackToOriginalFlow, onIllegalStateAndFinish = onIllegalScreenStateAndFinish, - getBiometricCancellationSignal = getBiometricCancellationSignal, + getBiometricCancellationSignal = getBiometricCancellationSignal ) + if (biometricFlowCalled) { + onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED) + } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index ba61b90fa4dc..517ad0069f85 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -166,7 +166,8 @@ fun GetCredentialScreen( onBiometricPromptStateChange = viewModel::onBiometricPromptStateChange, getBiometricCancellationSignal = - viewModel::getBiometricCancellationSignal + viewModel::getBiometricCancellationSignal, + onLog = { viewModel.logUiEvent(it) }, ) } else if (credmanBiometricApiEnabled() && getCredentialUiState.currentScreenState @@ -260,12 +261,13 @@ internal fun BiometricSelectionPage( getBiometricPromptState: () -> BiometricPromptState, onBiometricPromptStateChange: (BiometricPromptState) -> Unit, getBiometricCancellationSignal: () -> CancellationSignal, + onLog: @Composable (UiEventEnum) -> Unit, ) { if (biometricEntry == null) { fallbackToOriginalFlow(BiometricFlowType.GET) return } - runBiometricFlowForGet( + val biometricFlowCalled = runBiometricFlowForGet( biometricEntry = biometricEntry, context = LocalContext.current, openMoreOptionsPage = onMoreOptionSelected, @@ -280,6 +282,9 @@ internal fun BiometricSelectionPage( onBiometricFailureFallback = fallbackToOriginalFlow, getBiometricCancellationSignal = getBiometricCancellationSignal ) + if (biometricFlowCalled) { + onLog(GetCredentialEvent.CREDMAN_GET_CRED_BIOMETRIC_FLOW_LAUNCHED) + } } /** Draws the primary credential selection page, used in Android U. */ diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt index daa42be020ce..39f2fcee6a0a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt @@ -17,6 +17,7 @@ package com.android.credentialmanager.logging import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID enum class CreateCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum { @@ -52,7 +53,10 @@ enum class CreateCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnu CREDMAN_CREATE_CRED_EXTERNAL_ONLY_SELECTION(1327), @UiEvent(doc = "The more about passkeys intro card is visible on screen.") - CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO(1328); + CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO(1328), + + @UiEvent(doc = "The single tap biometric flow is launched.") + CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED(RESERVE_NEW_UI_EVENT_ID); override fun getId(): Int { return this.id diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt index 8de8895e8ffc..89fd72cdc8cc 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt @@ -17,6 +17,7 @@ package com.android.credentialmanager.logging import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID enum class GetCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum { @@ -54,7 +55,10 @@ enum class GetCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum { CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD(1341), @UiEvent(doc = "The all sign in option card is visible on screen.") - CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD(1342); + CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD(1342), + + @UiEvent(doc = "The single tap biometric flow is launched.") + CREDMAN_GET_CRED_BIOMETRIC_FLOW_LAUNCHED(RESERVE_NEW_UI_EVENT_ID); override fun getId(): Int { return this.id diff --git a/packages/SettingsLib/Color/res/values/colors.xml b/packages/SettingsLib/Color/res/values/colors.xml index ef0dd1b654b9..b0b9b10952b8 100644 --- a/packages/SettingsLib/Color/res/values/colors.xml +++ b/packages/SettingsLib/Color/res/values/colors.xml @@ -17,7 +17,6 @@ <resources> <!-- Dynamic colors--> - <color name="settingslib_color_blue700">#0B57D0</color> <color name="settingslib_color_blue600">#1a73e8</color> <color name="settingslib_color_blue400">#669df6</color> <color name="settingslib_color_blue300">#8ab4f8</color> diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt index 82423473e682..b4a91726ac1d 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt @@ -138,7 +138,7 @@ class BackupRestoreStorageManager private constructor(private val application: A private fun notifyBackupManager(key: Any?, reason: Int) { val name = storage.name // prefer not triggering backup immediately after restore - if (reason == ChangeReason.RESTORE) { + if (reason == DataChangeReason.RESTORE) { Log.d( LOG_TAG, "Notify BackupManager dataChanged ignored for restore: storage=$name key=$key" @@ -161,8 +161,8 @@ class BackupRestoreStorageManager private constructor(private val application: A fun notifyRestoreFinished() { when (storage) { - is KeyedObservable<*> -> storage.notifyChange(ChangeReason.RESTORE) - is Observable -> storage.notifyChange(ChangeReason.RESTORE) + is KeyedObservable<*> -> storage.notifyChange(DataChangeReason.RESTORE) + is Observable -> storage.notifyChange(DataChangeReason.RESTORE) } } } diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt new file mode 100644 index 000000000000..145fabea52af --- /dev/null +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.datastore + +import androidx.annotation.IntDef + +/** The reason of data change. */ +@IntDef( + DataChangeReason.UNKNOWN, + DataChangeReason.UPDATE, + DataChangeReason.DELETE, + DataChangeReason.RESTORE, + DataChangeReason.SYNC_ACROSS_PROFILES, +) +@Retention(AnnotationRetention.SOURCE) +annotation class DataChangeReason { + companion object { + /** Unknown reason of the change. */ + const val UNKNOWN = 0 + /** Data is updated. */ + const val UPDATE = 1 + /** Data is deleted. */ + const val DELETE = 2 + /** Data is restored from backup/restore framework. */ + const val RESTORE = 3 + /** Data is synced from another profile (e.g. personal profile to work profile). */ + const val SYNC_ACROSS_PROFILES = 4 + } +} diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt index 3ed4d46459cf..ede7c63d00b4 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt @@ -37,7 +37,7 @@ fun interface KeyedObserver<in K> { * @param reason the reason of change * @see KeyedObservable.addObserver */ - fun onKeyChanged(key: K, @ChangeReason reason: Int) + fun onKeyChanged(key: K, reason: Int) } /** @@ -89,7 +89,7 @@ interface KeyedObservable<K> { * * @param reason reason of the change */ - fun notifyChange(@ChangeReason reason: Int) + fun notifyChange(reason: Int) /** * Notifies observers that a change occurs on given key. @@ -99,7 +99,7 @@ interface KeyedObservable<K> { * @param key key of the change * @param reason reason of the change */ - fun notifyChange(key: K, @ChangeReason reason: Int) + fun notifyChange(key: K, reason: Int) } /** A thread safe implementation of [KeyedObservable]. */ @@ -141,7 +141,7 @@ class KeyedDataObservable<K> : KeyedObservable<K> { } } - override fun notifyChange(@ChangeReason reason: Int) { + override fun notifyChange(reason: Int) { // make a copy to avoid potential ConcurrentModificationException val observers = synchronized(observers) { observers.entries.toTypedArray() } val keyedObservers = synchronized(keyedObservers) { keyedObservers.copy() } @@ -165,7 +165,7 @@ class KeyedDataObservable<K> : KeyedObservable<K> { return result } - override fun notifyChange(key: K, @ChangeReason reason: Int) { + override fun notifyChange(key: K, reason: Int) { // make a copy to avoid potential ConcurrentModificationException val observers = synchronized(observers) { observers.entries.toTypedArray() } val keyedObservers = diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt index 6d0ca6690c9f..98d0f6e3f9a1 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/Observer.kt @@ -18,34 +18,9 @@ package com.android.settingslib.datastore import androidx.annotation.AnyThread import androidx.annotation.GuardedBy -import androidx.annotation.IntDef import java.util.WeakHashMap import java.util.concurrent.Executor -/** The reason of a change. */ -@IntDef( - ChangeReason.UNKNOWN, - ChangeReason.UPDATE, - ChangeReason.DELETE, - ChangeReason.RESTORE, - ChangeReason.SYNC_ACROSS_PROFILES, -) -@Retention(AnnotationRetention.SOURCE) -annotation class ChangeReason { - companion object { - /** Unknown reason of the change. */ - const val UNKNOWN = 0 - /** Data is updated. */ - const val UPDATE = 1 - /** Data is deleted. */ - const val DELETE = 2 - /** Data is restored from backup/restore framework. */ - const val RESTORE = 3 - /** Data is synced from another profile (e.g. personal profile to work profile). */ - const val SYNC_ACROSS_PROFILES = 4 - } -} - /** * Callback to be informed of changes in [Observable] object. * @@ -60,7 +35,7 @@ fun interface Observer { * @param reason the reason of change * @see [Observable.addObserver] for the notices. */ - fun onChanged(@ChangeReason reason: Int) + fun onChanged(reason: Int) } /** An observable object allows to observe change with [Observer]. */ @@ -90,7 +65,7 @@ interface Observable { * * @param reason reason of the change */ - fun notifyChange(@ChangeReason reason: Int) + fun notifyChange(reason: Int) } /** A thread safe implementation of [Observable]. */ @@ -110,7 +85,7 @@ class DataObservable : Observable { synchronized(observers) { observers.remove(observer) } } - override fun notifyChange(@ChangeReason reason: Int) { + override fun notifyChange(reason: Int) { // make a copy to avoid potential ConcurrentModificationException val entries = synchronized(observers) { observers.entries.toTypedArray() } for (entry in entries) { diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt index 9f9c0d839744..20a95d7efc4b 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt @@ -83,10 +83,10 @@ constructor( private val sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> if (key != null) { - notifyChange(key, ChangeReason.UPDATE) + notifyChange(key, DataChangeReason.UPDATE) } else { // On Android >= R, SharedPreferences.Editor.clear() will trigger this case - notifyChange(ChangeReason.DELETE) + notifyChange(DataChangeReason.DELETE) } } diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt index d8f502854402..19c574a843ca 100644 --- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt +++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/BackupRestoreStorageManagerTest.kt @@ -157,9 +157,9 @@ class BackupRestoreStorageManagerTest { manager.onRestoreFinished() - verify(keyedObserver).onKeyChanged("key", ChangeReason.RESTORE) - verify(anyKeyObserver).onKeyChanged(null, ChangeReason.RESTORE) - verify(observer).onChanged(ChangeReason.RESTORE) + verify(keyedObserver).onKeyChanged("key", DataChangeReason.RESTORE) + verify(anyKeyObserver).onKeyChanged(null, DataChangeReason.RESTORE) + verify(observer).onChanged(DataChangeReason.RESTORE) if (isRobolectric()) { Shadows.shadowOf(BackupManager(application)).apply { assertThat(isDataChanged).isFalse() @@ -186,8 +186,8 @@ class BackupRestoreStorageManagerTest { assertThat(dataChangedCount).isEqualTo(0) } - fileStorage.notifyChange(ChangeReason.UPDATE) - verify(observer).onChanged(ChangeReason.UPDATE) + fileStorage.notifyChange(DataChangeReason.UPDATE) + verify(observer).onChanged(DataChangeReason.UPDATE) verify(keyedObserver, never()).onKeyChanged(any(), any()) verify(anyKeyObserver, never()).onKeyChanged(any(), any()) reset(observer) @@ -196,10 +196,10 @@ class BackupRestoreStorageManagerTest { assertThat(dataChangedCount).isEqualTo(1) } - keyedStorage.notifyChange("key", ChangeReason.DELETE) + keyedStorage.notifyChange("key", DataChangeReason.DELETE) verify(observer, never()).onChanged(any()) - verify(keyedObserver).onKeyChanged("key", ChangeReason.DELETE) - verify(anyKeyObserver).onKeyChanged("key", ChangeReason.DELETE) + verify(keyedObserver).onKeyChanged("key", DataChangeReason.DELETE) + verify(anyKeyObserver).onKeyChanged("key", DataChangeReason.DELETE) backupManager?.apply { assertThat(isDataChanged).isTrue() assertThat(dataChangedCount).isEqualTo(2) @@ -207,11 +207,11 @@ class BackupRestoreStorageManagerTest { reset(keyedObserver) // backup manager is not notified for restore event - fileStorage.notifyChange(ChangeReason.RESTORE) - keyedStorage.notifyChange("key", ChangeReason.RESTORE) - verify(observer).onChanged(ChangeReason.RESTORE) - verify(keyedObserver).onKeyChanged("key", ChangeReason.RESTORE) - verify(anyKeyObserver).onKeyChanged("key", ChangeReason.RESTORE) + fileStorage.notifyChange(DataChangeReason.RESTORE) + keyedStorage.notifyChange("key", DataChangeReason.RESTORE) + verify(observer).onChanged(DataChangeReason.RESTORE) + verify(keyedObserver).onKeyChanged("key", DataChangeReason.RESTORE) + verify(anyKeyObserver).onKeyChanged("key", DataChangeReason.RESTORE) backupManager?.apply { assertThat(isDataChanged).isTrue() assertThat(dataChangedCount).isEqualTo(2) diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt index 8638b2f20b52..0fdecb034f83 100644 --- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt +++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt @@ -77,7 +77,7 @@ class KeyedObserverTest { var observer: KeyedObserver<Any?>? = KeyedObserver { _, _ -> counter.incrementAndGet() } keyedObservable.addObserver(observer!!, executor1) - keyedObservable.notifyChange(ChangeReason.UPDATE) + keyedObservable.notifyChange(DataChangeReason.UPDATE) assertThat(counter.get()).isEqualTo(1) // trigger GC, the observer callback should not be invoked @@ -85,7 +85,7 @@ class KeyedObserverTest { System.gc() System.runFinalization() - keyedObservable.notifyChange(ChangeReason.UPDATE) + keyedObservable.notifyChange(DataChangeReason.UPDATE) assertThat(counter.get()).isEqualTo(1) } @@ -95,7 +95,7 @@ class KeyedObserverTest { var keyObserver: KeyedObserver<Any>? = KeyedObserver { _, _ -> counter.incrementAndGet() } keyedObservable.addObserver(key1, keyObserver!!, executor1) - keyedObservable.notifyChange(key1, ChangeReason.UPDATE) + keyedObservable.notifyChange(key1, DataChangeReason.UPDATE) assertThat(counter.get()).isEqualTo(1) // trigger GC, the observer callback should not be invoked @@ -103,7 +103,7 @@ class KeyedObserverTest { System.gc() System.runFinalization() - keyedObservable.notifyChange(key1, ChangeReason.UPDATE) + keyedObservable.notifyChange(key1, DataChangeReason.UPDATE) assertThat(counter.get()).isEqualTo(1) } @@ -112,16 +112,16 @@ class KeyedObserverTest { keyedObservable.addObserver(observer1, executor1) keyedObservable.addObserver(observer2, executor2) - keyedObservable.notifyChange(ChangeReason.UPDATE) - verify(observer1).onKeyChanged(null, ChangeReason.UPDATE) - verify(observer2).onKeyChanged(null, ChangeReason.UPDATE) + keyedObservable.notifyChange(DataChangeReason.UPDATE) + verify(observer1).onKeyChanged(null, DataChangeReason.UPDATE) + verify(observer2).onKeyChanged(null, DataChangeReason.UPDATE) reset(observer1, observer2) keyedObservable.removeObserver(observer2) - keyedObservable.notifyChange(ChangeReason.DELETE) - verify(observer1).onKeyChanged(null, ChangeReason.DELETE) - verify(observer2, never()).onKeyChanged(null, ChangeReason.DELETE) + keyedObservable.notifyChange(DataChangeReason.DELETE) + verify(observer1).onKeyChanged(null, DataChangeReason.DELETE) + verify(observer2, never()).onKeyChanged(null, DataChangeReason.DELETE) } @Test @@ -129,16 +129,16 @@ class KeyedObserverTest { keyedObservable.addObserver(key1, keyedObserver1, executor1) keyedObservable.addObserver(key2, keyedObserver2, executor2) - keyedObservable.notifyChange(key1, ChangeReason.UPDATE) - verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE) - verify(keyedObserver2, never()).onKeyChanged(key2, ChangeReason.UPDATE) + keyedObservable.notifyChange(key1, DataChangeReason.UPDATE) + verify(keyedObserver1).onKeyChanged(key1, DataChangeReason.UPDATE) + verify(keyedObserver2, never()).onKeyChanged(key2, DataChangeReason.UPDATE) reset(keyedObserver1, keyedObserver2) keyedObservable.removeObserver(key1, keyedObserver1) - keyedObservable.notifyChange(key1, ChangeReason.DELETE) - verify(keyedObserver1, never()).onKeyChanged(key1, ChangeReason.DELETE) - verify(keyedObserver2, never()).onKeyChanged(key2, ChangeReason.DELETE) + keyedObservable.notifyChange(key1, DataChangeReason.DELETE) + verify(keyedObserver1, never()).onKeyChanged(key1, DataChangeReason.DELETE) + verify(keyedObserver2, never()).onKeyChanged(key2, DataChangeReason.DELETE) } @Test @@ -147,24 +147,24 @@ class KeyedObserverTest { keyedObservable.addObserver(key1, keyedObserver1, executor1) keyedObservable.addObserver(key2, keyedObserver2, executor1) - keyedObservable.notifyChange(ChangeReason.UPDATE) - verify(observer1).onKeyChanged(null, ChangeReason.UPDATE) - verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE) - verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE) + keyedObservable.notifyChange(DataChangeReason.UPDATE) + verify(observer1).onKeyChanged(null, DataChangeReason.UPDATE) + verify(keyedObserver1).onKeyChanged(key1, DataChangeReason.UPDATE) + verify(keyedObserver2).onKeyChanged(key2, DataChangeReason.UPDATE) reset(observer1, keyedObserver1, keyedObserver2) - keyedObservable.notifyChange(key1, ChangeReason.UPDATE) + keyedObservable.notifyChange(key1, DataChangeReason.UPDATE) - verify(observer1).onKeyChanged(key1, ChangeReason.UPDATE) - verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE) - verify(keyedObserver2, never()).onKeyChanged(key1, ChangeReason.UPDATE) + verify(observer1).onKeyChanged(key1, DataChangeReason.UPDATE) + verify(keyedObserver1).onKeyChanged(key1, DataChangeReason.UPDATE) + verify(keyedObserver2, never()).onKeyChanged(key1, DataChangeReason.UPDATE) reset(observer1, keyedObserver1, keyedObserver2) - keyedObservable.notifyChange(key2, ChangeReason.UPDATE) + keyedObservable.notifyChange(key2, DataChangeReason.UPDATE) - verify(observer1).onKeyChanged(key2, ChangeReason.UPDATE) - verify(keyedObserver1, never()).onKeyChanged(key2, ChangeReason.UPDATE) - verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE) + verify(observer1).onKeyChanged(key2, DataChangeReason.UPDATE) + verify(keyedObserver1, never()).onKeyChanged(key2, DataChangeReason.UPDATE) + verify(keyedObserver2).onKeyChanged(key2, DataChangeReason.UPDATE) } @Test @@ -176,7 +176,7 @@ class KeyedObserverTest { keyedObservable.addObserver(observer, executor1) - keyedObservable.notifyChange(ChangeReason.UPDATE) + keyedObservable.notifyChange(DataChangeReason.UPDATE) keyedObservable.removeObserver(observer) } @@ -189,7 +189,7 @@ class KeyedObserverTest { keyedObservable.addObserver(key1, keyObserver, executor1) - keyedObservable.notifyChange(key1, ChangeReason.UPDATE) + keyedObservable.notifyChange(key1, DataChangeReason.UPDATE) keyedObservable.removeObserver(key1, keyObserver) } } diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt index 173c2b1d4b81..5d0303c06d41 100644 --- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt +++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt @@ -58,7 +58,7 @@ class ObserverTest { var observer: Observer? = Observer { counter.incrementAndGet() } observable.addObserver(observer!!, executor1) - observable.notifyChange(ChangeReason.UPDATE) + observable.notifyChange(DataChangeReason.UPDATE) assertThat(counter.get()).isEqualTo(1) // trigger GC, the observer callback should not be invoked @@ -66,7 +66,7 @@ class ObserverTest { System.gc() System.runFinalization() - observable.notifyChange(ChangeReason.UPDATE) + observable.notifyChange(DataChangeReason.UPDATE) assertThat(counter.get()).isEqualTo(1) } @@ -75,17 +75,17 @@ class ObserverTest { observable.addObserver(observer1, executor1) observable.addObserver(observer2, executor2) - observable.notifyChange(ChangeReason.DELETE) + observable.notifyChange(DataChangeReason.DELETE) - verify(observer1).onChanged(ChangeReason.DELETE) - verify(observer2).onChanged(ChangeReason.DELETE) + verify(observer1).onChanged(DataChangeReason.DELETE) + verify(observer2).onChanged(DataChangeReason.DELETE) reset(observer1, observer2) observable.removeObserver(observer2) - observable.notifyChange(ChangeReason.UPDATE) - verify(observer1).onChanged(ChangeReason.UPDATE) - verify(observer2, never()).onChanged(ChangeReason.UPDATE) + observable.notifyChange(DataChangeReason.UPDATE) + verify(observer1).onChanged(DataChangeReason.UPDATE) + verify(observer2, never()).onChanged(DataChangeReason.UPDATE) } @Test @@ -93,7 +93,7 @@ class ObserverTest { // ConcurrentModificationException is raised if it is not implemented correctly val observer = Observer { observable.addObserver(observer1, executor1) } observable.addObserver(observer, executor1) - observable.notifyChange(ChangeReason.UPDATE) + observable.notifyChange(DataChangeReason.UPDATE) observable.removeObserver(observer) } } diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/SharedPreferencesStorageTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/SharedPreferencesStorageTest.kt index fec7d758b893..a135d77e6d25 100644 --- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/SharedPreferencesStorageTest.kt +++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/SharedPreferencesStorageTest.kt @@ -80,13 +80,13 @@ class SharedPreferencesStorageTest { storage.addObserver("key", keyedObserver, executor) storage.sharedPreferences.edit().putString("key", "string").applySync() - verify(observer).onKeyChanged("key", ChangeReason.UPDATE) - verify(keyedObserver).onKeyChanged("key", ChangeReason.UPDATE) + verify(observer).onKeyChanged("key", DataChangeReason.UPDATE) + verify(keyedObserver).onKeyChanged("key", DataChangeReason.UPDATE) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { storage.sharedPreferences.edit().clear().applySync() - verify(observer).onKeyChanged(null, ChangeReason.DELETE) - verify(keyedObserver).onKeyChanged("key", ChangeReason.DELETE) + verify(observer).onKeyChanged(null, DataChangeReason.DELETE) + verify(keyedObserver).onKeyChanged("key", DataChangeReason.DELETE) } } diff --git a/packages/SettingsLib/IllustrationPreference/Android.bp b/packages/SettingsLib/IllustrationPreference/Android.bp index c3a91a20c339..cd8f584953aa 100644 --- a/packages/SettingsLib/IllustrationPreference/Android.bp +++ b/packages/SettingsLib/IllustrationPreference/Android.bp @@ -47,6 +47,7 @@ java_aconfig_library { aconfig_declarations: "settingslib_illustrationpreference_flags", min_sdk_version: "30", + sdk_version: "system_current", apex_available: [ "//apex_available:platform", diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java index bc3488fc31fb..0447ef8357eb 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java @@ -56,9 +56,6 @@ public class LottieColorUtils { ".black", android.R.color.white); map.put( - ".blue200", - R.color.settingslib_color_blue700); - map.put( ".blue400", R.color.settingslib_color_blue600); map.put( diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt index e91fa65401a4..e9f9689cb319 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt @@ -18,9 +18,9 @@ package com.android.settingslib.spa.framework.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner @Composable fun LifecycleEffect( diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OnBackEffect.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OnBackEffect.kt index 3991f26e1b0c..0b1c92d78a57 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OnBackEffect.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OnBackEffect.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner /** * An effect for detecting presses of the system back button, and the back event will not be diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt index ee24a09d4395..007f47bd3c82 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt @@ -21,8 +21,8 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.slice.widget.SliceLiveData import androidx.slice.widget.SliceView diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt index de080e3d8ef4..022ddedd1062 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.window.DialogProperties data class AlertDialogButton( val text: String, + val enabled: Boolean = true, val onClick: () -> Unit = {}, ) @@ -114,6 +115,7 @@ private fun AlertDialogPresenter.Button(button: AlertDialogButton) { close() button.onClick() }, + enabled = button.enabled, ) { Text(button.text) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt index b471e50be275..bdbe62c07425 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -65,7 +66,7 @@ internal fun DropdownTextBox( OutlinedTextField( // The `menuAnchor` modifier must be passed to the text field for correctness. modifier = Modifier - .menuAnchor() + .menuAnchor(MenuAnchorType.PrimaryNotEditable) .fillMaxWidth(), value = text, onValueChange = { }, diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt index fe7baff43101..8b0efff591f2 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt @@ -18,9 +18,9 @@ package com.android.settingslib.spa.framework.compose import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogTest.kt index 9468f95a094e..20ea397d7222 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogTest.kt @@ -20,6 +20,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -67,7 +69,18 @@ class SettingsAlertDialogTest { rememberAlertDialogPresenter(confirmButton = AlertDialogButton(CONFIRM_TEXT)) } - composeTestRule.onDialogText(CONFIRM_TEXT).assertIsDisplayed() + composeTestRule.onDialogText(CONFIRM_TEXT).assertIsDisplayed().assertIsEnabled() + } + + @Test + fun confirmButton_disabled() { + setAndOpenDialog { + rememberAlertDialogPresenter( + confirmButton = AlertDialogButton(text = CONFIRM_TEXT, enabled = false) + ) + } + + composeTestRule.onDialogText(CONFIRM_TEXT).assertIsDisplayed().assertIsNotEnabled() } @Test @@ -90,7 +103,18 @@ class SettingsAlertDialogTest { rememberAlertDialogPresenter(dismissButton = AlertDialogButton(DISMISS_TEXT)) } - composeTestRule.onDialogText(DISMISS_TEXT).assertIsDisplayed() + composeTestRule.onDialogText(DISMISS_TEXT).assertIsDisplayed().assertIsEnabled() + } + + @Test + fun dismissButton_disabled() { + setAndOpenDialog { + rememberAlertDialogPresenter( + dismissButton = AlertDialogButton(text = DISMISS_TEXT, enabled = false) + ) + } + + composeTestRule.onDialogText(DISMISS_TEXT).assertIsDisplayed().assertIsNotEnabled() } @Test diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt index 977615b55a6a..f95cfc3191c8 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt @@ -30,22 +30,24 @@ import com.android.settingslib.spaprivileged.model.enterprise.rememberRestricted @Composable fun MoreOptionsScope.RestrictedMenuItem( text: String, + enabled: Boolean = true, restrictions: Restrictions, onClick: () -> Unit, ) { - RestrictedMenuItemImpl(text, restrictions, onClick, ::RestrictionsProviderImpl) + RestrictedMenuItemImpl(text, enabled, restrictions, onClick, ::RestrictionsProviderImpl) } @VisibleForTesting @Composable internal fun MoreOptionsScope.RestrictedMenuItemImpl( text: String, + enabled: Boolean = true, restrictions: Restrictions, onClick: () -> Unit, restrictionsProviderFactory: RestrictionsProviderFactory, ) { val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value - MenuItem(text = text, enabled = restrictedMode !== BaseUserRestricted) { + MenuItem(text = text, enabled = enabled && restrictedMode !== BaseUserRestricted) { when (restrictedMode) { is BlockedByAdmin -> restrictedMode.sendShowAdminSupportDetailsIntent() is BlockedByEcm -> restrictedMode.showRestrictedSettingsDetails() diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt index 556adc750763..4068bceb1475 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt @@ -49,6 +49,15 @@ class RestrictedMenuItemTest { private var menuItemOnClickIsCalled = false @Test + fun whenDisabled() { + val restrictions = Restrictions(userId = USER_ID, keys = emptyList()) + + setContent(restrictions, enabled = false) + + composeTestRule.onNodeWithText(TEXT).assertIsDisplayed().assertIsNotEnabled() + } + + @Test fun whenRestrictionsKeysIsEmpty_enabled() { val restrictions = Restrictions(userId = USER_ID, keys = emptyList()) @@ -153,13 +162,14 @@ class RestrictedMenuItemTest { assertThat(menuItemOnClickIsCalled).isFalse() } - private fun setContent(restrictions: Restrictions) { + private fun setContent(restrictions: Restrictions, enabled: Boolean = true) { val fakeMoreOptionsScope = object : MoreOptionsScope() { override fun dismiss() {} } composeTestRule.setContent { fakeMoreOptionsScope.RestrictedMenuItemImpl( text = TEXT, + enabled = enabled, restrictions = restrictions, onClick = { menuItemOnClickIsCalled = true }, restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider }, diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml index ab049042b5f9..470cdeea149b 100644 --- a/packages/SettingsLib/res/values/dimens.xml +++ b/packages/SettingsLib/res/values/dimens.xml @@ -32,7 +32,7 @@ <!-- Usage graph dimens --> <dimen name="usage_graph_margin_top_bottom">9dp</dimen> - <dimen name="usage_graph_labels_width">56dp</dimen> + <dimen name="usage_graph_labels_width">60dp</dimen> <dimen name="usage_graph_divider_size">1dp</dimen> diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt index 869fb7f4043c..70811951c9b7 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt @@ -81,7 +81,7 @@ class LocalMediaRepositoryImpl( localMediaManager.unregisterCallback(callback) } } - .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0) + .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 0) override val currentConnectedDevice: StateFlow<MediaDevice?> = merge(devicesChanges, mediaDevicesUpdates) @@ -89,8 +89,8 @@ class LocalMediaRepositoryImpl( .onStart { emit(localMediaManager.currentConnectedDevice) } .stateIn( coroutineScope, - SharingStarted.WhileSubscribed(), - localMediaManager.currentConnectedDevice + SharingStarted.Eagerly, + localMediaManager.currentConnectedDevice, ) private sealed interface DevicesUpdate { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index c8992c344b2d..04922d6e2c99 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -64,7 +64,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; -import java.io.InputStream; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; @@ -86,13 +85,13 @@ import java.util.concurrent.CountDownLatch; // FOR ACONFIGD TEST MISSION AND ROLLOUT import java.io.DataInputStream; import java.io.DataOutputStream; +import android.net.LocalSocketAddress; +import android.net.LocalSocket; import android.util.proto.ProtoInputStream; import android.aconfigd.Aconfigd.StorageRequestMessage; import android.aconfigd.Aconfigd.StorageRequestMessages; import android.aconfigd.Aconfigd.StorageReturnMessage; import android.aconfigd.Aconfigd.StorageReturnMessages; -import android.aconfigd.AconfigdClientSocket; -import android.aconfigd.AconfigdFlagInfo; import android.aconfigd.AconfigdJavaUtils; import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon; /** @@ -266,10 +265,6 @@ final class SettingsState { @NonNull private Map<String, Map<String, String>> mNamespaceDefaults; - // TOBO(b/312444587): remove the comparison logic after Test Mission 2. - @NonNull - private Map<String, AconfigdFlagInfo> mAconfigDefaultFlags; - public static final int SETTINGS_TYPE_GLOBAL = 0; public static final int SETTINGS_TYPE_SYSTEM = 1; public static final int SETTINGS_TYPE_SECURE = 2; @@ -339,13 +334,8 @@ final class SettingsState { + settingTypeToString(getTypeFromKey(key)) + "]"; } - public SettingsState( - Context context, - Object lock, - File file, - int key, - int maxBytesPerAppPackage, - Looper looper) { + public SettingsState(Context context, Object lock, File file, int key, + int maxBytesPerAppPackage, Looper looper) { // It is important that we use the same lock as the settings provider // to ensure multiple mutations on this state are atomically persisted // as the async persistence should be blocked while we make changes. @@ -363,15 +353,12 @@ final class SettingsState { mPackageToMemoryUsage = null; } - mHistoricalOperations = - Build.IS_DEBUGGABLE ? new ArrayList<>(HISTORICAL_OPERATION_COUNT) : null; + mHistoricalOperations = Build.IS_DEBUGGABLE + ? new ArrayList<>(HISTORICAL_OPERATION_COUNT) : null; mNamespaceDefaults = new HashMap<>(); - mAconfigDefaultFlags = new HashMap<>(); ProtoOutputStream requests = null; - Map<String, AconfigdFlagInfo> aconfigFlagMap = new HashMap<>(); - synchronized (mLock) { readStateSyncLocked(); @@ -388,114 +375,39 @@ final class SettingsState { } } - if (enableAconfigStorageDaemon()) { - if (isConfigSettingsKey(mKey)) { - aconfigFlagMap = getAllAconfigFlagsFromSettings(); - } - } - if (isConfigSettingsKey(mKey)) { - requests = handleBulkSyncToNewStorage(aconfigFlagMap); + requests = handleBulkSyncToNewStorage(); } } - if (enableAconfigStorageDaemon()) { - if (isConfigSettingsKey(mKey)){ - AconfigdClientSocket localSocket = AconfigdJavaUtils.getAconfigdClientSocket(); - if (requests != null) { - InputStream res = localSocket.send(requests.getBytes()); - if (res == null) { - Slog.w(LOG_TAG, "Bulk sync request to acongid failed."); - } - } - // TOBO(b/312444587): remove the comparison logic after Test Mission 2. - if (mSettings.get("aconfigd_marker/bulk_synced").value.equals("true") - && requests == null) { - Map<String, AconfigdFlagInfo> aconfigdFlagMap = - AconfigdJavaUtils.listFlagsValueInNewStorage(localSocket); - compareFlagValueInNewStorage( - aconfigFlagMap, - mAconfigDefaultFlags, - aconfigdFlagMap); - } + if (requests != null) { + LocalSocket client = new LocalSocket(); + try{ + client.connect(new LocalSocketAddress( + "aconfigd", LocalSocketAddress.Namespace.RESERVED)); + Slog.d(LOG_TAG, "connected to aconfigd socket"); + } catch (IOException ioe) { + Slog.e(LOG_TAG, "failed to connect to aconfigd socket", ioe); + return; } + AconfigdJavaUtils.sendAconfigdRequests(client, requests); } } - // TOBO(b/312444587): remove the comparison logic after Test Mission 2. - public int compareFlagValueInNewStorage( - Map<String, AconfigdFlagInfo> settingFlagMap, - Map<String, AconfigdFlagInfo> defaultFlagMap, - Map<String, AconfigdFlagInfo> aconfigdFlagMap) { - - // Get all defaults from the default map. The mSettings may not contain - // all flags, since it only contains updated flags. - int diffNum = 0; - for (Map.Entry<String, AconfigdFlagInfo> entry : defaultFlagMap.entrySet()) { - String key = entry.getKey(); - AconfigdFlagInfo flag = entry.getValue(); - if (settingFlagMap.containsKey(key)) { - flag.merge(settingFlagMap.get(key)); - } - - AconfigdFlagInfo aconfigdFlag = aconfigdFlagMap.get(key); - if (aconfigdFlag == null) { - Slog.w(LOG_TAG, String.format("Flag %s is missing from aconfigd", key)); - diffNum++; - continue; - } - String diff = flag.dumpDiff(aconfigdFlag); - if (!diff.isEmpty()) { - Slog.w( - LOG_TAG, - String.format( - "Flag %s is different in Settings and aconfig: %s", key, diff)); - diffNum++; - } - } - - for (String key : aconfigdFlagMap.keySet()) { - if (defaultFlagMap.containsKey(key)) continue; - Slog.w(LOG_TAG, String.format("Flag %s is missing from Settings", key)); - diffNum++; - } - - if (diffNum == 0) { - Slog.i(LOG_TAG, "Settings and new storage have same flags."); - } - return diffNum; - } - - @GuardedBy("mLock") - public Map<String, AconfigdFlagInfo> getAllAconfigFlagsFromSettings() { - Map<String, AconfigdFlagInfo> ret = new HashMap<>(); - int numSettings = mSettings.size(); - int num_requests = 0; - for (int i = 0; i < numSettings; i++) { - String name = mSettings.keyAt(i); - Setting setting = mSettings.valueAt(i); - AconfigdFlagInfo flag = - getFlagOverrideToSync(name, setting.getValue()); - if (flag == null) { - continue; - } - String fullFlagName = flag.getFullFlagName(); - AconfigdFlagInfo prev = ret.putIfAbsent(fullFlagName,flag); - if (prev != null) { - prev.merge(flag); - } - ++num_requests; - } - Slog.i(LOG_TAG, num_requests + " flag override requests created"); - return ret; + // TODO(b/341764371): migrate aconfig flag push to GMS core + public static class FlagOverrideToSync { + public String packageName; + public String flagName; + public String flagValue; + public boolean isLocal; } // TODO(b/341764371): migrate aconfig flag push to GMS core @VisibleForTesting @GuardedBy("mLock") - public AconfigdFlagInfo getFlagOverrideToSync(String name, String value) { + public FlagOverrideToSync getFlagOverrideToSync(String name, String value) { int slashIdx = name.indexOf("/"); - if (slashIdx <= 0 || slashIdx >= name.length() - 1) { + if (slashIdx <= 0 || slashIdx >= name.length()-1) { Slog.e(LOG_TAG, "invalid flag name " + name); return null; } @@ -518,9 +430,8 @@ final class SettingsState { } String aconfigName = namespace + "/" + fullFlagName; - boolean isAconfig = - mNamespaceDefaults.containsKey(namespace) - && mNamespaceDefaults.get(namespace).containsKey(aconfigName); + boolean isAconfig = mNamespaceDefaults.containsKey(namespace) + && mNamespaceDefaults.get(namespace).containsKey(aconfigName); if (!isAconfig) { return null; } @@ -532,30 +443,25 @@ final class SettingsState { return null; } - AconfigdFlagInfo.Builder builder = AconfigdFlagInfo.newBuilder() - .setPackageName(fullFlagName.substring(0, dotIdx)) - .setFlagName(fullFlagName.substring(dotIdx + 1)) - .setDefaultFlagValue(mNamespaceDefaults.get(namespace).get(aconfigName)); - - if (isLocal) { - builder.setHasLocalOverride(isLocal).setBootFlagValue(value).setLocalFlagValue(value); - } else { - builder.setHasServerOverride(true).setServerFlagValue(value).setBootFlagValue(value); - } - return builder.build(); + FlagOverrideToSync flag = new FlagOverrideToSync(); + flag.packageName = fullFlagName.substring(0, dotIdx); + flag.flagName = fullFlagName.substring(dotIdx + 1); + flag.isLocal = isLocal; + flag.flagValue = value; + return flag; } // TODO(b/341764371): migrate aconfig flag push to GMS core @VisibleForTesting @GuardedBy("mLock") - public ProtoOutputStream handleBulkSyncToNewStorage( - Map<String, AconfigdFlagInfo> aconfigFlagMap) { + public ProtoOutputStream handleBulkSyncToNewStorage() { // get marker or add marker if it does not exist final String bulkSyncMarkerName = new String("aconfigd_marker/bulk_synced"); Setting markerSetting = mSettings.get(bulkSyncMarkerName); if (markerSetting == null) { - markerSetting = new Setting(bulkSyncMarkerName, "false", false, "aconfig", "aconfig"); + markerSetting = new Setting( + bulkSyncMarkerName, "false", false, "aconfig", "aconfig"); mSettings.put(bulkSyncMarkerName, markerSetting); } @@ -573,19 +479,24 @@ final class SettingsState { AconfigdJavaUtils.writeResetStorageRequest(requests); // loop over all settings and add flag override requests - for (AconfigdFlagInfo flag : aconfigFlagMap.values()) { - String value = - flag.getHasLocalOverride() - ? flag.getLocalFlagValue() - : flag.getServerFlagValue(); + final int numSettings = mSettings.size(); + int num_requests = 0; + for (int i = 0; i < numSettings; i++) { + String name = mSettings.keyAt(i); + Setting setting = mSettings.valueAt(i); + FlagOverrideToSync flag = + getFlagOverrideToSync(name, setting.getValue()); + if (flag == null) { + continue; + } + ++num_requests; AconfigdJavaUtils.writeFlagOverrideRequest( - requests, - flag.getPackageName(), - flag.getFlagName(), - value, - flag.getHasLocalOverride()); + requests, flag.packageName, flag.flagName, flag.flagValue, + flag.isLocal); } + Slog.i(LOG_TAG, num_requests + " flag override requests created"); + // mark sync has been done markerSetting.value = "true"; scheduleWriteIfNeededLocked(); @@ -602,14 +513,14 @@ final class SettingsState { return null; } } + } @GuardedBy("mLock") private void loadAconfigDefaultValuesLocked(List<String> filePaths) { for (String fileName : filePaths) { try (FileInputStream inputStream = new FileInputStream(fileName)) { - loadAconfigDefaultValues( - inputStream.readAllBytes(), mNamespaceDefaults, mAconfigDefaultFlags); + loadAconfigDefaultValues(inputStream.readAllBytes(), mNamespaceDefaults); } catch (IOException e) { Slog.e(LOG_TAG, "failed to read protobuf", e); } @@ -655,30 +566,21 @@ final class SettingsState { @VisibleForTesting @GuardedBy("mLock") - public static void loadAconfigDefaultValues( - byte[] fileContents, - @NonNull Map<String, Map<String, String>> defaultMap, - @NonNull Map<String, AconfigdFlagInfo> flagInfoDefault) { + public static void loadAconfigDefaultValues(byte[] fileContents, + @NonNull Map<String, Map<String, String>> defaultMap) { try { - parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents); + parsed_flags parsedFlags = + parsed_flags.parseFrom(fileContents); for (parsed_flag flag : parsedFlags.getParsedFlagList()) { if (!defaultMap.containsKey(flag.getNamespace())) { Map<String, String> defaults = new HashMap<>(); defaultMap.put(flag.getNamespace(), defaults); } - String fullFlagName = flag.getPackage() + "." + flag.getName(); - String flagName = flag.getNamespace() + "/" + fullFlagName; - String flagValue = flag.getState() == flag_state.ENABLED ? "true" : "false"; + String flagName = flag.getNamespace() + + "/" + flag.getPackage() + "." + flag.getName(); + String flagValue = flag.getState() == flag_state.ENABLED + ? "true" : "false"; defaultMap.get(flag.getNamespace()).put(flagName, flagValue); - if (!flagInfoDefault.containsKey(fullFlagName)) { - flagInfoDefault.put( - fullFlagName, - AconfigdFlagInfo.newBuilder() - .setPackageName(flag.getPackage()) - .setFlagName(flag.getName()) - .setDefaultFlagValue(flagValue) - .build()); - } } } catch (IOException e) { Slog.e(LOG_TAG, "failed to parse protobuf", e); @@ -1744,6 +1646,7 @@ final class SettingsState { } } } + mSettings.put(name, new Setting(name, value, defaultValue, packageName, tag, fromSystem, id, isPreservedInRestore)); diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java index ca1e4c10d339..e4898daf3cbf 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java @@ -27,11 +27,11 @@ import android.support.test.uiautomator.UiDevice; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.bedstead.enterprise.annotations.EnsureHasNoWorkProfile; +import com.android.bedstead.enterprise.annotations.EnsureHasWorkProfile; import com.android.bedstead.harrier.BedsteadJUnit4; import com.android.bedstead.harrier.DeviceState; -import com.android.bedstead.harrier.annotations.EnsureHasNoWorkProfile; import com.android.bedstead.harrier.annotations.EnsureHasSecondaryUser; -import com.android.bedstead.harrier.annotations.EnsureHasWorkProfile; import com.android.bedstead.harrier.annotations.RequireFeature; import com.android.bedstead.harrier.annotations.RequireRunOnInitialUser; import com.android.bedstead.harrier.annotations.RequireRunOnPrimaryUser; diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index 256b999ca6c5..244c8c4d99bc 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -24,13 +24,13 @@ import static junit.framework.Assert.fail; import android.aconfig.Aconfig; import android.aconfig.Aconfig.parsed_flag; import android.aconfig.Aconfig.parsed_flags; -import android.aconfigd.AconfigdFlagInfo; import android.os.Looper; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.Xml; import android.util.proto.ProtoOutputStream; +import com.android.providers.settings.SettingsState.FlagOverrideToSync; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -145,32 +145,16 @@ public class SettingsStateTest { .setState(Aconfig.flag_state.ENABLED) .setPermission(Aconfig.flag_permission.READ_WRITE)) .build(); - - AconfigdFlagInfo flag1 = AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag1") - .setDefaultFlagValue("false") - .build(); - AconfigdFlagInfo flag2 = AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag2") - .setDefaultFlagValue("true") - .build(); - Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>(); synchronized (lock) { Map<String, Map<String, String>> defaults = new HashMap<>(); - settingsState.loadAconfigDefaultValues( - flags.toByteArray(), defaults, flagInfoDefault); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); Map<String, String> namespaceDefaults = defaults.get("test_namespace"); assertEquals(2, namespaceDefaults.keySet().size()); assertEquals("false", namespaceDefaults.get("test_namespace/com.android.flags.flag1")); assertEquals("true", namespaceDefaults.get("test_namespace/com.android.flags.flag2")); } - - assertEquals(flag1, flagInfoDefault.get(flag1.getFullFlagName())); - assertEquals(flag2, flagInfoDefault.get(flag2.getFullFlagName())); } @Test @@ -181,8 +165,6 @@ public class SettingsStateTest { InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); - Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>(); - parsed_flags flags = parsed_flags .newBuilder() .addParsedFlag(parsed_flag @@ -195,8 +177,7 @@ public class SettingsStateTest { synchronized (lock) { Map<String, Map<String, String>> defaults = new HashMap<>(); - settingsState.loadAconfigDefaultValues( - flags.toByteArray(), defaults, flagInfoDefault); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); Map<String, String> namespaceDefaults = defaults.get("test_namespace"); assertEquals(null, namespaceDefaults); @@ -223,12 +204,10 @@ public class SettingsStateTest { .setState(Aconfig.flag_state.DISABLED) .setPermission(Aconfig.flag_permission.READ_WRITE)) .build(); - Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>(); synchronized (lock) { Map<String, Map<String, String>> defaults = new HashMap<>(); - settingsState.loadAconfigDefaultValues( - flags.toByteArray(), defaults, flagInfoDefault); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); settingsState.addAconfigDefaultValuesFromMap(defaults); settingsState.insertSettingLocked("test_namespace/com.android.flags.flag5", @@ -259,10 +238,8 @@ public class SettingsStateTest { @Test public void testInvalidAconfigProtoDoesNotCrash() { Map<String, Map<String, String>> defaults = new HashMap<>(); - Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>(); SettingsState settingsState = getSettingStateObject(); - settingsState.loadAconfigDefaultValues( - "invalid protobuf".getBytes(), defaults, flagInfoDefault); + settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes(), defaults); } @Test @@ -782,8 +759,6 @@ public class SettingsStateTest { Map<String, String> keyValues = Map.of("test_namespace/com.android.flags.flag3", "true"); - Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>(); - parsed_flags flags = parsed_flags .newBuilder() .addParsedFlag(parsed_flag @@ -799,8 +774,7 @@ public class SettingsStateTest { synchronized (mLock) { settingsState.loadAconfigDefaultValues( - flags.toByteArray(), - settingsState.getAconfigDefaultValues(), flagInfoDefault); + flags.toByteArray(), settingsState.getAconfigDefaultValues()); List<String> updates = settingsState.setSettingsLocked("test_namespace/", keyValues, packageName); assertEquals(1, updates.size()); @@ -866,13 +840,10 @@ public class SettingsStateTest { .setState(Aconfig.flag_state.DISABLED) .setPermission(Aconfig.flag_permission.READ_WRITE)) .build(); - Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>(); synchronized (mLock) { settingsState.loadAconfigDefaultValues( - flags.toByteArray(), - settingsState.getAconfigDefaultValues(), - flagInfoDefault); + flags.toByteArray(), settingsState.getAconfigDefaultValues()); List<String> updates = settingsState.setSettingsLocked("test_namespace/", keyValues, packageName); assertEquals(3, updates.size()); @@ -1002,12 +973,10 @@ public class SettingsStateTest { .setState(Aconfig.flag_state.DISABLED) .setPermission(Aconfig.flag_permission.READ_WRITE)) .build(); - Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>(); synchronized (lock) { Map<String, Map<String, String>> defaults = new HashMap<>(); - settingsState.loadAconfigDefaultValues( - flags.toByteArray(), defaults, flagInfoDefault); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); Map<String, String> namespaceDefaults = defaults.get("test_namespace"); assertEquals(1, namespaceDefaults.keySet().size()); settingsState.addAconfigDefaultValuesFromMap(defaults); @@ -1022,28 +991,22 @@ public class SettingsStateTest { "some_namespace/some_flag", "false") == null); // server override - AconfigdFlagInfo flag = settingsState.getFlagOverrideToSync( + FlagOverrideToSync flag = settingsState.getFlagOverrideToSync( "test_namespace/com.android.flags.flag1", "false"); assertTrue(flag != null); - assertEquals(flag.getPackageName(), "com.android.flags"); - assertEquals(flag.getFlagName(), "flag1"); - assertEquals("false", flag.getBootFlagValue()); - assertEquals("false", flag.getServerFlagValue()); - assertFalse(flag.getHasLocalOverride()); - assertNull(flag.getLocalFlagValue()); - assertEquals("false", flag.getDefaultFlagValue()); + assertEquals(flag.packageName, "com.android.flags"); + assertEquals(flag.flagName, "flag1"); + assertEquals(flag.flagValue, "false"); + assertEquals(flag.isLocal, false); // local override flag = settingsState.getFlagOverrideToSync( "device_config_overrides/test_namespace:com.android.flags.flag1", "false"); assertTrue(flag != null); - assertEquals(flag.getPackageName(), "com.android.flags"); - assertEquals(flag.getFlagName(), "flag1"); - assertEquals("false", flag.getLocalFlagValue()); - assertEquals("false", flag.getBootFlagValue()); - assertTrue(flag.getHasLocalOverride()); - assertNull(flag.getServerFlagValue()); - assertEquals("false", flag.getDefaultFlagValue()); + assertEquals(flag.packageName, "com.android.flags"); + assertEquals(flag.flagName, "flag1"); + assertEquals(flag.flagValue, "false"); + assertEquals(flag.isLocal, true); } @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -1057,25 +1020,18 @@ public class SettingsStateTest { InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); - Map<String, AconfigdFlagInfo> flags = new HashMap<>(); - AconfigdFlagInfo flag = AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag1") - .setBootFlagValue("true").build(); - flags.put("com.android.flags/flag1", flag); - synchronized (lock) { settingsState.insertSettingLocked("aconfigd_marker/bulk_synced", "false", null, false, "aconfig"); // first bulk sync - ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage(flags); + ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage(); assertTrue(requests != null); String value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue(); assertEquals("true", value); // send time should no longer bulk sync - requests = settingsState.handleBulkSyncToNewStorage(flags); + requests = settingsState.handleBulkSyncToNewStorage(); assertTrue(requests == null); value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue(); assertEquals("true", value); @@ -1091,200 +1047,21 @@ public class SettingsStateTest { InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); - Map<String, AconfigdFlagInfo> flags = new HashMap<>(); synchronized (lock) { settingsState.insertSettingLocked("aconfigd_marker/bulk_synced", "true", null, false, "aconfig"); // when aconfigd is off, should change the marker to false - ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage(flags); + ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage(); assertTrue(requests == null); String value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue(); assertEquals("false", value); // marker started with false value, after call, it should remain false - requests = settingsState.handleBulkSyncToNewStorage(flags); + requests = settingsState.handleBulkSyncToNewStorage(); assertTrue(requests == null); value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue(); assertEquals("false", value); } } - - @Test - public void testGetAllAconfigFlagsFromSettings() throws Exception { - final Object lock = new Object(); - final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile)); - os.print( - "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>" - + "<settings version=\"120\">" - + " <setting id=\"0\" name=\"test_namespace/com.android.flags.flag1\" " - + "value=\"false\" package=\"com.android.flags\" />" - + " <setting id=\"1\" name=\"device_config_overrides/test_namespace:com.android.flags.flag1\" " - + "value=\"true\" package=\"com.android.flags\" />" - + " <setting id=\"2\" name=\"device_config_overrides/test_namespace:com.android.flags.flag2\" " - + "value=\"true\" package=\"com.android.flags\" />" - + " <setting id=\"3\" name=\"test_namespace/com.android.flags.flag3\" " - + "value=\"true\" package=\"com.android.flags\" />" - + "</settings>"); - os.close(); - - int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); - - SettingsState settingsState = new SettingsState( - InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); - - Map<String, AconfigdFlagInfo> ret; - synchronized (lock) { - ret = settingsState.getAllAconfigFlagsFromSettings(); - } - - assertTrue(ret.isEmpty()); - - parsed_flags flags = - parsed_flags - .newBuilder() - .addParsedFlag( - parsed_flag - .newBuilder() - .setPackage("com.android.flags") - .setName("flag1") - .setNamespace("test_namespace") - .setDescription("test flag") - .addBug("12345678") - .setState(Aconfig.flag_state.DISABLED) - .setPermission(Aconfig.flag_permission.READ_WRITE)) - .addParsedFlag( - parsed_flag - .newBuilder() - .setPackage("com.android.flags") - .setName("flag2") - .setNamespace("test_namespace") - .setDescription("test flag") - .addBug("12345678") - .setState(Aconfig.flag_state.DISABLED) - .setPermission(Aconfig.flag_permission.READ_WRITE)) - .addParsedFlag( - parsed_flag - .newBuilder() - .setPackage("com.android.flags") - .setName("flag3") - .setNamespace("test_namespace") - .setDescription("test flag") - .addBug("12345678") - .setState(Aconfig.flag_state.DISABLED) - .setPermission(Aconfig.flag_permission.READ_WRITE)) - .build(); - - Map<String, Map<String, String>> defaults = new HashMap<>(); - Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>(); - synchronized (lock) { - settingsState.loadAconfigDefaultValues( - flags.toByteArray(), defaults, flagInfoDefault); - settingsState.addAconfigDefaultValuesFromMap(defaults); - ret = settingsState.getAllAconfigFlagsFromSettings(); - } - - AconfigdFlagInfo expectedFlag1 = - AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag1") - .setServerFlagValue("false") - .setLocalFlagValue("true") - .setDefaultFlagValue("false") - .setBootFlagValue("true") - .setHasServerOverride(true) - .setHasLocalOverride(true) - .setIsReadWrite(false) - .build(); - - AconfigdFlagInfo expectedFlag2 = - AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag2") - .setLocalFlagValue("true") - .setDefaultFlagValue("false") - .setBootFlagValue("true") - .setHasLocalOverride(true) - .setHasServerOverride(false) - .setIsReadWrite(false) - .build(); - - - AconfigdFlagInfo expectedFlag3 = - AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag3") - .setServerFlagValue("true") - .setBootFlagValue("true") - .setDefaultFlagValue("false") - .setHasServerOverride(true) - .setIsReadWrite(false) - .build(); - - assertEquals(expectedFlag1, ret.get("com.android.flags.flag1")); - assertEquals(expectedFlag2, ret.get("com.android.flags.flag2")); - assertEquals(expectedFlag3, ret.get("com.android.flags.flag3")); - } - - @Test - public void testCompareFlagValueInNewStorage() { - int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); - Object lock = new Object(); - SettingsState settingsState = - new SettingsState( - InstrumentationRegistry.getContext(), - lock, - mSettingsFile, - configKey, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, - Looper.getMainLooper()); - - AconfigdFlagInfo defaultFlag1 = - AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag1") - .setDefaultFlagValue("false") - .build(); - - AconfigdFlagInfo settingFlag1 = - AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag1") - .setServerFlagValue("true") - .setHasServerOverride(true) - .build(); - - AconfigdFlagInfo expectedFlag1 = - AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag1") - .setBootFlagValue("true") - .setServerFlagValue("true") - .setDefaultFlagValue("false") - .setHasServerOverride(true) - .build(); - - Map<String, AconfigdFlagInfo> settingMap = new HashMap<>(); - Map<String, AconfigdFlagInfo> aconfigdMap = new HashMap<>(); - Map<String, AconfigdFlagInfo> defaultMap = new HashMap<>(); - - defaultMap.put("com.android.flags.flag1", defaultFlag1); - settingMap.put("com.android.flags.flag1", settingFlag1); - aconfigdMap.put("com.android.flags.flag1", expectedFlag1); - - int ret = settingsState.compareFlagValueInNewStorage(settingMap, defaultMap, aconfigdMap); - assertEquals(0, ret); - - AconfigdFlagInfo defaultFlag2 = - AconfigdFlagInfo.newBuilder() - .setPackageName("com.android.flags") - .setFlagName("flag2") - .setDefaultFlagValue("false") - .build(); - defaultMap.put("com.android.flags.flag2", defaultFlag2); - - ret = settingsState.compareFlagValueInNewStorage(settingMap, defaultMap, aconfigdMap); - assertEquals(1, ret); - } } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 1df9c88e48ac..63a52d624ca7 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -177,16 +177,9 @@ flag { } flag { - name: "notification_throttle_hun" - namespace: "systemui" - description: "During notification avalanche, throttle HUNs showing in fast succession." - bug: "307288824" -} - -flag { name: "notification_avalanche_throttle_hun" namespace: "systemui" - description: "(currently unused) During notification avalanche, throttle HUNs showing in fast succession." + description: "During notification avalanche, throttle HUNs showing in fast succession." bug: "307288824" } @@ -997,6 +990,13 @@ flag { } flag { + name: "glanceable_hub_fullscreen_swipe" + namespace: "systemui" + description: "Increase swipe area for gestures to bring in glanceable hub" + bug: "339665673" +} + +flag { name: "glanceable_hub_shortcut_button" namespace: "systemui" description: "Shows a button over the dream and lock screen to open the glanceable hub" @@ -1026,3 +1026,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "notification_media_manager_background_execution" + namespace: "systemui" + description: "Decide whether to execute binder calls in background thread" + bug: "336612071" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/platform/DensityAwareComposeView.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/platform/DensityAwareComposeView.kt deleted file mode 100644 index dff8753fd880..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/ui/platform/DensityAwareComposeView.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.compose.ui.platform - -import android.content.Context -import android.content.res.Configuration -import android.util.AttributeSet -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.AbstractComposeView - -/** - * A ComposeView that recreates its composition if the display size or font scale was changed. - * - * TODO(b/317317814): Remove this workaround. - */ -class DensityAwareComposeView(context: Context) : OpenComposeView(context) { - private var lastDensityDpi: Int = -1 - private var lastFontScale: Float = -1f - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - - val configuration = context.resources.configuration - lastDensityDpi = configuration.densityDpi - lastFontScale = configuration.fontScale - } - - override fun dispatchConfigurationChanged(newConfig: Configuration) { - super.dispatchConfigurationChanged(newConfig) - - // If the density or font scale changed, we dispose then recreate the composition. Note that - // we do this here after dispatching the new configuration to children (instead of doing - // this in onConfigurationChanged()) because the new configuration should first be - // dispatched to the AndroidComposeView that holds the current density before we recreate - // the composition. - val densityDpi = newConfig.densityDpi - val fontScale = newConfig.fontScale - if (densityDpi != lastDensityDpi || fontScale != lastFontScale) { - lastDensityDpi = densityDpi - lastFontScale = fontScale - - disposeComposition() - if (isAttachedToWindow) { - createComposition() - } - } - } -} - -/** A fork of [androidx.compose.ui.platform.ComposeView] that is open and can be subclassed. */ -open class OpenComposeView -internal constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : - AbstractComposeView(context, attrs, defStyleAttr) { - - private val content = mutableStateOf<(@Composable () -> Unit)?>(null) - - @Suppress("RedundantVisibilityModifier") - protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false - - @Composable - override fun Content() { - content.value?.invoke() - } - - override fun getAccessibilityClassName(): CharSequence { - return javaClass.name - } - - /** - * Set the Jetpack Compose UI content for this view. Initial composition will occur when the - * view becomes attached to a window or when [createComposition] is called, whichever comes - * first. - */ - fun setContent(content: @Composable () -> Unit) { - shouldCreateCompositionOnAttachedToWindow = true - this.content.value = content - if (isAttachedToWindow) { - createComposition() - } - } -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index feb1f5b17bef..a90f82eda1af 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.CommunalSwipeDetector +import com.android.compose.animation.scene.DefaultSwipeDetector import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher @@ -35,6 +37,7 @@ import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.observableTransitionState import com.android.compose.animation.scene.transitions import com.android.systemui.Flags +import com.android.systemui.Flags.glanceableHubFullscreenSwipe import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys import com.android.systemui.communal.ui.compose.extensions.allowGestures @@ -108,6 +111,8 @@ fun CommunalContainer( ) } + val detector = remember { CommunalSwipeDetector() } + DisposableEffect(state) { val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope) dataSourceDelegator.setDelegate(dataSource) @@ -121,13 +126,25 @@ fun CommunalContainer( onDispose { viewModel.setTransitionState(null) } } + val swipeSourceDetector = + if (glanceableHubFullscreenSwipe()) { + detector + } else { + FixedSizeEdgeDetector(dimensionResource(id = R.dimen.communal_gesture_initiation_width)) + } + + val swipeDetector = + if (glanceableHubFullscreenSwipe()) { + detector + } else { + DefaultSwipeDetector + } + SceneTransitionLayout( state = state, modifier = modifier.fillMaxSize(), - swipeSourceDetector = - FixedSizeEdgeDetector( - dimensionResource(id = R.dimen.communal_gesture_initiation_width) - ), + swipeSourceDetector = swipeSourceDetector, + swipeDetector = swipeDetector, ) { scene( CommunalScenes.Blank, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 8ee1e1f7f504..1f7f07bb072d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -23,11 +23,14 @@ import android.util.SizeF import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS import android.widget.FrameLayout +import androidx.annotation.VisibleForTesting import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -162,7 +165,6 @@ fun CommunalHub( var removeButtonCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } var toolbarSize: IntSize? by remember { mutableStateOf(null) } var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } - var isDraggingToRemove by remember { mutableStateOf(false) } val gridState = rememberLazyGridState() val contentListState = rememberContentListState(widgetConfigurator, communalContent, viewModel) val reorderingWidgets by viewModel.reorderingWidgets.collectAsStateWithLifecycle() @@ -250,12 +252,11 @@ fun CommunalHub( contentOffset = contentOffset, setGridCoordinates = { gridCoordinates = it }, updateDragPositionForRemove = { offset -> - isDraggingToRemove = - isPointerWithinCoordinates( - offset = gridCoordinates?.let { it.positionInWindow() + offset }, - containerToCheck = removeButtonCoordinates - ) - isDraggingToRemove + isPointerWithinEnabledRemoveButton( + removeEnabled = removeButtonEnabled, + offset = gridCoordinates?.let { it.positionInWindow() + offset }, + containerToCheck = removeButtonCoordinates + ) }, gridState = gridState, contentListState = contentListState, @@ -446,6 +447,14 @@ private fun BoxScope.CommunalHubLazyGrid( val selected by remember(index) { derivedStateOf { list[index].key == selectedKey.value } } DraggableItem( + modifier = + if (dragDropState.draggingItemIndex == index) { + Modifier + } else { + Modifier.animateItem( + placementSpec = spring(stiffness = Spring.StiffnessMediumLow) + ) + }, dragDropState = dragDropState, selected = selected, enabled = list[index].isWidgetContent(), @@ -1200,11 +1209,13 @@ private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingIn * Check whether the pointer position that the item is being dragged at is within the coordinates of * the remove button in the toolbar. Returns true if the item is removable. */ -private fun isPointerWithinCoordinates( +@VisibleForTesting +fun isPointerWithinEnabledRemoveButton( + removeEnabled: Boolean, offset: Offset?, containerToCheck: LayoutCoordinates? ): Boolean { - if (offset == null || containerToCheck == null) { + if (!removeEnabled || offset == null || containerToCheck == null) { return false } val container = containerToCheck.boundsInWindow() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index c26259f3287c..ee3ffce27d51 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -126,7 +126,8 @@ fun SceneScope.HeadsUpNotificationSpace( " size=${coordinates.size}" + " bounds=$boundsInWindow" } - viewModel.onHeadsUpTopChanged(boundsInWindow.top) + // Note: boundsInWindow doesn't scroll off the screen + stackScrollView.setHeadsUpTop(boundsInWindow.top) } ) { content {} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt index ae53d56e331a..7ca68fb40b61 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt @@ -18,8 +18,8 @@ package com.android.systemui.notifications.ui.composable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -75,7 +75,7 @@ constructor( OverlayShade( modifier = modifier, viewModel = overlayShadeViewModel, - horizontalArrangement = Arrangement.Start, + horizontalArrangement = Arrangement.End, lockscreenContent = lockscreenContent, ) { Column { @@ -95,7 +95,7 @@ constructor( shouldPunchHoleBehindScrim = false, shouldFillMaxSize = false, shadeMode = ShadeMode.Dual, - modifier = Modifier.width(416.dp), + modifier = Modifier.fillMaxWidth(), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index 975829ab3760..efda4cd3638e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -17,17 +17,28 @@ package com.android.systemui.scene.ui.composable import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.IntOffset import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneFloatAsState +import com.android.internal.policy.SystemBarUtils import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace import com.android.systemui.qs.ui.composable.QuickSettings +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.viewmodel.GoneSceneViewModel +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow @@ -39,6 +50,8 @@ import kotlinx.coroutines.flow.StateFlow class GoneScene @Inject constructor( + private val notificationStackScrolLView: Lazy<NotificationScrollView>, + private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, private val viewModel: GoneSceneViewModel, ) : ComposableScene { override val key = Scenes.Gone @@ -55,5 +68,28 @@ constructor( key = QuickSettings.SharedValues.TilesSquishiness, ) Spacer(modifier.fillMaxSize()) + HeadsUpNotificationStack( + stackScrollView = notificationStackScrolLView.get(), + viewModel = notificationsPlaceholderViewModel + ) } } + +@Composable +private fun SceneScope.HeadsUpNotificationStack( + stackScrollView: NotificationScrollView, + viewModel: NotificationsPlaceholderViewModel, +) { + val context = LocalContext.current + val density = LocalDensity.current + val statusBarHeight = SystemBarUtils.getStatusBarHeight(context) + val headsUpPadding = + with(density) { dimensionResource(id = R.dimen.heads_up_status_bar_padding).roundToPx() } + + HeadsUpNotificationSpace( + stackScrollView = stackScrollView, + viewModel = viewModel, + modifier = + Modifier.absoluteOffset { IntOffset(x = 0, y = statusBarHeight + headsUpPadding) } + ) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index f5a0ef2adde7..3255b0805fdb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -22,6 +22,7 @@ import com.android.systemui.scene.ui.composable.transitions.lockscreenToQuickSet import com.android.systemui.scene.ui.composable.transitions.lockscreenToShadeTransition import com.android.systemui.scene.ui.composable.transitions.lockscreenToSplitShadeTransition import com.android.systemui.scene.ui.composable.transitions.shadeToQuickSettingsTransition +import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.Shade /** @@ -102,4 +103,10 @@ val SceneContainerTransitions = transitions { y = { Shade.Dimensions.ScrimOverscrollLimit } ) } + overscroll(Scenes.NotificationsShade, Orientation.Vertical) { + translate(OverlayShade.Elements.Panel, y = { OverlayShade.Dimensions.OverscrollLimit }) + } + overscroll(Scenes.QuickSettingsShade, Orientation.Vertical) { + translate(OverlayShade.Elements.Panel, y = { OverlayShade.Dimensions.OverscrollLimit }) + } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt index a6b268d59bf4..6b3b7602050d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt @@ -50,8 +50,7 @@ fun TransitionBuilder.toNotificationsShadeTransition( } } - translate(OverlayShade.Elements.PanelBackground, Edge.Top) - translate(Notifications.Elements.NotificationScrim, Edge.Top) + translate(OverlayShade.Elements.Panel, Edge.Top) fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt index 2baaecf47ec5..ec2f14f34dba 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt @@ -48,7 +48,7 @@ fun TransitionBuilder.toQuickSettingsShadeTransition( } } - translate(OverlayShade.Elements.PanelBackground, Edge.Top) + translate(OverlayShade.Elements.Panel, Edge.Top) fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 34cc6769eb40..a7302061d02f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -79,7 +79,10 @@ fun SceneScope.OverlayShade( }, horizontalArrangement = horizontalArrangement, ) { - Panel(modifier = Modifier.panelSize(), content = content) + Panel( + modifier = Modifier.element(OverlayShade.Elements.Panel).panelSize(), + content = content + ) } } } @@ -138,6 +141,7 @@ private fun Modifier.panelSize(): Modifier { object OverlayShade { object Elements { val Scrim = ElementKey("OverlayShadeScrim", scenePicker = LowestZIndexScenePicker) + val Panel = ElementKey("OverlayShadePanel", scenePicker = LowestZIndexScenePicker) val PanelBackground = ElementKey("OverlayShadePanelBackground", scenePicker = LowestZIndexScenePicker) } @@ -153,6 +157,7 @@ object OverlayShade { val PanelCornerRadius = 46.dp val PanelWidthMedium = 390.dp val PanelWidthLarge = 474.dp + val OverscrollLimit = 100f } object Shapes { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 271eb9601dbd..fbf91b702fb9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -68,9 +68,7 @@ fun VolumeSlider( state.a11yClickDescription?.let { customActions = listOf( - CustomAccessibilityAction( - it, - ) { + CustomAccessibilityAction(it) { onIconTapped() true } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt index ac5004e16a3b..580aba586ee1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.ui.composable +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -56,17 +57,19 @@ fun VolumePanelComposeScope.HorizontalVolumePanelContent( with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) } } } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(spacing), - ) { - for (component in layout.footerComponents) { - AnimatedVisibility( - visible = component.isVisible, - modifier = Modifier.weight(1f), - ) { - with(component.component as ComposeVolumePanelUiComponent) { - Content(Modifier) + AnimatedContent( + targetState = layout.footerComponents, + label = "FooterComponentAnimation", + ) { footerComponents -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing), + ) { + for (component in footerComponents) { + if (component.isVisible) { + with(component.component as ComposeVolumePanelUiComponent) { + Content(Modifier.weight(1f)) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt index 9ea20b9da4b6..6349c1406a12 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.ui.composable +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -50,26 +51,27 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) } } } - if (layout.footerComponents.isNotEmpty()) { + + AnimatedContent( + targetState = layout.footerComponents, + label = "FooterComponentAnimation", + ) { footerComponents -> Row( modifier = Modifier.fillMaxWidth().wrapContentHeight(), horizontalArrangement = Arrangement.spacedBy(if (isLargeScreen) 28.dp else 20.dp), ) { val visibleComponentsCount = - layout.footerComponents.fastSumBy { if (it.isVisible) 1 else 0 } + footerComponents.fastSumBy { if (it.isVisible) 1 else 0 } // Center footer component if there is only one present if (visibleComponentsCount == 1) { Spacer(modifier = Modifier.weight(0.5f)) } - for (component in layout.footerComponents) { - AnimatedVisibility( - visible = component.isVisible, - modifier = Modifier.weight(1f), - ) { + for (component in footerComponents) { + if (component.isVisible) { with(component.component as ComposeVolumePanelUiComponent) { - Content(Modifier) + Content(Modifier.weight(1f)) } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt new file mode 100644 index 000000000000..7be34cabfaf8 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.animation.scene + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import kotlin.math.abs + +private const val TRAVEL_RATIO_THRESHOLD = .5f + +/** + * {@link CommunalSwipeDetector} provides an implementation of {@link SwipeDetector} and {@link + * SwipeSourceDetector} to enable fullscreen swipe handling to transition to and from the glanceable + * hub. + */ +class CommunalSwipeDetector(private var lastDirection: SwipeSource? = null) : + SwipeSourceDetector, SwipeDetector { + override fun source( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation + ): SwipeSource? { + return lastDirection + } + + override fun detectSwipe(change: PointerInputChange): Boolean { + if (change.positionChange().x > 0) { + lastDirection = Edge.Left + } else { + lastDirection = Edge.Right + } + + // Determine whether the ratio of the distance traveled horizontally to the distance + // traveled vertically exceeds the threshold. + return abs(change.positionChange().x / change.positionChange().y) > TRAVEL_RATIO_THRESHOLD + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 0fc0053ce4a1..3cc8431cd87e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -72,6 +72,7 @@ internal fun Modifier.multiPointerDraggable( enabled: () -> Boolean, startDragImmediately: (startedPosition: Offset) -> Boolean, onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + swipeDetector: SwipeDetector = DefaultSwipeDetector, ): Modifier = this.then( MultiPointerDraggableElement( @@ -79,6 +80,7 @@ internal fun Modifier.multiPointerDraggable( enabled, startDragImmediately, onDragStarted, + swipeDetector, ) ) @@ -88,6 +90,7 @@ private data class MultiPointerDraggableElement( private val startDragImmediately: (startedPosition: Offset) -> Boolean, private val onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + private val swipeDetector: SwipeDetector, ) : ModifierNodeElement<MultiPointerDraggableNode>() { override fun create(): MultiPointerDraggableNode = MultiPointerDraggableNode( @@ -95,6 +98,7 @@ private data class MultiPointerDraggableElement( enabled = enabled, startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, + swipeDetector = swipeDetector, ) override fun update(node: MultiPointerDraggableNode) { @@ -102,6 +106,7 @@ private data class MultiPointerDraggableElement( node.enabled = enabled node.startDragImmediately = startDragImmediately node.onDragStarted = onDragStarted + node.swipeDetector = swipeDetector } } @@ -111,6 +116,7 @@ internal class MultiPointerDraggableNode( var startDragImmediately: (startedPosition: Offset) -> Boolean, var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + var swipeDetector: SwipeDetector = DefaultSwipeDetector, ) : PointerInputModifierNode, DelegatingNode(), @@ -199,6 +205,7 @@ internal class MultiPointerDraggableNode( onDragCancel = { controller -> controller.onStop(velocity = 0f, canChangeScene = true) }, + swipeDetector = swipeDetector ) } catch (exception: CancellationException) { // If the coroutine scope is active, we can just restart the drag cycle. @@ -226,7 +233,8 @@ internal class MultiPointerDraggableNode( (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, onDrag: (controller: DragController, change: PointerInputChange, dragAmount: Float) -> Unit, onDragEnd: (controller: DragController) -> Unit, - onDragCancel: (controller: DragController) -> Unit + onDragCancel: (controller: DragController) -> Unit, + swipeDetector: SwipeDetector, ) { // Wait for a consumable event in [PointerEventPass.Main] pass val consumablePointer = awaitConsumableEvent().changes.first() @@ -238,8 +246,10 @@ internal class MultiPointerDraggableNode( consumablePointer } else { val onSlopReached = { change: PointerInputChange, over: Float -> - change.consume() - overSlop = over + if (swipeDetector.detectSwipe(change)) { + change.consume() + overSlop = over + } } // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once it diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 11e711ace971..cf8c5841f797 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -55,6 +55,7 @@ fun SceneTransitionLayout( state: SceneTransitionLayoutState, modifier: Modifier = Modifier, swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, + swipeDetector: SwipeDetector = DefaultSwipeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, scenes: SceneTransitionLayoutScope.() -> Unit, ) { @@ -62,6 +63,7 @@ fun SceneTransitionLayout( state, modifier, swipeSourceDetector, + swipeDetector, transitionInterceptionThreshold, onLayoutImpl = null, scenes, @@ -95,6 +97,7 @@ fun SceneTransitionLayout( transitions: SceneTransitions, modifier: Modifier = Modifier, swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, + swipeDetector: SwipeDetector = DefaultSwipeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, scenes: SceneTransitionLayoutScope.() -> Unit, @@ -111,6 +114,7 @@ fun SceneTransitionLayout( state, modifier, swipeSourceDetector, + swipeDetector, transitionInterceptionThreshold, scenes, ) @@ -467,6 +471,7 @@ internal fun SceneTransitionLayoutForTesting( state: SceneTransitionLayoutState, modifier: Modifier = Modifier, swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, + swipeDetector: SwipeDetector = DefaultSwipeDetector, transitionInterceptionThreshold: Float = 0f, onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null, scenes: SceneTransitionLayoutScope.() -> Unit, @@ -502,5 +507,5 @@ internal fun SceneTransitionLayoutForTesting( layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold } - layoutImpl.Content(modifier) + layoutImpl.Content(modifier, swipeDetector) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 7856498aa365..c614265e2ae1 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -185,14 +185,14 @@ internal class SceneTransitionLayoutImpl( } @Composable - internal fun Content(modifier: Modifier) { + internal fun Content(modifier: Modifier, swipeDetector: SwipeDetector) { Box( modifier // Handle horizontal and vertical swipes on this layout. // Note: order here is important and will give a slight priority to the vertical // swipes. - .swipeToScene(horizontalDraggableHandler) - .swipeToScene(verticalDraggableHandler) + .swipeToScene(horizontalDraggableHandler, swipeDetector) + .swipeToScene(verticalDraggableHandler, swipeDetector) .then(LayoutElement(layoutImpl = this)) ) { LookaheadScope { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt new file mode 100644 index 000000000000..54ee78366875 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.animation.scene + +import androidx.compose.runtime.Stable +import androidx.compose.ui.input.pointer.PointerInputChange + +/** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */ +@Stable +interface SwipeDetector { + /** + * Invoked on changes to pointer input. Returns {@code true} if a swipe has been recognized, + * {@code false} otherwise. + */ + fun detectSwipe(change: PointerInputChange): Boolean +} + +val DefaultSwipeDetector = PassthroughSwipeDetector() + +/** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */ +class PassthroughSwipeDetector : SwipeDetector { + override fun detectSwipe(change: PointerInputChange): Boolean { + // Simply accept all changes as a swipe + return true + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index b618369c2369..171e2430c004 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -31,14 +31,18 @@ import androidx.compose.ui.unit.IntSize * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state. */ @Stable -internal fun Modifier.swipeToScene(draggableHandler: DraggableHandlerImpl): Modifier { - return this.then(SwipeToSceneElement(draggableHandler)) +internal fun Modifier.swipeToScene( + draggableHandler: DraggableHandlerImpl, + swipeDetector: SwipeDetector +): Modifier { + return this.then(SwipeToSceneElement(draggableHandler, swipeDetector)) } private data class SwipeToSceneElement( val draggableHandler: DraggableHandlerImpl, + val swipeDetector: SwipeDetector ) : ModifierNodeElement<SwipeToSceneNode>() { - override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler) + override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler, swipeDetector) override fun update(node: SwipeToSceneNode) { node.draggableHandler = draggableHandler @@ -47,6 +51,7 @@ private data class SwipeToSceneElement( private class SwipeToSceneNode( draggableHandler: DraggableHandlerImpl, + swipeDetector: SwipeDetector, ) : DelegatingNode(), PointerInputModifierNode { private val delegate = delegate( @@ -55,6 +60,7 @@ private class SwipeToSceneNode( enabled = ::enabled, startDragImmediately = ::startDragImmediately, onDragStarted = draggableHandler::onDragStarted, + swipeDetector = swipeDetector, ) ) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index aa6d1130fc2a..4bb643f8b89e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalViewConfiguration @@ -346,4 +347,69 @@ class MultiPointerDraggableTest { continueDraggingDown() assertThat(stopped).isTrue() } + + @Test + fun multiPointerSwipeDetectorInteraction() { + val size = 200f + val middle = Offset(size / 2f, size / 2f) + + var started = false + + var capturedChange: PointerInputChange? = null + var swipeConsume = false + + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + Box( + Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .multiPointerDraggable( + orientation = Orientation.Vertical, + enabled = { true }, + startDragImmediately = { false }, + swipeDetector = + object : SwipeDetector { + override fun detectSwipe(change: PointerInputChange): Boolean { + capturedChange = change + return swipeConsume + } + }, + onDragStarted = { _, _, _ -> + started = true + object : DragController { + override fun onDrag(delta: Float) {} + + override fun onStop(velocity: Float, canChangeScene: Boolean) {} + } + }, + ) + ) {} + } + + fun startDraggingDown() { + rule.onRoot().performTouchInput { + down(middle) + moveBy(Offset(0f, touchSlop)) + } + } + + fun continueDraggingDown() { + rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) } + } + + startDraggingDown() + assertThat(capturedChange).isNotNull() + capturedChange = null + assertThat(started).isFalse() + + swipeConsume = true + continueDraggingDown() + assertThat(capturedChange).isNotNull() + capturedChange = null + + continueDraggingDown() + assertThat(capturedChange).isNull() + + assertThat(started).isTrue() + } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java index 165e9728b9d7..de9baa59b2b7 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java +++ b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java @@ -79,6 +79,21 @@ public class Assert { } } + /** + * Asserts that the current thread is the same as the given thread, or that the current thread + * is the test thread. + * @param expected The looper we expected to be running on + */ + public static void isCurrentThread(Looper expected) { + if (!expected.isCurrentThread() + && (sTestThread == null || sTestThread != Thread.currentThread())) { + throw new IllegalStateException("Called on wrong thread thread." + + " wanted " + expected.getThread().getName() + + " but instead got Thread.currentThread()=" + + Thread.currentThread().getName()); + } + } + public static void isNotMainThread() { if (sMainLooper.isCurrentThread() && (sTestThread == null || sTestThread == Thread.currentThread())) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt index e39ad4f0b405..a676c7db4290 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt @@ -25,15 +25,18 @@ import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.brightness.data.model.LinearBrightness +import com.android.systemui.brightness.shared.model.LinearBrightness import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.log.core.FakeLogBuffer +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -74,6 +77,8 @@ class ScreenBrightnessDisplayManagerRepositoryTest : SysuiTestCase() { ScreenBrightnessDisplayManagerRepository( displayId, displayManager, + FakeLogBuffer.Factory.create(), + mock<TableLogBuffer>(), kosmos.applicationCoroutineScope, kosmos.testDispatcher, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt index 33c44f8a331e..b6616bf0c8de 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt @@ -20,13 +20,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.display.BrightnessUtils import com.android.systemui.SysuiTestCase -import com.android.systemui.brightness.data.model.LinearBrightness import com.android.systemui.brightness.data.repository.fakeScreenBrightnessRepository import com.android.systemui.brightness.data.repository.screenBrightnessRepository -import com.android.systemui.brightness.shared.GammaBrightness +import com.android.systemui.brightness.shared.model.GammaBrightness +import com.android.systemui.brightness.shared.model.LinearBrightness import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent @@ -41,7 +44,14 @@ class ScreenBrightnessInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() - private val underTest = ScreenBrightnessInteractor(kosmos.screenBrightnessRepository) + private val underTest = + with(kosmos) { + ScreenBrightnessInteractor( + screenBrightnessRepository, + applicationCoroutineScope, + mock<TableLogBuffer>() + ) + } @Test fun gammaBrightness() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt index 0058ee4a9c4e..8402676dbd6b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt @@ -20,15 +20,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.display.BrightnessUtils import com.android.systemui.SysuiTestCase -import com.android.systemui.brightness.data.model.LinearBrightness import com.android.systemui.brightness.data.repository.fakeScreenBrightnessRepository import com.android.systemui.brightness.domain.interactor.brightnessPolicyEnforcementInteractor import com.android.systemui.brightness.domain.interactor.screenBrightnessInteractor -import com.android.systemui.brightness.shared.GammaBrightness +import com.android.systemui.brightness.shared.model.GammaBrightness +import com.android.systemui.brightness.shared.model.LinearBrightness import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.testKosmos @@ -52,6 +53,7 @@ class BrightnessSliderViewModelTest : SysuiTestCase() { BrightnessSliderViewModel( screenBrightnessInteractor, brightnessPolicyEnforcementInteractor, + applicationCoroutineScope, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt index 45e7d8a43c2a..fd0bf4dae198 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt @@ -41,6 +41,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() { private val underTest by lazy { CommunalSceneRepositoryImpl( kosmos.applicationCoroutineScope, + kosmos.applicationCoroutineScope, kosmos.sceneDataSource, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/compose/CommunalHubUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/compose/CommunalHubUtilsTest.kt new file mode 100644 index 000000000000..643063e738da --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/compose/CommunalHubUtilsTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.compose + +import android.testing.TestableLooper +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@SmallTest +class CommunalHubUtilsTest : SysuiTestCase() { + @Test + fun isPointerWithinEnabledRemoveButton_ensureDisabledStatePriority() { + assertThat( + isPointerWithinEnabledRemoveButton(false, mock<Offset>(), mock<LayoutCoordinates>()) + ) + .isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index a3a49524e15f..ee8a22c17455 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -397,6 +397,9 @@ class DreamOverlayServiceTest : SysuiTestCase() { verify(mStateController).setOverlayActive(false) verify(mStateController).setLowLightActive(false) verify(mStateController).setEntryAnimationsFinished(false) + + // Verify touch monitor destroyed + verify(mTouchMonitor).destroy() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt index 723f6a2bfff4..9300db9a24c8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt @@ -16,29 +16,40 @@ package com.android.systemui.dreams.homecontrols import android.app.Activity +import android.content.Intent +import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL +import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_DREAM +import android.service.controls.ControlsProviderService.EXTRA_CONTROLS_SURFACE +import android.window.TaskFragmentInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.controls.settings.FakeControlsSettingsRepository import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope -import com.android.systemui.log.core.FakeLogBuffer.Factory.Companion.create import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.whenever import com.android.systemui.util.wakelock.WakeLockFake import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations - +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class HomeControlsDreamServiceTest : SysuiTestCase() { @@ -46,31 +57,38 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder - private lateinit var fakeWakeLock: WakeLockFake - - @Mock private lateinit var taskFragmentComponentFactory: TaskFragmentComponent.Factory - @Mock private lateinit var taskFragmentComponent: TaskFragmentComponent - @Mock private lateinit var activity: Activity + private val fakeWakeLock = WakeLockFake() + private val fakeWakeLockBuilder by lazy { + WakeLockFake.Builder(context).apply { setWakeLock(fakeWakeLock) } + } + + private val taskFragmentComponent = mock<TaskFragmentComponent>() + private val activity = mock<Activity>() + private val onCreateCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>() + private val onInfoChangedCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>() + private val hideCallback = argumentCaptor<() -> Unit>() + private val dreamServiceDelegate = + mock<DreamServiceDelegate> { on { getActivity(any()) } doReturn activity } + + private val taskFragmentComponentFactory = + mock<TaskFragmentComponent.Factory> { + on { + create( + activity = eq(activity), + onCreateCallback = onCreateCallback.capture(), + onInfoChangedCallback = onInfoChangedCallback.capture(), + hide = hideCallback.capture(), + ) + } doReturn taskFragmentComponent + } - private lateinit var underTest: HomeControlsDreamService + private val underTest: HomeControlsDreamService by lazy { buildService() } @Before - fun setup() = - with(kosmos) { - MockitoAnnotations.initMocks(this@HomeControlsDreamServiceTest) - whenever(taskFragmentComponentFactory.create(any(), any(), any(), any())) - .thenReturn(taskFragmentComponent) - - fakeWakeLock = WakeLockFake() - fakeWakeLockBuilder = WakeLockFake.Builder(context) - fakeWakeLockBuilder.setWakeLock(fakeWakeLock) - - whenever(controlsComponent.getControlsListingController()) - .thenReturn(Optional.of(controlsListingController)) - - underTest = buildService { activity } - } + fun setup() { + whenever(kosmos.controlsComponent.getControlsListingController()) + .thenReturn(Optional.of(kosmos.controlsListingController)) + } @Test fun testOnAttachedToWindowCreatesTaskFragmentComponent() = @@ -90,9 +108,12 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { @Test fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() = testScope.runTest { - underTest = buildService { null } + val serviceWithNullActivity = + buildService( + mock<DreamServiceDelegate> { on { getActivity(underTest) } doReturn null } + ) - underTest.onAttachedToWindow() + serviceWithNullActivity.onAttachedToWindow() verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any()) } @@ -102,6 +123,7 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { underTest.onAttachedToWindow() assertThat(fakeWakeLock.isHeld).isTrue() } + @Test fun testDetachWindow_wakeLockCanBeReleased() = testScope.runTest { @@ -112,14 +134,60 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { assertThat(fakeWakeLock.isHeld).isFalse() } - private fun buildService(activityProvider: DreamActivityProvider): HomeControlsDreamService = + @Test + fun testFinishesDreamWithoutRestartingActivityWhenNotRedirectingWakes() = + testScope.runTest { + whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(false) + underTest.onAttachedToWindow() + onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>()) + verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher()) + + // Task fragment becomes empty + onInfoChangedCallback.firstValue.invoke( + mock<TaskFragmentInfo> { on { isEmpty } doReturn true } + ) + advanceUntilIdle() + // Dream is finished and activity is not restarted + verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher()) + verify(dreamServiceDelegate, never()).wakeUp(any()) + verify(dreamServiceDelegate).finish(any()) + } + + @Test + fun testRestartsActivityWhenRedirectingWakes() = + testScope.runTest { + whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(true) + underTest.onAttachedToWindow() + onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>()) + verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher()) + + // Task fragment becomes empty + onInfoChangedCallback.firstValue.invoke( + mock<TaskFragmentInfo> { on { isEmpty } doReturn true } + ) + advanceUntilIdle() + // Activity is restarted instead of finishing the dream. + verify(taskFragmentComponent, times(2)).startActivityInTaskFragment(intentMatcher()) + verify(dreamServiceDelegate).wakeUp(any()) + verify(dreamServiceDelegate, never()).finish(any()) + } + + private fun intentMatcher() = + argThat<Intent> { + getIntExtra(EXTRA_CONTROLS_SURFACE, CONTROLS_SURFACE_ACTIVITY_PANEL) == + CONTROLS_SURFACE_DREAM + } + + private fun buildService( + activityProvider: DreamServiceDelegate = dreamServiceDelegate + ): HomeControlsDreamService = with(kosmos) { return HomeControlsDreamService( controlsSettingsRepository = FakeControlsSettingsRepository(), taskFragmentFactory = taskFragmentComponentFactory, homeControlsComponentInteractor = homeControlsComponentInteractor, - fakeWakeLockBuilder, - dreamActivityProvider = activityProvider, + wakeLockBuilder = fakeWakeLockBuilder, + dreamServiceDelegate = activityProvider, bgDispatcher = testDispatcher, logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest") ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt index d630a2f64c5f..6c5001ab9415 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt @@ -136,20 +136,20 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) - fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissableKeyguard() = + fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissibleKeyguard() = testScope.runTest { powerInteractor.onCameraLaunchGestureDetected() powerInteractor.setAwakeForTest() advanceTimeBy(100) // account for debouncing - // We should head back to GONE since we started there. + // We should head to OCCLUDED because keyguard is not dismissible. assertThat(transitionRepository) .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED) } @Test @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) - fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissableKeyguard() = + fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissibleKeyguard() = testScope.runTest { kosmos.fakeKeyguardRepository.setKeyguardDismissible(true) powerInteractor.onCameraLaunchGestureDetected() @@ -188,6 +188,7 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { ) // Detect a power gesture and then wake up. + kosmos.fakeKeyguardRepository.setKeyguardDismissible(true) reset(transitionRepository) powerInteractor.onCameraLaunchGestureDetected() powerInteractor.setAwakeForTest() @@ -355,6 +356,7 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { ) // Detect a power gesture and then wake up. + kosmos.fakeKeyguardRepository.setKeyguardDismissible(true) reset(transitionRepository) powerInteractor.onCameraLaunchGestureDetected() powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index bfc777509c7b..612f2e73e4bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -56,13 +56,13 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.testKosmos import junit.framework.Assert.assertEquals -import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking 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.Mockito.reset import org.mockito.Mockito.spy @@ -230,6 +230,7 @@ class FromDozingTransitionInteractorTest : SysuiTestCase() { ) // Detect a power gesture and then wake up. + kosmos.fakeKeyguardRepository.setKeyguardDismissible(true) reset(transitionRepository) powerInteractor.onCameraLaunchGestureDetected() powerInteractor.setAwakeForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 78a116737349..5068f6830fa1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -350,7 +350,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { } @Test - fun quickAffordance_updateOncePerShadeExpansion() = + fun quickAffordance_doNotSendUpdatesWhileShadeExpandingAndStillHidden() = testScope.runTest { val shadeExpansion = MutableStateFlow(0f) whenever(shadeInteractor.anyExpansion).thenReturn(shadeExpansion) @@ -365,7 +365,9 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { shadeExpansion.value = i / 10f } - assertThat(collectedValue.size).isEqualTo(initialSize + 1) + assertThat(collectedValue[0]) + .isInstanceOf(KeyguardQuickAffordanceModel.Hidden::class.java) + assertThat(collectedValue.size).isEqualTo(initialSize) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt index 04c270d07b0a..ad24a711e9b7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.BurnInModel import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.kosmos.testScope @@ -104,6 +105,7 @@ class KeyguardIndicationAreaViewModelTest : SysuiTestCase() { burnInInteractor = burnInInteractor, shortcutsCombinedViewModel = shortcutsCombinedViewModel, configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()), + keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt index 37d472169ae5..7ebebd7afa91 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -203,6 +203,21 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { .containsExactlyElementsIn(DEFAULT_TILES.toTileSpecs() + startingTiles) } + @Test + fun prependDefault_noChangesWhenInRetail() = + testScope.runTest { + val user = 0 + retailModeRepository.setRetailMode(true) + val startingTiles = "a" + storeTilesForUser(startingTiles, user) + + runCurrent() + underTest.prependDefault(user) + runCurrent() + + assertThat(loadTilesForUser(user)).isEqualTo(startingTiles) + } + private fun TestScope.storeTilesForUser(specs: String, forUser: Int) { secureSettings.putStringForUser(SETTING, specs, forUser) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt index 58fc10917d44..b12fbc2066a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt @@ -327,6 +327,32 @@ class UserTileSpecRepositoryTest : SysuiTestCase() { assertThat(loadTiles()).isEqualTo(expected) } + @Test + fun setTilesWithRepeats_onlyDistinctTiles() = + testScope.runTest { + val tilesToSet = "a,b,c,a,d,b".toTileSpecs() + val expected = "a,b,c,d" + + val tiles by collectLastValue(underTest.tiles()) + underTest.setTiles(tilesToSet) + + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(expected) + } + + @Test + fun prependDefaultTwice_doesntAddMoreTiles() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles()) + underTest.setTiles(listOf(TileSpec.create("a"))) + + underTest.prependDefault() + val currentTiles = tiles!! + underTest.prependDefault() + + assertThat(tiles).isEqualTo(currentTiles) + } + private fun getDefaultTileSpecs(): List<TileSpec> { return defaultTilesRepository.defaultTiles } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt index 1c73fe2b305d..6ad4b317b94c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt @@ -49,6 +49,7 @@ import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.qs.tiles.di.NewQSTileFactory import com.android.systemui.qs.toProto +import com.android.systemui.retail.data.repository.FakeRetailModeRepository import com.android.systemui.settings.UserTracker import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.any @@ -85,6 +86,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { private val pipelineFlags = QSPipelineFlagsRepository() private val tileLifecycleManagerFactory = TLMFactory() private val minimumTilesRepository = MinimumTilesFixedRepository() + private val retailModeRepository = FakeRetailModeRepository() @Mock private lateinit var customTileStatePersister: CustomTileStatePersister @@ -118,6 +120,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { installedTilesComponentRepository = installedTilesPackageRepository, userRepository = userRepository, minimumTilesRepository = minimumTilesRepository, + retailModeRepository = retailModeRepository, customTileStatePersister = customTileStatePersister, tileFactory = tileFactory, newQSTileFactory = { newQSTileFactory }, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt index 260189d401d2..e8ad038f8fbc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt @@ -34,6 +34,8 @@ import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedReposit import com.android.systemui.qs.pipeline.data.repository.fakeDefaultTilesRepository import com.android.systemui.qs.pipeline.data.repository.fakeMinimumTilesRepository import com.android.systemui.qs.pipeline.data.repository.fakeRestoreRepository +import com.android.systemui.qs.pipeline.data.repository.fakeRetailModeRepository +import com.android.systemui.qs.pipeline.data.repository.fakeTileSpecRepository import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.qsTileFactory import com.android.systemui.settings.fakeUserTracker @@ -138,6 +140,19 @@ class NoLowNumberOfTilesTest : SysuiTestCase() { } } + @Test + fun inRetailMode_onlyOneTile_noPrependDefault() = + with(kosmos) { + testScope.runTest { + fakeRetailModeRepository.setRetailMode(true) + fakeTileSpecRepository.setTiles(0, listOf(goodTile)) + val tiles by collectLastValue(currentTilesInteractor.currentTiles) + runCurrent() + + assertThat(tiles!!.map { it.spec }).isEqualTo(listOf(goodTile)) + } + } + private fun tileCreator(spec: String): QSTile? { return if (spec.contains("OEM")) { null // We don't know how to create OEM spec tiles diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt index a67a8ab643fb..fed613153a4e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt @@ -16,6 +16,10 @@ package com.android.systemui.statusbar +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.os.fakeExecutorHandler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.service.notification.NotificationListenerService @@ -54,6 +58,7 @@ class NotificationMediaManagerTest : SysuiTestCase() { private val notifPipeline = kosmos.notifPipeline private val notifCollection = kosmos.mockNotifCollection private val dumpManager = kosmos.dumpManager + private val handler = kosmos.fakeExecutorHandler private val mediaDataManager = mock<MediaDataManager>() private val backgroundExecutor = FakeExecutor(FakeSystemClock()) @@ -72,7 +77,11 @@ class NotificationMediaManagerTest : SysuiTestCase() { mediaDataManager, dumpManager, backgroundExecutor, + handler, ) + val mediaSession = MediaSession(context, "TEST") + notificationMediaManager.mMediaController = + MediaController(context, mediaSession.sessionToken) verify(mediaDataManager).addListener(listenerCaptor.capture()) } @@ -114,4 +123,32 @@ class NotificationMediaManagerTest : SysuiTestCase() { verify(notifCollection).dismissNotification(notifEntryCaptor.capture(), any()) assertThat(notifEntryCaptor.lastValue.key).isEqualTo(KEY) } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION) + fun clearMediaNotification_flagOn_resetMediaMetadata() { + // set up media metadata. + notificationMediaManager.mMediaListener.onMetadataChanged(MediaMetadata.Builder().build()) + backgroundExecutor.runAllReady() + + // clear media notification. + notificationMediaManager.clearCurrentMediaNotification() + backgroundExecutor.runAllReady() + + assertThat(notificationMediaManager.mediaMetadata).isNull() + assertThat(notificationMediaManager.mMediaController).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_NOTIFICATION_MEDIA_MANAGER_BACKGROUND_EXECUTION) + fun clearMediaNotification_flagOff_resetMediaMetadata() { + // set up media metadata. + notificationMediaManager.mMediaListener.onMetadataChanged(MediaMetadata.Builder().build()) + + // clear media notification. + notificationMediaManager.clearCurrentMediaNotification() + + assertThat(notificationMediaManager.mediaMetadata).isNull() + assertThat(notificationMediaManager.mMediaController).isNull() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt index 63f19fbdfed9..6b5d07282a08 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt @@ -78,7 +78,7 @@ class AvalancheControllerTest : SysuiTestCase() { // Initialize AvalancheController and TestableHeadsUpManager during setUp instead of // declaration, where mocks are null - mAvalancheController = AvalancheController(dumpManager) + mAvalancheController = AvalancheController(dumpManager, mUiEventLoggerFake) testableHeadsUpManager = TestableHeadsUpManager( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java index 3bfc046e46b4..88bef91d043f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper; @@ -147,7 +148,7 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { @Override public void SysuiSetup() throws Exception { super.SysuiSetup(); - mAvalancheController = new AvalancheController(dumpManager); + mAvalancheController = new AvalancheController(dumpManager, mUiEventLoggerFake); } @Test @@ -610,7 +611,31 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { } @Test - public void testPinEntry_logsPeek() { + @EnableFlags(NotificationThrottleHun.FLAG_NAME) + public void testPinEntry_logsPeek_throttleEnabled() { + final BaseHeadsUpManager hum = createHeadsUpManager(); + + // Needs full screen intent in order to be pinned + final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry( + HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext)); + + // Note: the standard way to show a notification would be calling showNotification rather + // than onAlertEntryAdded. However, in practice showNotification in effect adds + // the notification and then updates it; in order to not log twice, the entry needs + // to have a functional ExpandableNotificationRow that can keep track of whether it's + // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit. + hum.onEntryAdded(entryToPin); + + assertEquals(2, mUiEventLoggerFake.numLogs()); + assertEquals(AvalancheController.ThrottleEvent.SHOWN.getId(), + mUiEventLoggerFake.eventId(0)); + assertEquals(BaseHeadsUpManager.NotificationPeekEvent.NOTIFICATION_PEEK.getId(), + mUiEventLoggerFake.eventId(1)); + } + + @Test + @DisableFlags(NotificationThrottleHun.FLAG_NAME) + public void testPinEntry_logsPeek_throttleDisabled() { final BaseHeadsUpManager hum = createHeadsUpManager(); // Needs full screen intent in order to be pinned diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java index 9feb914c56e9..200e92e4370b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java @@ -167,7 +167,7 @@ public class HeadsUpManagerPhoneTest extends BaseHeadsUpManagerTest { mContext.getOrCreateTestableResources().addOverride( R.integer.ambient_notification_extension_time, 500); - mAvalancheController = new AvalancheController(dumpManager); + mAvalancheController = new AvalancheController(dumpManager, mUiEventLogger); } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt index cec8ccf96b4a..b83b93b8f77e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt @@ -18,33 +18,26 @@ package com.android.systemui.volume.domain.interactor import android.bluetooth.BluetoothDevice import android.graphics.drawable.TestStubDrawable -import android.media.AudioDeviceInfo -import android.media.AudioDevicePort import android.media.AudioManager import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.R import com.android.settingslib.bluetooth.CachedBluetoothDevice -import com.android.settingslib.media.BluetoothMediaDevice -import com.android.settingslib.media.MediaDevice -import com.android.settingslib.media.PhoneMediaDevice import com.android.systemui.SysuiTestCase import com.android.systemui.bluetooth.bluetoothAdapter import com.android.systemui.bluetooth.cachedBluetoothDeviceManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.data.repository.TestAudioDevicesFactory import com.android.systemui.volume.data.repository.audioRepository import com.android.systemui.volume.data.repository.audioSharingRepository import com.android.systemui.volume.domain.model.AudioOutputDevice import com.android.systemui.volume.localMediaController import com.android.systemui.volume.localMediaRepository import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.TestMediaDevicesFactory import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent @@ -52,6 +45,10 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @@ -84,7 +81,7 @@ class AudioOutputInteractorTest : SysuiTestCase() { testScope.runTest { with(audioRepository) { setMode(AudioManager.MODE_IN_CALL) - setCommunicationDevice(builtInDevice) + setCommunicationDevice(TestAudioDevicesFactory.builtInDevice()) } val device by collectLastValue(underTest.currentAudioDevice) @@ -104,7 +101,7 @@ class AudioOutputInteractorTest : SysuiTestCase() { testScope.runTest { with(audioRepository) { setMode(AudioManager.MODE_IN_CALL) - setCommunicationDevice(wiredDevice) + setCommunicationDevice(TestAudioDevicesFactory.wiredDevice()) } val device by collectLastValue(underTest.currentAudioDevice) @@ -122,17 +119,18 @@ class AudioOutputInteractorTest : SysuiTestCase() { fun inCall_bluetooth_returnsCommunicationDevice() { with(kosmos) { testScope.runTest { + val btDevice = TestAudioDevicesFactory.bluetoothDevice() with(audioRepository) { setMode(AudioManager.MODE_IN_CALL) setCommunicationDevice(btDevice) } val bluetoothDevice: BluetoothDevice = mock { - whenever(address).thenReturn(btDevice.address) + on { address }.thenReturn(btDevice.address) } val cachedBluetoothDevice: CachedBluetoothDevice = mock { - whenever(address).thenReturn(btDevice.address) - whenever(name).thenReturn(btDevice.productName.toString()) - whenever(isHearingAidDevice).thenReturn(true) + on { address }.thenReturn(btDevice.address) + on { name }.thenReturn(btDevice.productName.toString()) + on { isHearingAidDevice }.thenReturn(true) } whenever(bluetoothAdapter.getRemoteDevice(eq(btDevice.address))) .thenReturn(bluetoothDevice) @@ -156,7 +154,9 @@ class AudioOutputInteractorTest : SysuiTestCase() { testScope.runTest { audioRepository.setMode(AudioManager.MODE_NORMAL) mediaControllerRepository.setActiveSessions(listOf(localMediaController)) - localMediaRepository.updateCurrentConnectedDevice(builtInMediaDevice) + localMediaRepository.updateCurrentConnectedDevice( + TestMediaDevicesFactory.builtInMediaDevice() + ) val device by collectLastValue(underTest.currentAudioDevice) @@ -175,7 +175,9 @@ class AudioOutputInteractorTest : SysuiTestCase() { testScope.runTest { audioRepository.setMode(AudioManager.MODE_NORMAL) mediaControllerRepository.setActiveSessions(listOf(localMediaController)) - localMediaRepository.updateCurrentConnectedDevice(wiredMediaDevice) + localMediaRepository.updateCurrentConnectedDevice( + TestMediaDevicesFactory.wiredMediaDevice() + ) val device by collectLastValue(underTest.currentAudioDevice) @@ -194,7 +196,9 @@ class AudioOutputInteractorTest : SysuiTestCase() { testScope.runTest { audioRepository.setMode(AudioManager.MODE_NORMAL) mediaControllerRepository.setActiveSessions(listOf(localMediaController)) - localMediaRepository.updateCurrentConnectedDevice(bluetoothMediaDevice) + localMediaRepository.updateCurrentConnectedDevice( + TestMediaDevicesFactory.bluetoothMediaDevice() + ) val device by collectLastValue(underTest.currentAudioDevice) @@ -208,48 +212,8 @@ class AudioOutputInteractorTest : SysuiTestCase() { } private companion object { + val testIcon = TestStubDrawable() - val builtInDevice = - AudioDeviceInfo( - AudioDevicePort.createForTesting( - AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, - "built_in", - "" - ) - ) - val wiredDevice = - AudioDeviceInfo( - AudioDevicePort.createForTesting(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, "wired", "") - ) - val btDevice = - AudioDeviceInfo( - AudioDevicePort.createForTesting( - AudioDeviceInfo.TYPE_BLE_HEADSET, - "bt", - "test_address" - ) - ) - val builtInMediaDevice: MediaDevice = - mock<PhoneMediaDevice> { - whenever(name).thenReturn("built_in_media") - whenever(icon).thenReturn(testIcon) - } - val wiredMediaDevice: MediaDevice = - mock<PhoneMediaDevice> { - whenever(deviceType) - .thenReturn(MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE) - whenever(name).thenReturn("wired_media") - whenever(icon).thenReturn(testIcon) - } - val bluetoothMediaDevice: MediaDevice = - mock<BluetoothMediaDevice> { - whenever(name).thenReturn("bt_media") - whenever(icon).thenReturn(testIcon) - val cachedBluetoothDevice: CachedBluetoothDevice = mock { - whenever(isHearingAidDevice).thenReturn(true) - } - whenever(cachedDevice).thenReturn(cachedBluetoothDevice) - } } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt new file mode 100644 index 000000000000..7934b02126bd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.ui.navigation + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.uiEventLoggerFake +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.activityStarter +import com.android.systemui.testKosmos +import com.android.systemui.volume.domain.model.VolumePanelRoute +import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor +import com.android.systemui.volume.panel.ui.viewmodel.volumePanelViewModelFactory +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class VolumeNavigatorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private val underTest: VolumeNavigator = + with(kosmos) { + VolumeNavigator( + testScope.backgroundScope, + testDispatcher, + mock {}, + activityStarter, + volumePanelViewModelFactory, + mock { + on { create(any(), anyInt(), anyBoolean(), any()) }.thenReturn(mock {}) + on { applicationContext }.thenReturn(context) + }, + uiEventLoggerFake, + volumePanelGlobalStateInteractor, + ) + } + + @Test + fun showNewVolumePanel_keyguardLocked_notShown() = + with(kosmos) { + testScope.runTest { + val panelState by collectLastValue(volumePanelGlobalStateInteractor.globalState) + + underTest.openVolumePanel(VolumePanelRoute.COMPOSE_VOLUME_PANEL) + runCurrent() + + assertThat(panelState!!.isVisible).isFalse() + } + } + + @Test + fun showNewVolumePanel_keyguardUnlocked_shown() = + with(kosmos) { + testScope.runTest { + whenever(activityStarter.dismissKeyguardThenExecute(any(), any(), anyBoolean())) + .then { (it.arguments[0] as ActivityStarter.OnDismissAction).onDismiss() } + val panelState by collectLastValue(volumePanelGlobalStateInteractor.globalState) + + underTest.openVolumePanel(VolumePanelRoute.COMPOSE_VOLUME_PANEL) + runCurrent() + + assertThat(panelState!!.isVisible).isTrue() + } + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java index 3244eb43c8c4..bf58eee9a9ce 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java @@ -94,6 +94,9 @@ public interface QS extends FragmentBase { default void setHasNotifications(boolean hasNotifications) { } + /** Sets whether the squishiness fraction should be updated on the media host. */ + default void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) {} + /** * Should touches from the notification panel be disallowed? * The notification panel might grab any touches rom QS at any time to collapse the shade. diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml deleted file mode 100644 index 4a2a1cb9dc6d..000000000000 --- a/packages/SystemUI/res-keyguard/layout/footer_actions.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -** Copyright 2022, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. ---> - -<!-- Action buttons for footer in QS/QQS, containing settings button, power off button etc --> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="@dimen/footer_actions_height" - android:elevation="@dimen/qs_panel_elevation" - android:paddingTop="@dimen/qs_footer_actions_top_padding" - android:paddingBottom="@dimen/qs_footer_actions_bottom_padding" - android:background="@drawable/qs_footer_actions_background" - android:gravity="center_vertical|end" - android:layout_gravity="bottom" -/>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml deleted file mode 100644 index fad41c822ec0..000000000000 --- a/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2022 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<com.android.systemui.statusbar.AlphaOptimizedFrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="@dimen/qs_footer_action_button_size" - android:layout_height="@dimen/qs_footer_action_button_size" - android:visibility="gone"> - <ImageView - android:id="@+id/icon" - android:layout_width="@dimen/qs_footer_icon_size" - android:layout_height="@dimen/qs_footer_icon_size" - android:layout_gravity="center" - android:scaleType="centerInside" /> -</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml deleted file mode 100644 index c09607d19bdd..000000000000 --- a/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml +++ /dev/null @@ -1,39 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2022 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<com.android.systemui.statusbar.AlphaOptimizedFrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="@dimen/qs_footer_action_button_size" - android:layout_height="@dimen/qs_footer_action_button_size" - android:background="@drawable/qs_footer_action_circle" - android:visibility="gone"> - <TextView - android:id="@+id/number" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.QS.SecurityFooter" - android:layout_gravity="center" - android:textColor="?attr/onShadeInactiveVariant" - android:textSize="18sp"/> - <ImageView - android:id="@+id/new_dot" - android:layout_width="12dp" - android:layout_height="12dp" - android:scaleType="fitCenter" - android:layout_gravity="bottom|end" - android:src="@drawable/fgs_dot" - android:contentDescription="@string/fgs_dot_content_description" /> -</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml deleted file mode 100644 index 1c31f1da0681..000000000000 --- a/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml +++ /dev/null @@ -1,66 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2022 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<com.android.systemui.animation.view.LaunchableLinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="0dp" - android:layout_height="@dimen/qs_security_footer_single_line_height" - android:layout_weight="1" - android:orientation="horizontal" - android:paddingHorizontal="@dimen/qs_footer_padding" - android:gravity="center_vertical" - android:layout_marginEnd="@dimen/qs_footer_action_inset" - android:background="@drawable/qs_security_footer_background" - android:visibility="gone"> - <ImageView - android:id="@+id/icon" - android:layout_width="@dimen/qs_footer_icon_size" - android:layout_height="@dimen/qs_footer_icon_size" - android:gravity="start" - android:layout_marginEnd="12dp" - android:contentDescription="@null" - android:src="@drawable/ic_info_outline" - android:tint="?attr/onSurfaceVariant" /> - - <TextView - android:id="@+id/text" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:maxLines="1" - android:ellipsize="end" - android:textAppearance="@style/TextAppearance.QS.SecurityFooter" - android:textColor="?attr/onSurfaceVariant"/> - - <ImageView - android:id="@+id/new_dot" - android:layout_width="12dp" - android:layout_height="12dp" - android:scaleType="fitCenter" - android:src="@drawable/fgs_dot" - android:contentDescription="@string/fgs_dot_content_description" - /> - - <ImageView - android:id="@+id/chevron_icon" - android:layout_width="@dimen/qs_footer_icon_size" - android:layout_height="@dimen/qs_footer_icon_size" - android:layout_marginStart="8dp" - android:contentDescription="@null" - android:src="@*android:drawable/ic_chevron_end" - android:autoMirrored="true" - android:tint="?attr/onSurfaceVariant" /> -</com.android.systemui.animation.view.LaunchableLinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml b/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml deleted file mode 100644 index a8abd793bd00..000000000000 --- a/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="true" android:color="?attr/onShadeActive" android:alpha="0.12" /> - <item android:state_hovered="true" android:color="?attr/onShadeActive" android:alpha="0.09" /> - <item android:color="@color/transparent" /> -</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/fgs_dot.xml b/packages/SystemUI/res/drawable/fgs_dot.xml deleted file mode 100644 index 0881d7c5c2b5..000000000000 --- a/packages/SystemUI/res/drawable/fgs_dot.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -** Copyright 2022, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. ---> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:shape="oval" - android:width="12dp" - android:height="12dp"> - <solid android:color="?attr/tertiary" /> -</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml deleted file mode 100644 index 4a5d4af96497..000000000000 --- a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml +++ /dev/null @@ -1,37 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2022 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<inset xmlns:android="http://schemas.android.com/apk/res/android" - android:inset="@dimen/qs_footer_action_inset"> - <ripple - android:color="?android:attr/colorControlHighlight"> - <item android:id="@android:id/mask"> - <!-- We make this shape a rounded rectangle instead of a oval so that it can animate --> - <!-- properly into an app/dialog. --> - <shape android:shape="rectangle"> - <solid android:color="@android:color/white"/> - <corners android:radius="@dimen/qs_footer_action_corner_radius"/> - </shape> - </item> - <item> - <shape android:shape="rectangle"> - <solid android:color="?attr/shadeInactive"/> - <corners android:radius="@dimen/qs_footer_action_corner_radius"/> - </shape> - </item> - - </ripple> -</inset>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml deleted file mode 100644 index 47a2965bcfac..000000000000 --- a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml +++ /dev/null @@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2022 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<inset xmlns:android="http://schemas.android.com/apk/res/android" - android:inset="@dimen/qs_footer_action_inset"> - <ripple - android:color="?android:attr/colorControlHighlight"> - <item android:id="@android:id/mask"> - <!-- We make this shape a rounded rectangle instead of a oval so that it can animate --> - <!-- properly into an app/dialog. --> - <shape android:shape="rectangle"> - <solid android:color="@android:color/white"/> - <corners android:radius="@dimen/qs_footer_action_corner_radius"/> - </shape> - </item> - <item> - <shape android:shape="rectangle"> - <solid android:color="?attr/shadeActive"/> - <corners android:radius="@dimen/qs_footer_action_corner_radius"/> - </shape> - </item> - <item> - <shape android:shape="rectangle"> - <solid android:color="@color/qs_footer_power_button_overlay_color"/> - <corners android:radius="@dimen/qs_footer_action_corner_radius"/> - </shape> - </item> - - </ripple> -</inset>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/qs_security_footer_background.xml b/packages/SystemUI/res/drawable/qs_security_footer_background.xml deleted file mode 100644 index 0b0055b1f020..000000000000 --- a/packages/SystemUI/res/drawable/qs_security_footer_background.xml +++ /dev/null @@ -1,37 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2021 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<inset xmlns:android="http://schemas.android.com/apk/res/android" - android:insetTop="@dimen/qs_footer_action_inset" - android:insetBottom="@dimen/qs_footer_action_inset" - > - <ripple - android:color="?android:attr/colorControlHighlight"> - <item android:id="@android:id/mask"> - <shape android:shape="rectangle"> - <solid android:color="@android:color/white"/> - <corners android:radius="@dimen/qs_security_footer_corner_radius"/> - </shape> - </item> - <item> - <shape android:shape="rectangle"> - <stroke android:width="1dp" - android:color="?attr/shadeInactive"/> - <corners android:radius="@dimen/qs_security_footer_corner_radius"/> - </shape> - </item> - </ripple> -</inset>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/rounded_bg_full_large_radius.xml b/packages/SystemUI/res/drawable/rounded_bg_full_large_radius.xml deleted file mode 100644 index 29a014a713f7..000000000000 --- a/packages/SystemUI/res/drawable/rounded_bg_full_large_radius.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2021 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorAccentPrimary" /> - <corners android:radius="40dp" /> -</shape> diff --git a/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml deleted file mode 100644 index 8b9eabc5bd93..000000000000 --- a/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml +++ /dev/null @@ -1,237 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:id="@+id/biometric_prompt_constraint_layout" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <ImageView - android:id="@+id/background" - android:layout_width="0dp" - android:layout_height="0dp" - android:contentDescription="@string/biometric_dialog_empty_space_description" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <View - android:id="@+id/panel" - style="@style/AuthCredentialPanelStyle" - android:layout_width="0dp" - android:layout_height="0dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/rightGuideline" - app:layout_constraintStart_toStartOf="@+id/leftGuideline" - app:layout_constraintTop_toTopOf="@+id/topBarrier" - app:layout_constraintWidth_max="640dp" /> - - <include - android:id="@+id/button_bar" - layout="@layout/biometric_prompt_button_bar" - android:layout_width="0dp" - android:layout_height="wrap_content" - app:layout_constraintBottom_toBottomOf="@id/bottomGuideline" - app:layout_constraintEnd_toEndOf="@id/panel" - app:layout_constraintStart_toStartOf="@id/panel" /> - - <ScrollView - android:id="@+id/scrollView" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:fadeScrollbars="false" - android:fillViewport="true" - android:paddingBottom="32dp" - android:paddingHorizontal="32dp" - android:paddingTop="24dp" - app:layout_constrainedHeight="true" - app:layout_constrainedWidth="true" - app:layout_constraintBottom_toTopOf="@+id/scrollBarrier" - app:layout_constraintEnd_toEndOf="@id/panel" - app:layout_constraintStart_toStartOf="@id/panel" - app:layout_constraintTop_toTopOf="@+id/topGuideline" - app:layout_constraintVertical_bias="1.0"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/innerConstraint" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <ImageView - android:id="@+id/logo" - android:layout_width="@dimen/biometric_prompt_logo_size" - android:layout_height="@dimen/biometric_prompt_logo_size" - android:layout_gravity="center" - android:contentDescription="@string/biometric_dialog_logo" - android:scaleType="fitXY" - android:visibility="visible" - app:layout_constraintBottom_toTopOf="@+id/logo_description" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/logo_description" - style="@style/TextAppearance.AuthCredential.LogoDescription" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingTop="16dp" - app:layout_constraintBottom_toTopOf="@+id/title" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/logo" /> - - <TextView - android:id="@+id/title" - style="@style/TextAppearance.AuthCredential.Title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:paddingTop="16dp" - app:layout_constraintBottom_toTopOf="@+id/subtitle" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/logo_description" /> - - <TextView - android:id="@+id/subtitle" - style="@style/TextAppearance.AuthCredential.Subtitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:paddingTop="16dp" - app:layout_constraintBottom_toTopOf="@+id/contentBarrier" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/title" /> - - <LinearLayout - android:id="@+id/customized_view_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="center_vertical" - android:orientation="vertical" - android:paddingTop="24dp" - android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/subtitle" /> - - <TextView - android:id="@+id/description" - style="@style/TextAppearance.AuthCredential.Description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:paddingTop="16dp" - android:textAlignment="viewStart" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/subtitle" /> - - <androidx.constraintlayout.widget.Barrier - android:id="@+id/contentBarrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierAllowsGoneWidgets="false" - app:barrierDirection="top" - app:constraint_referenced_ids="description, customized_view_container" /> - - </androidx.constraintlayout.widget.ConstraintLayout> - </ScrollView> - - <!-- Cancel Button, replaces negative button when biometric is accepted --> - <TextView - android:id="@+id/indicator" - style="@style/TextAppearance.AuthCredential.Indicator" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="24dp" - android:accessibilityLiveRegion="assertive" - android:fadingEdge="horizontal" - android:gravity="center_horizontal" - android:scrollHorizontally="true" - app:layout_constraintBottom_toTopOf="@+id/button_bar" - app:layout_constraintEnd_toEndOf="@+id/panel" - app:layout_constraintStart_toStartOf="@+id/panel" - app:layout_constraintTop_toBottomOf="@+id/biometric_icon" - app:layout_constraintVertical_bias="0.0" /> - - <!-- "Use Credential" Button, replaces if device credential is allowed --> - - <!-- Positive Button --> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/topBarrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierAllowsGoneWidgets="false" - app:barrierDirection="top" - app:constraint_referenced_ids="scrollView" /> - - <!-- Try Again Button --> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/scrollBarrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierAllowsGoneWidgets="false" - app:barrierDirection="top" - app:constraint_referenced_ids="biometric_icon, button_bar" /> - - <!-- Guidelines for setting panel border --> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/leftGuideline" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_begin="@dimen/biometric_dialog_border_padding" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/rightGuideline" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_end="@dimen/biometric_dialog_border_padding" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/bottomGuideline" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="40dp" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/topGuideline" - android:layout_width="0dp" - android:layout_height="0dp" - android:orientation="horizontal" - app:layout_constraintGuide_begin="56dp" /> - - <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper - android:id="@+id/biometric_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="1.0" - tools:srcCompat="@tools:sample/avatars" /> - - <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper - android:id="@+id/biometric_icon_overlay" - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_gravity="center" - android:contentDescription="@null" - android:scaleType="fitXY" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/biometric_icon" - app:layout_constraintEnd_toEndOf="@+id/biometric_icon" - app:layout_constraintStart_toStartOf="@+id/biometric_icon" - app:layout_constraintTop_toTopOf="@+id/biometric_icon" /> - -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml index 292e49610e2a..06d1bf4c01cb 100644 --- a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml +++ b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml @@ -5,9 +5,9 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <LinearLayout + <FrameLayout android:id="@+id/shortcut_helper_sheet" - style="@style/Widget.Material3.BottomSheet" + style="@style/ShortcutHelperBottomSheet" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" @@ -19,13 +19,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> - <TextView + <androidx.compose.ui.platform.ComposeView + android:id="@+id/shortcut_helper_compose_container" android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" - android:gravity="center" - android:textAppearance="?textAppearanceDisplayLarge" - android:background="?colorTertiaryContainer" - android:text="Shortcut Helper Content" /> - </LinearLayout> -</androidx.coordinatorlayout.widget.CoordinatorLayout> + android:layout_height="match_parent" /> + </FrameLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml index 9b5b59fc116f..8d50bfa00fd5 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml @@ -22,9 +22,11 @@ android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="@id/rightGuideline" + app:layout_constraintEnd_toStartOf="@id/rightGuideline" app:layout_constraintStart_toStartOf="@id/leftGuideline" - app:layout_constraintTop_toTopOf="@id/topBarrier" /> + app:layout_constraintTop_toTopOf="@id/topBarrier" + app:layout_constraintWidth_max="@dimen/biometric_prompt_panel_max_width" /> + <include android:id="@+id/button_bar" @@ -41,8 +43,8 @@ android:layout_height="wrap_content" android:fillViewport="true" android:fadeScrollbars="false" - android:paddingBottom="24dp" - android:paddingHorizontal="24dp" + android:paddingBottom="@dimen/biometric_prompt_top_scroll_view_bottom_padding" + android:paddingHorizontal="@dimen/biometric_prompt_top_scroll_view_horizontal_padding" android:paddingTop="24dp" app:layout_constrainedHeight="true" app:layout_constrainedWidth="true" @@ -76,7 +78,7 @@ style="@style/TextAppearance.AuthCredential.LogoDescription" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingTop="8dp" + android:paddingTop="@dimen/biometric_prompt_logo_description_top_padding" app:layout_constraintBottom_toTopOf="@+id/title" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -180,14 +182,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" - app:layout_constraintGuide_begin="0dp" /> + app:layout_constraintGuide_begin="@dimen/biometric_prompt_one_pane_medium_horizontal_guideline_padding" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/rightGuideline" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" - app:layout_constraintGuide_end="0dp" /> + app:layout_constraintGuide_end="@dimen/biometric_prompt_one_pane_medium_horizontal_guideline_padding" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/bottomGuideline" @@ -201,7 +203,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:orientation="horizontal" - app:layout_constraintGuide_begin="119dp" /> + app:layout_constraintGuide_begin="@dimen/biometric_prompt_one_pane_medium_top_guideline_padding" /> <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper android:id="@+id/biometric_icon" diff --git a/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml index 01b9f7e2e38a..01b9f7e2e38a 100644 --- a/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml diff --git a/packages/SystemUI/res/layout/ongoing_activity_chip.xml b/packages/SystemUI/res/layout/ongoing_activity_chip.xml index a33be12a655a..cd5c37d43633 100644 --- a/packages/SystemUI/res/layout/ongoing_activity_chip.xml +++ b/packages/SystemUI/res/layout/ongoing_activity_chip.xml @@ -40,6 +40,7 @@ <ImageView android:src="@*android:drawable/ic_phone" + android:id="@+id/ongoing_activity_chip_icon" android:layout_width="@dimen/ongoing_activity_chip_icon_size" android:layout_height="@dimen/ongoing_activity_chip_icon_size" android:tint="?android:attr/colorPrimary" diff --git a/packages/SystemUI/res/layout/people_space_activity.xml b/packages/SystemUI/res/layout/people_space_activity.xml deleted file mode 100644 index f45cc7c464d5..000000000000 --- a/packages/SystemUI/res/layout/people_space_activity.xml +++ /dev/null @@ -1,23 +0,0 @@ -<!-- - ~ Copyright (C) 2020 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/container" - android:layout_width="match_parent" - android:layout_height="match_parent"> - <!-- The content of people_space_activity_(no|with)_conversations.xml will be added here at - runtime depending on the number of conversations to show. --> -</FrameLayout> diff --git a/packages/SystemUI/res/layout/people_space_activity_list_divider.xml b/packages/SystemUI/res/layout/people_space_activity_list_divider.xml deleted file mode 100644 index 3b9fb3be3814..000000000000 --- a/packages/SystemUI/res/layout/people_space_activity_list_divider.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2021 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<View - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="2dp" - android:background="?android:attr/colorBackground" /> diff --git a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml b/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml deleted file mode 100644 index a97c90c5e8ac..000000000000 --- a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml +++ /dev/null @@ -1,79 +0,0 @@ -<!-- - ~ Copyright (C) 2021 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<RelativeLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:id="@+id/top_level_no_conversations" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:padding="24dp" - android:clipToOutline="true"> - <TextView - android:id="@+id/select_conversation_title" - android:gravity="center" - android:text="@string/select_conversation_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerHorizontal="true" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorPrimary" - android:textSize="24sp" - android:layout_alignParentTop="true" /> - - <TextView - android:id="@+id/select_conversation" - android:gravity="center" - android:text="@string/no_conversations_text" - android:layout_width="match_parent" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorPrimary" - android:textSize="16sp" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:padding="24dp" - android:layout_marginTop="26dp" - android:layout_below="@id/select_conversation_title"/> - - <Button - style="?android:attr/buttonBarButtonStyle" - android:id="@+id/got_it_button" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:background="@drawable/rounded_bg_full_large_radius" - android:text="@string/got_it" - android:textColor="?androidprv:attr/textColorOnAccent" - android:layout_marginBottom="60dp" - android:layout_alignParentBottom="true" /> - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_above="@id/got_it_button" - android:layout_below="@id/select_conversation" - android:layout_centerInParent="true" - android:clipToOutline="true"> - <LinearLayout - android:id="@+id/widget_initial_layout" - android:layout_width="200dp" - android:layout_height="100dp" - android:layout_gravity="center" - android:background="@drawable/rounded_bg_full_large_radius" - android:layout_above="@id/got_it_button"> - <include layout="@layout/people_space_placeholder_layout" /> - </LinearLayout> - </LinearLayout> -</RelativeLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml b/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml deleted file mode 100644 index 2384963c44db..000000000000 --- a/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml +++ /dev/null @@ -1,115 +0,0 @@ -<!-- - ~ Copyright (C) 2022 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:id="@+id/top_level_with_conversations" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:padding="8dp"> - <TextView - android:id="@+id/select_conversation_title" - android:text="@string/select_conversation_title" - android:gravity="center" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorPrimary" - android:textSize="24sp"/> - - <TextView - android:id="@+id/select_conversation" - android:text="@string/select_conversation_text" - android:gravity="center" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorPrimary" - android:textSize="16sp" - android:paddingVertical="24dp" - android:paddingHorizontal="48dp"/> - - <androidx.core.widget.NestedScrollView - android:id="@+id/scroll_view" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <LinearLayout - android:id="@+id/scroll_layout" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/priority" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginBottom="35dp"> - <TextView - android:id="@+id/priority_header" - android:text="@string/priority_conversations" - android:layout_width="wrap_content" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" - android:textColor="?androidprv:attr/colorAccentPrimaryVariant" - android:textSize="14sp" - android:paddingStart="16dp" - android:layout_height="wrap_content"/> - - <LinearLayout - android:id="@+id/priority_tiles" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="10dp" - android:orientation="vertical" - android:background="@drawable/rounded_bg_full_large_radius" - android:clipToOutline="true"> - </LinearLayout> - </LinearLayout> - - <LinearLayout - android:id="@+id/recent" - android:orientation="vertical" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - <TextView - android:id="@+id/recent_header" - android:gravity="start" - android:text="@string/recent_conversations" - android:layout_width="wrap_content" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" - android:textColor="?androidprv:attr/colorAccentPrimaryVariant" - android:textSize="14sp" - android:paddingStart="16dp" - android:layout_height="wrap_content"/> - - <LinearLayout - android:id="@+id/recent_tiles" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="10dp" - android:orientation="vertical" - android:background="@drawable/rounded_bg_full_large_radius" - android:clipToOutline="true"> - </LinearLayout> - </LinearLayout> - </LinearLayout> - </androidx.core.widget.NestedScrollView> -</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/people_space_tile_view.xml b/packages/SystemUI/res/layout/people_space_tile_view.xml deleted file mode 100644 index b0599caae6df..000000000000 --- a/packages/SystemUI/res/layout/people_space_tile_view.xml +++ /dev/null @@ -1,60 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ Copyright (C) 2020 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:id="@+id/tile_view" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> - - <LinearLayout - android:orientation="vertical" - android:background="?androidprv:attr/colorSurface" - android:padding="12dp" - android:elevation="4dp" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <LinearLayout - android:orientation="horizontal" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="start"> - - <ImageView - android:id="@+id/tile_view_person_icon" - android:layout_width="@dimen/avatar_size_for_medium" - android:layout_height="@dimen/avatar_size_for_medium" /> - - <LinearLayout - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical"> - - <TextView - android:id="@+id/tile_view_name" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:paddingHorizontal="16dp" - android:textSize="22sp" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical"/> - </LinearLayout> - </LinearLayout> - </LinearLayout> -</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml index e3c5a7d03d2e..5f77f61d805b 100644 --- a/packages/SystemUI/res/layout/qs_panel.xml +++ b/packages/SystemUI/res/layout/qs_panel.xml @@ -47,13 +47,12 @@ <include layout="@layout/quick_status_bar_expanded_header" /> - <include - layout="@layout/footer_actions" + <androidx.compose.ui.platform.ComposeView android:id="@+id/qs_footer_actions" android:layout_height="@dimen/footer_actions_height" android:layout_width="match_parent" android:layout_gravity="bottom" - /> + android:elevation="@dimen/qs_panel_elevation" /> <include android:id="@+id/qs_customize" diff --git a/packages/SystemUI/res/raw/face_dialog_authenticating.json b/packages/SystemUI/res/raw/face_dialog_authenticating.json deleted file mode 100644 index 4e25e6d933c4..000000000000 --- a/packages/SystemUI/res/raw/face_dialog_authenticating.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.7.13","fr":60,"ip":0,"op":61,"w":64,"h":64,"nm":"face_scanning 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".blue200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[32,32,0],"ix":2,"l":2},"a":{"a":0,"k":[27.25,27.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[95,95,100]},{"t":60,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-1.243],[-1.244,0],[0,1.243],[1.242,0]],"o":[[0,1.243],[1.242,0],[0,-1.243],[-1.244,0]],"v":[[-2.249,0.001],[0.001,2.251],[2.249,0.001],[0.001,-2.251]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[15.1,20.495],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-1.243],[-1.242,0],[0,1.243],[1.242,0]],"o":[[0,1.243],[1.242,0],[0,-1.243],[-1.242,0]],"v":[[-2.249,0],[0.001,2.25],[2.249,0],[0.001,-2.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[39.4,20.495],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.814,3.523],[-2.814,3.523],[-2.814,1.363],[0.652,1.363],[0.652,-3.523],[2.814,-3.523]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.791,28.479],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.154,0.15],[0,0],[0.117,-0.095],[0,0],[0.228,-0.121],[0.358,-0.103],[0.922,0.261],[0.3,0.16],[0.24,0.185],[0.14,0.139],[0.178,0.261],[0.143,0.451],[0,0],[0,0.494],[0,0],[-0.214,-0.676],[-0.392,-0.572],[-0.323,-0.317],[-0.228,-0.177],[-0.333,-0.179],[-0.503,-0.145],[-0.662,0],[-0.653,0.184],[-0.437,0.233],[-0.336,0.258],[0,0],[0,0]],"o":[[0,0],[-0.107,0.106],[0,0],[-0.24,0.185],[-0.301,0.16],[-0.92,0.261],[-0.357,-0.103],[-0.228,-0.121],[-0.158,-0.122],[-0.225,-0.221],[-0.272,-0.393],[0,0],[-0.147,-0.466],[0,0],[0,0.716],[0.206,0.656],[0.256,0.372],[0.204,0.201],[0.336,0.258],[0.436,0.233],[0.655,0.184],[0.662,0],[0.503,-0.145],[0.332,-0.179],[0,0],[0,0],[0.165,-0.136]],"v":[[6.094,1.465],[4.579,-0.076],[4.242,0.225],[4.124,0.315],[3.43,0.771],[2.439,1.165],[-0.342,1.165],[-1.331,0.771],[-2.027,0.315],[-2.48,-0.075],[-3.087,-0.801],[-3.712,-2.075],[-3.712,-2.075],[-3.934,-3.523],[-6.094,-3.523],[-5.771,-1.424],[-4.868,0.424],[-3.995,1.465],[-3.344,2.027],[-2.35,2.676],[-0.934,3.243],[1.049,3.523],[3.031,3.243],[4.449,2.676],[5.441,2.027],[5.482,1.997],[5.615,1.895]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[26.201,40.411],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.398,0],[0,-13.4],[13.398,0],[0,13.4]],"o":[[13.398,0],[0,13.4],[-13.398,0],[0,-13.4]],"v":[[0,-24.3],[24.3,0],[0,24.3],[-24.3,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[14.904,0],[0,-14.904],[-14.904,0],[0,14.904]],"o":[[-14.904,0],[0,14.904],[14.904,0],[0,-14.904]],"v":[[0,-27],[-27,0],[0,27],[27,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.25,27.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":4,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1200,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 56ebc0668097..aea79e84f7e8 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -28,9 +28,7 @@ <!-- In landscape the security footer is actually part of the header, and needs to be as short as the header --> - <dimen name="qs_security_footer_single_line_height">@*android:dimen/quick_qs_offset_height</dimen> <dimen name="qs_footer_padding">14dp</dimen> - <dimen name="qs_security_footer_background_inset">12dp</dimen> <dimen name="volume_tool_tip_top_margin">12dp</dimen> <dimen name="volume_row_slider_height">128dp</dimen> diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 2cfba01fe1c8..29e0dbea24f2 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -68,11 +68,6 @@ <dimen name="qs_brightness_margin_bottom">16dp</dimen> - <!-- For large screens the security footer appears below the footer, - same as phones in portrait --> - <dimen name="qs_security_footer_single_line_height">48dp</dimen> - <dimen name="qs_security_footer_background_inset">0dp</dimen> - <dimen name="qs_panel_padding_top">8dp</dimen> <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) --> @@ -102,6 +97,17 @@ <dimen name="lockscreen_shade_status_bar_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> <dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen> + <!-- Dimensions for biometric prompt panel padding --> + <dimen name="biometric_prompt_one_pane_medium_top_guideline_padding">56dp</dimen> + <dimen name="biometric_prompt_one_pane_medium_horizontal_guideline_padding">@dimen/biometric_dialog_border_padding</dimen> + + <!-- Dimensions for biometric prompt scroll view padding --> + <dimen name="biometric_prompt_top_scroll_view_bottom_padding">32dp</dimen> + <dimen name="biometric_prompt_top_scroll_view_horizontal_padding">32dp</dimen> + + <!-- Dimensions for biometric prompt custom content view. --> + <dimen name="biometric_prompt_logo_description_top_padding">16dp</dimen> + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> <dimen name="biometric_auth_pattern_view_size">348dp</dimen> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index edd3d77555f7..8ce20684d892 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -713,7 +713,6 @@ <dimen name="qs_header_mobile_icon_size">@dimen/status_bar_icon_drawing_size</dimen> <dimen name="qs_header_carrier_separator_width">6dp</dimen> <dimen name="qs_carrier_margin_width">4dp</dimen> - <dimen name="qs_footer_icon_size">20dp</dimen> <dimen name="qs_header_height">120dp</dimen> <dimen name="qs_header_row_min_height">48dp</dimen> @@ -721,11 +720,7 @@ <dimen name="new_qs_header_non_clickable_element_height">24sp</dimen> <dimen name="qs_footer_padding">20dp</dimen> - <dimen name="qs_security_footer_height">88dp</dimen> - <dimen name="qs_security_footer_single_line_height">48dp</dimen> <dimen name="qs_footers_margin_bottom">8dp</dimen> - <dimen name="qs_security_footer_background_inset">0dp</dimen> - <dimen name="qs_security_footer_corner_radius">28dp</dimen> <dimen name="segmented_button_spacing">0dp</dimen> <dimen name="borderless_button_radius">2dp</dimen> @@ -1119,11 +1114,18 @@ <dimen name="biometric_dialog_height">240dp</dimen> <!-- Dimensions for biometric prompt panel padding --> - <dimen name="biometric_prompt_small_horizontal_guideline_padding">344dp</dimen> - <dimen name="biometric_prompt_udfps_horizontal_guideline_padding">114dp</dimen> - <dimen name="biometric_prompt_udfps_mid_guideline_padding">409dp</dimen> - <dimen name="biometric_prompt_medium_horizontal_guideline_padding">640dp</dimen> - <dimen name="biometric_prompt_medium_mid_guideline_padding">330dp</dimen> + <dimen name="biometric_prompt_panel_max_width">640dp</dimen> + <dimen name="biometric_prompt_land_small_horizontal_guideline_padding">344dp</dimen> + <dimen name="biometric_prompt_two_pane_udfps_horizontal_guideline_padding">114dp</dimen> + <dimen name="biometric_prompt_two_pane_udfps_mid_guideline_padding">409dp</dimen> + <dimen name="biometric_prompt_two_pane_medium_horizontal_guideline_padding">640dp</dimen> + <dimen name="biometric_prompt_two_pane_medium_mid_guideline_padding">330dp</dimen> + <dimen name="biometric_prompt_one_pane_medium_top_guideline_padding">119dp</dimen> + <dimen name="biometric_prompt_one_pane_medium_horizontal_guideline_padding">0dp</dimen> + + <!-- Dimensions for biometric prompt scroll view padding --> + <dimen name="biometric_prompt_top_scroll_view_bottom_padding">24dp</dimen> + <dimen name="biometric_prompt_top_scroll_view_horizontal_padding">24dp</dimen> <!-- Dimensions for biometric prompt icon padding --> <dimen name="biometric_prompt_portrait_small_bottom_padding">60dp</dimen> @@ -1136,6 +1138,7 @@ <!-- Dimensions for biometric prompt custom content view. --> <dimen name="biometric_prompt_logo_size">32dp</dimen> + <dimen name="biometric_prompt_logo_description_top_padding">8dp</dimen> <dimen name="biometric_prompt_content_corner_radius">28dp</dimen> <dimen name="biometric_prompt_content_padding_horizontal">24dp</dimen> <dimen name="biometric_prompt_content_padding_vertical">16dp</dimen> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index b993a5ad75b9..177ba598add7 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -259,9 +259,6 @@ <!-- ID of the Scene Container root Composable view --> <item type='id' name="scene_container_root_composable" /> - <!-- Tag set on the Compose implementation of the QS footer actions. --> - <item type="id" name="tag_compose_qs_footer_actions" /> - <!-- Ids for the device entry icon. device_entry_icon_view: parent view of both device_entry_icon and device_entry_icon_bg diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c038a8207d43..1226bbf21a0f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3489,6 +3489,45 @@ <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] --> <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string> + <!-- Title of the keyboard shortcut helper category "System". The helper is a component that + shows the user which keyboard shortcuts they can use. The "System" shortcuts are for + example "Take a screenshot" or "Go back". [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_system">System</string> + <!-- Title of the keyboard shortcut helper category "Multitasking". The helper is a component + that shows the user which keyboard shortcuts they can use. The "Multitasking" shortcuts are + for example "Enter split screen". [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_multitasking">Multitasking</string> + <!-- Title of the keyboard shortcut helper category "Input". The helper is a component + that shows the user which keyboard shortcuts they can use. The "Input" shortcuts are + the ones provided by the keyboard. Examples are "Access emoji" or "Switch to next language" + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_input">Input</string> + <!-- Title of the keyboard shortcut helper category "App shortcuts". The helper is a component + that shows the user which keyboard shortcuts they can use. The "App shortcuts" are + for example "Open browser" or "Open calculator". [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_app_shortcuts">App shortcuts</string> + <!-- Title of the keyboard shortcut helper category "Accessibility". The helper is a component + that shows the user which keyboard shortcuts they can use. The "Accessibility" shortcuts + are for example "Turn on talkback". [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_category_a11y">Accessibility</string> + <!-- Title at the top of the keyboard shortcut helper UI. The helper is a component + that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_title">Keyboard shortcuts</string> + <!-- Placeholder text shown in the search box of the keyboard shortcut helper, when the user + hasn't typed in anything in the search box yet. The helper is a component that shows the + user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_search_placeholder">Search shortcuts</string> + <!-- Content description of the icon that allows to collapse a keyboard shortcut helper category + panel. The helper is a component that shows the user which keyboard shortcuts they can + use. The helper shows shortcuts in categories, which can be collapsed or expanded. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_content_description_collapse_icon">Collapse icon</string> + <!-- Content description of the icon that allows to expand a keyboard shortcut helper category + panel. The helper is a component that shows the user which keyboard shortcuts they can + use. The helper shows shortcuts in categories, which can be collapsed or expanded. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_content_description_expand_icon">Expand icon</string> + <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] --> <string name="keyboard_backlight_dialog_title">Keyboard backlight</string> <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index b8f71c10dc89..1e0adec4e84f 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -149,11 +149,6 @@ <item name="android:letterSpacing">0.01</item> </style> - <style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item> - <item name="android:textColor">?attr/onSurface</item> - </style> - <style name="TextAppearance.QS.Status.Carriers" /> <style name="TextAppearance.QS.Status.Carriers.NoCarrierText"> @@ -1670,6 +1665,10 @@ <item name="android:colorBackground">@color/transparent</item> </style> + <style name="ShortcutHelperBottomSheet" parent="@style/Widget.Material3.BottomSheet"> + <item name="backgroundTint">?colorSurfaceContainer</item> + </style> + <style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity"> <item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item> <item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item> diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/PromptKind.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/PromptKind.kt index b99c51489521..44f2059d4e41 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/PromptKind.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/PromptKind.kt @@ -22,15 +22,28 @@ sealed interface PromptKind { data class Biometric( /** The available modalities for the authentication on the prompt. */ val activeModalities: BiometricModalities = BiometricModalities(), - // TODO(b/330908557): Use this value to decide whether to show two pane layout, instead of - // simply depending on rotations. - val showTwoPane: Boolean = false, - ) : PromptKind + val paneType: PaneType = PaneType.ONE_PANE_PORTRAIT, + ) : PromptKind { + enum class PaneType { + TWO_PANE_LANDSCAPE, + ONE_PANE_PORTRAIT, + ONE_PANE_NO_SENSOR_LANDSCAPE, + ONE_PANE_LARGE_SCREEN_LANDSCAPE + } + } - object Pin : PromptKind - object Pattern : PromptKind - object Password : PromptKind + data object Pin : PromptKind + data object Pattern : PromptKind + data object Password : PromptKind fun isBiometric() = this is Biometric - fun isCredential() = (this is Pin) or (this is Pattern) or (this is Password) + fun isTwoPaneLandscapeBiometric(): Boolean = + (this as? Biometric)?.paneType == Biometric.PaneType.TWO_PANE_LANDSCAPE + fun isOnePanePortraitBiometric() = + (this as? Biometric)?.paneType == Biometric.PaneType.ONE_PANE_PORTRAIT + fun isOnePaneNoSensorLandscapeBiometric() = + (this as? Biometric)?.paneType == Biometric.PaneType.ONE_PANE_NO_SENSOR_LANDSCAPE + fun isOnePaneLargeScreenLandscapeBiometric() = + (this as? Biometric)?.paneType == Biometric.PaneType.ONE_PANE_LARGE_SCREEN_LANDSCAPE + fun isCredential() = (this is Pin) || (this is Pattern) || (this is Password) } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java index 019f498a01f8..f905241addeb 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java @@ -269,6 +269,7 @@ public class BouncerSwipeTouchHandler implements TouchHandler { } mScrimManager.removeCallback(mScrimManagerCallback); mCapture = null; + mTouchSession = null; if (!Flags.communalBouncerDoNotModifyPluginOpen()) { mNotificationShadeWindowController.setForcePluginOpen(false, this); diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java index 227e4dba5d04..61b4401d6b22 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java @@ -49,11 +49,14 @@ import com.android.systemui.util.display.DisplayHelper; import com.google.common.util.concurrent.ListenableFuture; +import kotlinx.coroutines.Job; + import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import java.util.concurrent.CancellationException; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -78,15 +81,7 @@ public class TouchMonitor { private final Lifecycle mLifecycle; private Rect mExclusionRect = null; - private ISystemGestureExclusionListener mGestureExclusionListener = - new ISystemGestureExclusionListener.Stub() { - @Override - public void onSystemGestureExclusionChanged(int displayId, - Region systemGestureExclusion, - Region systemGestureExclusionUnrestricted) { - mExclusionRect = systemGestureExclusion.getBounds(); - } - }; + private ISystemGestureExclusionListener mGestureExclusionListener; private Consumer<Rect> mMaxBoundsConsumer = rect -> mMaxBounds = rect; @@ -274,6 +269,14 @@ public class TouchMonitor { if (bouncerAreaExclusion()) { mBackgroundExecutor.execute(() -> { try { + mGestureExclusionListener = new ISystemGestureExclusionListener.Stub() { + @Override + public void onSystemGestureExclusionChanged(int displayId, + Region systemGestureExclusion, + Region systemGestureExclusionUnrestricted) { + mExclusionRect = systemGestureExclusion.getBounds(); + } + }; mWindowManagerService.registerSystemGestureExclusionListener( mGestureExclusionListener, mDisplayId); } catch (RemoteException e) { @@ -298,8 +301,11 @@ public class TouchMonitor { if (bouncerAreaExclusion()) { mBackgroundExecutor.execute(() -> { try { - mWindowManagerService.unregisterSystemGestureExclusionListener( - mGestureExclusionListener, mDisplayId); + if (mGestureExclusionListener != null) { + mWindowManagerService.unregisterSystemGestureExclusionListener( + mGestureExclusionListener, mDisplayId); + mGestureExclusionListener = null; + } } catch (RemoteException e) { // Handle the exception Log.e(TAG, "unregisterSystemGestureExclusionListener: failed", e); @@ -494,6 +500,10 @@ public class TouchMonitor { private Rect mMaxBounds; + private Job mBoundsFlow; + + private boolean mInitialized; + /** * Designated constructor for {@link TouchMonitor} @@ -535,10 +545,35 @@ public class TouchMonitor { * Initializes the monitor. should only be called once after creation. */ public void init() { + if (mInitialized) { + throw new IllegalStateException("TouchMonitor already initialized"); + } + mLifecycle.addObserver(mLifecycleObserver); if (Flags.ambientTouchMonitorListenToDisplayChanges()) { - collectFlow(mLifecycle, mConfigurationInteractor.getMaxBounds(), mMaxBoundsConsumer); + mBoundsFlow = collectFlow(mLifecycle, mConfigurationInteractor.getMaxBounds(), + mMaxBoundsConsumer); } + + mInitialized = true; + } + + /** + * Called when the TouchMonitor should be discarded and will not be used anymore. + */ + public void destroy() { + if (!mInitialized) { + throw new IllegalStateException("TouchMonitor not initialized"); + } + + stopMonitoring(true); + + mLifecycle.removeObserver(mLifecycleObserver); + if (Flags.ambientTouchMonitorListenToDisplayChanges()) { + mBoundsFlow.cancel(new CancellationException()); + } + + mInitialized = false; } private void isolate(Set<TouchSessionImpl> sessions) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index b75b292be597..1ee4908437a6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -29,6 +29,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AlertDialog; import android.content.Context; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PixelFormat; @@ -360,15 +361,23 @@ public class AuthContainerView extends LinearLayout Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds), Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds)); + final boolean isLandscape = mContext.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; mPromptSelectorInteractorProvider = promptSelectorInteractorProvider; mPromptSelectorInteractorProvider.get().setPrompt(mConfig.mPromptInfo, mEffectiveUserId, getRequestId(), biometricModalities, mConfig.mOperationId, mConfig.mOpPackageName, - false /*onSwitchToCredential*/); + false /*onSwitchToCredential*/, isLandscape); final LayoutInflater layoutInflater = LayoutInflater.from(mContext); - if (constraintBp() && mPromptViewModel.getPromptKind().getValue().isBiometric()) { - mLayout = (ConstraintLayout) layoutInflater.inflate( - R.layout.biometric_prompt_constraint_layout, this, false /* attachToRoot */); + final PromptKind kind = mPromptViewModel.getPromptKind().getValue(); + if (constraintBp() && kind.isBiometric()) { + if (kind.isTwoPaneLandscapeBiometric()) { + mLayout = (ConstraintLayout) layoutInflater.inflate( + R.layout.biometric_prompt_two_pane_layout, this, false /* attachToRoot */); + } else { + mLayout = (ConstraintLayout) layoutInflater.inflate( + R.layout.biometric_prompt_one_pane_layout, this, false /* attachToRoot */); + } } else { mLayout = (FrameLayout) layoutInflater.inflate( R.layout.auth_container_view, this, false /* attachToRoot */); @@ -631,7 +640,7 @@ public class AuthContainerView extends LinearLayout if (fpProp != null && fpProp.isAnyUdfpsType()) { maybeUpdatePositionForUdfps(forceInvalidate /* invalidate */); } - if (faceProp != null && mBiometricView.isFaceOnly()) { + if (faceProp != null && mBiometricView != null && mBiometricView.isFaceOnly()) { alwaysUpdatePositionAtScreenBottom(forceInvalidate /* invalidate */); } if (fpProp != null && fpProp.sensorType == TYPE_POWER_BUTTON) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt index 8e5a97bd5d8d..9b14d6f68e35 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt @@ -29,11 +29,10 @@ import com.android.systemui.display.data.repository.DeviceStateRepository import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState.REAR_DISPLAY import com.android.systemui.display.data.repository.DisplayRepository import javax.inject.Inject +import kotlin.math.min import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -58,7 +57,7 @@ interface DisplayStateRepository { val currentDisplaySize: StateFlow<Size> /** Provides whether the current display is large screen */ - val isLargeScreen: Flow<Boolean> + val isLargeScreen: StateFlow<Boolean> } @SysUISingleton @@ -127,16 +126,29 @@ constructor( ), ) - override val isLargeScreen: Flow<Boolean> = + override val isLargeScreen: StateFlow<Boolean> = currentDisplayInfo .map { - // TODO: This works, but investigate better way to handle this - it.logicalWidth * 160 / it.logicalDensityDpi > DisplayMetrics.DENSITY_XXXHIGH && - it.logicalHeight * 160 / it.logicalDensityDpi > DisplayMetrics.DENSITY_XXHIGH + // copied from systemui/shared/...Utilities.java + val smallestWidth = + dpiFromPx( + min(it.logicalWidth, it.logicalHeight).toFloat(), + context.resources.configuration.densityDpi + ) + smallestWidth >= LARGE_SCREEN_MIN_DPS } - .distinctUntilChanged() + .stateIn( + backgroundScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + private fun dpiFromPx(size: Float, densityDpi: Int): Float { + val densityRatio = densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT + return size / densityRatio + } companion object { const val TAG = "DisplayStateRepositoryImpl" + const val LARGE_SCREEN_MIN_DPS = 600f } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt index 591da4096956..40313e3158aa 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt @@ -65,7 +65,8 @@ interface DisplayStateInteractor { /** Called on configuration changes, used to keep the display state in sync */ fun onConfigurationChanged(newConfig: Configuration) - val isLargeScreen: Flow<Boolean> + /** Provides whether the current display is large screen */ + val isLargeScreen: StateFlow<Boolean> } /** Encapsulates logic for interacting with the display state. */ @@ -127,7 +128,7 @@ constructor( override val isDefaultDisplayOff = displayRepository.defaultDisplayOff - override val isLargeScreen: Flow<Boolean> = displayStateRepository.isLargeScreen + override val isLargeScreen: StateFlow<Boolean> = displayStateRepository.isLargeScreen companion object { private const val TAG = "DisplayStateInteractor" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt index a74b0b07299c..b8ff3bb43203 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt @@ -98,11 +98,11 @@ constructor( ) { unscaledSensorLocation, scale -> val sensorLocation = SensorLocation( - unscaledSensorLocation.sensorLocationX, - unscaledSensorLocation.sensorLocationY, - unscaledSensorLocation.sensorRadius, + naturalCenterX = unscaledSensorLocation.sensorLocationX, + naturalCenterY = unscaledSensorLocation.sensorLocationY, + naturalRadius = unscaledSensorLocation.sensorRadius, + scale = scale ) - sensorLocation.scale = scale sensorLocation } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index dc338d07f9e7..c08756f6ae36 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -91,6 +91,7 @@ interface PromptSelectorInteractor { challenge: Long, opPackageName: String, onSwitchToCredential: Boolean, + isLandscape: Boolean, ) /** Unset the current authentication request. */ @@ -102,6 +103,7 @@ class PromptSelectorInteractorImpl @Inject constructor( fingerprintPropertyRepository: FingerprintPropertyRepository, + private val displayStateInteractor: DisplayStateInteractor, private val promptRepository: PromptRepository, private val lockPatternUtils: LockPatternUtils, ) : PromptSelectorInteractor { @@ -166,7 +168,9 @@ constructor( modalities, promptRepository.challenge.value!!, promptRepository.opPackageName.value!!, - true /*onSwitchToCredential*/ + onSwitchToCredential = true, + // isLandscape value is not important when onSwitchToCredential is true + isLandscape = false, ) } @@ -178,6 +182,7 @@ constructor( challenge: Long, opPackageName: String, onSwitchToCredential: Boolean, + isLandscape: Boolean, ) { val hasCredentialViewShown = promptKind.value.isCredential() val showBpForCredential = @@ -189,11 +194,30 @@ constructor( !promptInfo.isContentViewMoreOptionsButtonUsed val showBpWithoutIconForCredential = showBpForCredential && !hasCredentialViewShown var kind: PromptKind = PromptKind.None + if (onSwitchToCredential) { kind = getCredentialType(lockPatternUtils, effectiveUserId) } else if (Utils.isBiometricAllowed(promptInfo) || showBpWithoutIconForCredential) { - // TODO(b/330908557): check to show one pane or two pane - kind = PromptKind.Biometric(modalities) + // TODO(b/330908557): Subscribe to + // displayStateInteractor.currentRotation.value.isDefaultOrientation() for checking + // `isLandscape` after removing AuthContinerView. + kind = + if (isLandscape) { + val paneType = + when { + displayStateInteractor.isLargeScreen.value -> + PromptKind.Biometric.PaneType.ONE_PANE_LARGE_SCREEN_LANDSCAPE + showBpWithoutIconForCredential -> + PromptKind.Biometric.PaneType.ONE_PANE_NO_SENSOR_LANDSCAPE + else -> PromptKind.Biometric.PaneType.TWO_PANE_LANDSCAPE + } + PromptKind.Biometric( + modalities, + paneType = paneType, + ) + } else { + PromptKind.Biometric(modalities) + } } else if (isDeviceCredentialAllowed(promptInfo)) { kind = getCredentialType(lockPatternUtils, effectiveUserId) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt index dddadbd5e036..2f2f3a35dbaa 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt @@ -16,18 +16,18 @@ package com.android.systemui.biometrics.shared.model -/** Provides current sensor location information in the current screen resolution [scale]. */ +/** + * Provides current sensor location information in the current screen resolution [scale]. + * + * @property scale Scale to apply to the sensor location's natural parameters to support different + * screen resolutions. + */ data class SensorLocation( private val naturalCenterX: Int, private val naturalCenterY: Int, - private val naturalRadius: Int + private val naturalRadius: Int, + private val scale: Float = 1f ) { - /** - * Scale to apply to the sensor location's natural parameters to support different screen - * resolutions. - */ - var scale: Float = 1f - val centerX: Float get() { return naturalCenterX * scale diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index 47174c006735..c836f89a8ff4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -93,6 +93,7 @@ object BiometricViewSizeBinder { if (constraintBp()) { val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline) + val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline) val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline) val midGuideline = view.findViewById<Guideline>(R.id.midGuideline) @@ -355,6 +356,18 @@ object BiometricViewSizeBinder { ) } + if (bounds.top >= 0) { + mediumConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) + smallConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) + } else if (bounds.top < 0) { + mediumConstraintSet.setGuidelineEnd( + topGuideline.id, + abs(bounds.top) + ) + smallConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top)) + } + + // Use rect bottom to set mid guideline of two-pane. if (midGuideline != null) { if (bounds.bottom >= 0) { midGuideline.setGuidelineEnd(bounds.bottom) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt index fcc69927c2b3..9e836c31c177 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt @@ -17,7 +17,9 @@ package com.android.systemui.biometrics.ui.binder +import android.graphics.drawable.Animatable2 import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView @@ -28,8 +30,8 @@ import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.res.R import com.android.systemui.util.kotlin.Utils.Companion.toQuad +import com.android.systemui.util.kotlin.Utils.Companion.toQuint import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import kotlinx.coroutines.flow.combine @@ -61,6 +63,16 @@ object PromptIconViewBinder { } var faceIcon: AnimatedVectorDrawable? = null + val faceIconCallback = + object : Animatable2.AnimationCallback() { + override fun onAnimationStart(drawable: Drawable) { + viewModel.onAnimationStart() + } + + override fun onAnimationEnd(drawable: Drawable) { + viewModel.onAnimationEnd() + } + } if (!constraintBp()) { launch { @@ -126,13 +138,19 @@ object PromptIconViewBinder { combine( viewModel.activeAuthType, viewModel.shouldAnimateIconView, + viewModel.shouldRepeatAnimation, viewModel.showingError, - ::Triple + ::toQuad ), - ::toQuad + ::toQuint ) - .collect { (iconAsset, activeAuthType, shouldAnimateIconView, showingError) - -> + .collect { + ( + iconAsset, + activeAuthType, + shouldAnimateIconView, + shouldRepeatAnimation, + showingError) -> if (iconAsset != -1) { when (activeAuthType) { AuthType.Fingerprint, @@ -145,27 +163,21 @@ object PromptIconViewBinder { } } AuthType.Face -> { - // TODO(b/318569643): Consolidate logic once all face auth - // assets are migrated from drawable to json - if (iconAsset == R.raw.face_dialog_authenticating) { - iconView.setAnimation(iconAsset) - iconView.frame = 0 - + faceIcon?.apply { + unregisterAnimationCallback(faceIconCallback) + stop() + } + faceIcon = + iconView.context.getDrawable(iconAsset) + as AnimatedVectorDrawable + faceIcon?.apply { + iconView.setImageDrawable(this) if (shouldAnimateIconView) { - iconView.playAnimation() - iconView.loop(true) - } - } else { - faceIcon?.apply { stop() } - faceIcon = - iconView.context.getDrawable(iconAsset) - as AnimatedVectorDrawable - faceIcon?.apply { - iconView.setImageDrawable(this) - if (shouldAnimateIconView) { - forceAnimationOnUI() - start() + forceAnimationOnUI() + if (shouldRepeatAnimation) { + registerAnimationCallback(faceIconCallback) } + start() } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt index 901d7517c5e9..bde3e992a295 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt @@ -21,6 +21,7 @@ import android.annotation.DrawableRes import android.annotation.RawRes import android.content.res.Configuration import android.graphics.Rect +import android.hardware.face.Face import android.util.RotationUtils import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor @@ -31,10 +32,12 @@ import com.android.systemui.res.R import com.android.systemui.util.kotlin.combine import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map /** * Models UI of [BiometricPromptLayout.iconView] and [BiometricPromptLayout.biometric_icon_overlay] @@ -55,8 +58,11 @@ constructor( } /** - * Indicates what auth type the UI currently displays. Fingerprint-only auth -> Fingerprint - * Face-only auth -> Face Co-ex auth, implicit flow -> Face Co-ex auth, explicit flow -> Coex + * Indicates what auth type the UI currently displays. + * Fingerprint-only auth -> Fingerprint + * Face-only auth -> Face + * Co-ex auth, implicit flow -> Face + * Co-ex auth, explicit flow -> Coex */ val activeAuthType: Flow<AuthType> = combine( @@ -113,6 +119,35 @@ constructor( _previousIconOverlayWasError.value = previousIconOverlayWasError } + /** Called when iconView begins animating. */ + fun onAnimationStart() { + _animationEnded.value = false + } + + /** Called when iconView ends animating. */ + fun onAnimationEnd() { + _animationEnded.value = true + } + + private val _animationEnded: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** + * Whether a face iconView should pulse (i.e. while isAuthenticating and previous animation + * ended). + */ + val shouldPulseAnimation: Flow<Boolean> = + combine(_animationEnded, promptViewModel.isAuthenticating) { + animationEnded, + isAuthenticating -> + animationEnded && isAuthenticating + } + .distinctUntilChanged() + + private val _lastPulseLightToDark: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** Tracks whether a face iconView last pulsed light to dark (vs. dark to light) */ + val lastPulseLightToDark: Flow<Boolean> = _lastPulseLightToDark.asStateFlow() + val iconSize: Flow<Pair<Int, Int>> = combine( promptViewModel.position, @@ -160,22 +195,35 @@ constructor( } } AuthType.Face -> - combine( - promptViewModel.isAuthenticated.distinctUntilChanged(), - promptViewModel.isAuthenticating.distinctUntilChanged(), - promptViewModel.isPendingConfirmation.distinctUntilChanged(), - promptViewModel.showingError.distinctUntilChanged() - ) { - authState: PromptAuthState, - isAuthenticating: Boolean, - isPendingConfirmation: Boolean, - showingError: Boolean -> - getFaceIconViewAsset( - authState, - isAuthenticating, - isPendingConfirmation, - showingError - ) + shouldPulseAnimation.flatMapLatest { shouldPulseAnimation: Boolean -> + if (shouldPulseAnimation) { + val iconAsset = + if (_lastPulseLightToDark.value) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + _lastPulseLightToDark.value = !_lastPulseLightToDark.value + flowOf(iconAsset) + } else { + combine( + promptViewModel.isAuthenticated.distinctUntilChanged(), + promptViewModel.isAuthenticating.distinctUntilChanged(), + promptViewModel.isPendingConfirmation.distinctUntilChanged(), + promptViewModel.showingError.distinctUntilChanged() + ) { + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + getFaceIconViewAsset( + authState, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + } } AuthType.Coex -> combine( @@ -279,7 +327,8 @@ constructor( } else if (authState.isAuthenticated) { R.drawable.face_dialog_dark_to_checkmark } else if (isAuthenticating) { - R.raw.face_dialog_authenticating + _lastPulseLightToDark.value = false + R.drawable.face_dialog_pulse_dark_to_light } else if (showingError) { R.drawable.face_dialog_dark_to_error } else if (_previousIconWasError.value) { @@ -654,6 +703,16 @@ constructor( } } + /** Whether the current BiometricPromptLayout.iconView asset animation should be repeated. */ + val shouldRepeatAnimation: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> flowOf(false) + AuthType.Face -> promptViewModel.isAuthenticating.map { it } + } + } + /** Called on configuration changes */ fun onConfigurationChanged(newConfig: Configuration) { displayStateInteractor.onConfigurationChanged(newConfig) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 156ec6b975a5..c17b83dd4fbe 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -261,10 +261,13 @@ constructor( combine( _forceLargeSize, displayStateInteractor.isLargeScreen, - displayStateInteractor.currentRotation + displayStateInteractor.currentRotation, ) { forceLarge, isLargeScreen, rotation -> when { - forceLarge || isLargeScreen -> PromptPosition.Bottom + forceLarge || + isLargeScreen || + promptKind.value.isOnePaneNoSensorLandscapeBiometric() -> + PromptPosition.Bottom rotation == DisplayRotation.ROTATION_90 -> PromptPosition.Right rotation == DisplayRotation.ROTATION_270 -> PromptPosition.Left rotation == DisplayRotation.ROTATION_180 -> PromptPosition.Top @@ -297,23 +300,27 @@ constructor( /** Prompt panel size padding */ private val smallHorizontalGuidelinePadding = context.resources.getDimensionPixelSize( - R.dimen.biometric_prompt_small_horizontal_guideline_padding + R.dimen.biometric_prompt_land_small_horizontal_guideline_padding ) private val udfpsHorizontalGuidelinePadding = context.resources.getDimensionPixelSize( - R.dimen.biometric_prompt_udfps_horizontal_guideline_padding + R.dimen.biometric_prompt_two_pane_udfps_horizontal_guideline_padding ) private val udfpsMidGuidelinePadding = context.resources.getDimensionPixelSize( - R.dimen.biometric_prompt_udfps_mid_guideline_padding + R.dimen.biometric_prompt_two_pane_udfps_mid_guideline_padding + ) + private val mediumTopGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_one_pane_medium_top_guideline_padding ) private val mediumHorizontalGuidelinePadding = context.resources.getDimensionPixelSize( - R.dimen.biometric_prompt_medium_horizontal_guideline_padding + R.dimen.biometric_prompt_two_pane_medium_horizontal_guideline_padding ) private val mediumMidGuidelinePadding = context.resources.getDimensionPixelSize( - R.dimen.biometric_prompt_medium_mid_guideline_padding + R.dimen.biometric_prompt_two_pane_medium_mid_guideline_padding ) /** Rect for positioning biometric icon */ @@ -416,9 +423,9 @@ constructor( * asset to be loaded before determining the prompt size. */ val isIconViewLoaded: Flow<Boolean> = - combine(modalities, _isIconViewLoaded.asStateFlow()) { modalities, isIconViewLoaded -> - val noIcon = modalities.isEmpty - noIcon || isIconViewLoaded + combine(hideSensorIcon, _isIconViewLoaded.asStateFlow()) { hideSensorIcon, isIconViewLoaded + -> + hideSensorIcon || isIconViewLoaded } .distinctUntilChanged() @@ -448,17 +455,24 @@ constructor( * from opposite side of the screen */ val guidelineBounds: Flow<Rect> = - combine(iconPosition, size, position, modalities) { _, size, position, modalities -> + combine(iconPosition, promptKind, size, position, modalities) { + _, + promptKind, + size, + position, + modalities -> when (position) { - PromptPosition.Bottom -> Rect(0, 0, 0, 0) + PromptPosition.Bottom -> + if (promptKind.isOnePaneNoSensorLandscapeBiometric()) { + Rect(0, 0, 0, 0) + } else { + Rect(0, mediumTopGuidelinePadding, 0, 0) + } PromptPosition.Right -> if (size.isSmall) { Rect(-smallHorizontalGuidelinePadding, 0, 0, 0) } else if (modalities.hasUdfps) { Rect(udfpsHorizontalGuidelinePadding, 0, 0, udfpsMidGuidelinePadding) - } else if (modalities.isEmpty) { - // TODO: Temporary fix until no biometric landscape layout is added - Rect(-mediumHorizontalGuidelinePadding, 0, 0, 6) } else { Rect(-mediumHorizontalGuidelinePadding, 0, 0, mediumMidGuidelinePadding) } @@ -467,9 +481,6 @@ constructor( Rect(0, 0, -smallHorizontalGuidelinePadding, 0) } else if (modalities.hasUdfps) { Rect(0, 0, udfpsHorizontalGuidelinePadding, -udfpsMidGuidelinePadding) - } else if (modalities.isEmpty) { - // TODO: Temporary fix until no biometric landscape layout is added - Rect(0, 0, -mediumHorizontalGuidelinePadding, -6) } else { Rect( 0, diff --git a/packages/SystemUI/src/com/android/systemui/brightness/dagger/ScreenBrightnessModule.kt b/packages/SystemUI/src/com/android/systemui/brightness/dagger/ScreenBrightnessModule.kt index 2b9fc73458d8..7a9429e56c88 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/dagger/ScreenBrightnessModule.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/dagger/ScreenBrightnessModule.kt @@ -20,8 +20,15 @@ import com.android.systemui.brightness.data.repository.BrightnessPolicyRepositor import com.android.systemui.brightness.data.repository.BrightnessPolicyRepositoryImpl import com.android.systemui.brightness.data.repository.ScreenBrightnessDisplayManagerRepository import com.android.systemui.brightness.data.repository.ScreenBrightnessRepository +import com.android.systemui.brightness.shared.model.BrightnessLog +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogBufferFactory +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory import dagger.Binds import dagger.Module +import dagger.Provides @Module interface ScreenBrightnessModule { @@ -33,4 +40,20 @@ interface ScreenBrightnessModule { @Binds fun bindPolicyRepository(impl: BrightnessPolicyRepositoryImpl): BrightnessPolicyRepository + + companion object { + @Provides + @SysUISingleton + @BrightnessLog + fun providesBrightnessTableLog(factory: TableLogBufferFactory): TableLogBuffer { + return factory.create("BrightnessTableLog", 50) + } + + @Provides + @SysUISingleton + @BrightnessLog + fun providesBrightnessLog(factory: LogBufferFactory): LogBuffer { + return factory.create("BrightnessLog", 50) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt index 9ed11d13d4d4..37d1887730b9 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt @@ -19,12 +19,18 @@ package com.android.systemui.brightness.data.repository import android.annotation.SuppressLint import android.hardware.display.BrightnessInfo import android.hardware.display.DisplayManager -import com.android.systemui.brightness.data.model.LinearBrightness +import com.android.systemui.brightness.shared.model.BrightnessLog +import com.android.systemui.brightness.shared.model.LinearBrightness +import com.android.systemui.brightness.shared.model.formatBrightness +import com.android.systemui.brightness.shared.model.logDiffForTable import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.DisplayId +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.table.TableLogBuffer import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -32,13 +38,13 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -78,6 +84,8 @@ class ScreenBrightnessDisplayManagerRepository constructor( @DisplayId private val displayId: Int, private val displayManager: DisplayManager, + @BrightnessLog private val logBuffer: LogBuffer, + @BrightnessLog private val tableBuffer: TableLogBuffer, @Application private val applicationScope: CoroutineScope, @Background private val backgroundContext: CoroutineContext, ) : ScreenBrightnessRepository { @@ -100,6 +108,7 @@ constructor( displayManager.setBrightness(displayId, value) } } + logBrightnessChange(call is SetBrightnessMethod.Permanent, value) } } } @@ -147,13 +156,15 @@ constructor( brightnessInfo .filterNotNull() .map { LinearBrightness(it.brightnessMinimum) } - .shareIn(applicationScope, SharingStarted.WhileSubscribed()) + .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_MIN, null) + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(0f)) - override val maxLinearBrightness = + override val maxLinearBrightness: SharedFlow<LinearBrightness> = brightnessInfo .filterNotNull() .map { LinearBrightness(it.brightnessMaximum) } - .shareIn(applicationScope, SharingStarted.WhileSubscribed()) + .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_MAX, null) + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(1f)) override suspend fun getMinMaxLinearBrightness(): Pair<LinearBrightness, LinearBrightness> { val brightnessInfo = brightnessInfo.value ?: brightnessInfoValue() @@ -166,7 +177,8 @@ constructor( brightnessInfo .filterNotNull() .map { LinearBrightness(it.brightness) } - .shareIn(applicationScope, SharingStarted.WhileSubscribed()) + .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_BRIGHTNESS, null) + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(0f)) override fun setTemporaryBrightness(value: LinearBrightness) { apiQueue.trySend(SetBrightnessMethod.Temporary(value)) @@ -183,4 +195,21 @@ constructor( @JvmInline value class Permanent(override val value: LinearBrightness) : SetBrightnessMethod } + + private fun logBrightnessChange(permanent: Boolean, value: Float) { + logBuffer.log( + LOG_BUFFER_BRIGHTNESS_CHANGE_TAG, + if (permanent) LogLevel.DEBUG else LogLevel.VERBOSE, + { str1 = value.formatBrightness() }, + { "Change requested: $str1" } + ) + } + + private companion object { + const val TABLE_COLUMN_BRIGHTNESS = "brightness" + const val TABLE_COLUMN_MIN = "min" + const val TABLE_COLUMN_MAX = "max" + const val TABLE_PREFIX_LINEAR = "linear" + const val LOG_BUFFER_BRIGHTNESS_CHANGE_TAG = "BrightnessChange" + } } diff --git a/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt b/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt index 799a0a14c99d..5647f521762f 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt @@ -17,12 +17,20 @@ package com.android.systemui.brightness.domain.interactor import com.android.settingslib.display.BrightnessUtils -import com.android.systemui.brightness.data.model.LinearBrightness import com.android.systemui.brightness.data.repository.ScreenBrightnessRepository -import com.android.systemui.brightness.shared.GammaBrightness +import com.android.systemui.brightness.shared.model.BrightnessLog +import com.android.systemui.brightness.shared.model.GammaBrightness +import com.android.systemui.brightness.shared.model.LinearBrightness +import com.android.systemui.brightness.shared.model.logDiffForTable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn /** * Converts between [GammaBrightness] and [LinearBrightness]. @@ -34,6 +42,8 @@ class ScreenBrightnessInteractor @Inject constructor( private val screenBrightnessRepository: ScreenBrightnessRepository, + @Application private val applicationScope: CoroutineScope, + @BrightnessLog private val tableBuffer: TableLogBuffer, ) { /** Maximum value in the Gamma space for brightness */ val maxGammaBrightness = GammaBrightness(BrightnessUtils.GAMMA_SPACE_MAX) @@ -45,15 +55,17 @@ constructor( * Brightness in the Gamma space for the current display. It will always represent a value * between [minGammaBrightness] and [maxGammaBrightness] */ - val gammaBrightness = + val gammaBrightness: Flow<GammaBrightness> = with(screenBrightnessRepository) { combine( - linearBrightness, - minLinearBrightness, - maxLinearBrightness, - ) { brightness, min, max -> - brightness.toGammaBrightness(min, max) - } + linearBrightness, + minLinearBrightness, + maxLinearBrightness, + ) { brightness, min, max -> + brightness.toGammaBrightness(min, max) + } + .logDiffForTable(tableBuffer, TABLE_PREFIX_GAMMA, TABLE_COLUMN_BRIGHTNESS, null) + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), GammaBrightness(0)) } /** Sets the brightness temporarily, while the user is changing it. */ @@ -91,4 +103,9 @@ constructor( BrightnessUtils.convertLinearToGammaFloat(floatValue, min.floatValue, max.floatValue) ) } + + private companion object { + const val TABLE_COLUMN_BRIGHTNESS = "brightness" + const val TABLE_PREFIX_GAMMA = "gamma" + } } diff --git a/packages/SystemUI/src/com/android/systemui/brightness/shared/GammaBrightness.kt b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/BrightnessLog.kt index e20d003bb989..b514fefbff0e 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/shared/GammaBrightness.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/BrightnessLog.kt @@ -14,16 +14,11 @@ * limitations under the License. */ -package com.android.systemui.brightness.shared +package com.android.systemui.brightness.shared.model -import androidx.annotation.IntRange -import com.android.settingslib.display.BrightnessUtils +import javax.inject.Qualifier -@JvmInline -value class GammaBrightness( - @IntRange( - from = BrightnessUtils.GAMMA_SPACE_MIN.toLong(), - to = BrightnessUtils.GAMMA_SPACE_MAX.toLong() - ) - val value: Int -) +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class BrightnessLog() diff --git a/packages/SystemUI/src/com/android/systemui/brightness/shared/model/GammaBrightness.kt b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/GammaBrightness.kt new file mode 100644 index 000000000000..7eba6268869c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/GammaBrightness.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.brightness.shared.model + +import androidx.annotation.IntRange +import com.android.settingslib.display.BrightnessUtils +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.util.kotlin.pairwiseBy +import kotlinx.coroutines.flow.Flow + +@JvmInline +value class GammaBrightness( + @IntRange( + from = BrightnessUtils.GAMMA_SPACE_MIN.toLong(), + to = BrightnessUtils.GAMMA_SPACE_MAX.toLong() + ) + val value: Int +) + +internal fun Flow<GammaBrightness>.logDiffForTable( + tableLogBuffer: TableLogBuffer, + columnPrefix: String, + columnName: String, + initialValue: GammaBrightness?, +): Flow<GammaBrightness> { + val initialValueFun = { + tableLogBuffer.logChange(columnPrefix, columnName, initialValue?.value, isInitial = true) + initialValue + } + return this.pairwiseBy(initialValueFun) { prevVal: GammaBrightness?, newVal: GammaBrightness -> + if (prevVal != newVal) { + tableLogBuffer.logChange(columnPrefix, columnName, newVal.value) + } + newVal + } +} diff --git a/packages/SystemUI/src/com/android/systemui/brightness/shared/model/LinearBrightness.kt b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/LinearBrightness.kt new file mode 100644 index 000000000000..1c886e6b1477 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/LinearBrightness.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.brightness.shared.model + +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.util.kotlin.pairwiseBy +import kotlinx.coroutines.flow.Flow + +@JvmInline +value class LinearBrightness(val floatValue: Float) { + fun clamp(min: LinearBrightness, max: LinearBrightness): LinearBrightness { + return if (floatValue < min.floatValue) { + min + } else if (floatValue > max.floatValue) { + max + } else { + this + } + } + + val loggableString: String + get() = floatValue.formatBrightness() +} + +fun Float.formatBrightness(): String { + return "%.3f".format(this) +} + +internal fun Flow<LinearBrightness>.logDiffForTable( + tableLogBuffer: TableLogBuffer, + columnPrefix: String, + columnName: String, + initialValue: LinearBrightness?, +): Flow<LinearBrightness> { + val initialValueFun = { + tableLogBuffer.logChange( + columnPrefix, + columnName, + initialValue?.loggableString, + isInitial = true + ) + initialValue + } + return this.pairwiseBy(initialValueFun) { prevVal: LinearBrightness?, newVal: LinearBrightness + -> + if (prevVal != newVal) { + tableLogBuffer.logChange(columnPrefix, columnName, newVal.loggableString) + } + newVal + } +} diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index a51d8ff4faa5..f991d5b8405f 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -33,14 +33,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.PlatformSlider -import com.android.systemui.brightness.shared.GammaBrightness +import com.android.systemui.brightness.shared.model.GammaBrightness import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel import com.android.systemui.brightness.ui.viewmodel.Drag import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.common.ui.compose.Icon import com.android.systemui.utils.PolicyRestriction -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @Composable @@ -107,8 +106,8 @@ fun BrightnessSliderContainer( viewModel: BrightnessSliderViewModel, modifier: Modifier = Modifier, ) { - val gamma: Int by - viewModel.currentBrightness.map { it.value }.collectAsStateWithLifecycle(initialValue = 0) + val state by viewModel.currentBrightness.collectAsStateWithLifecycle() + val gamma = state.value val coroutineScope = rememberCoroutineScope() val restriction by viewModel.policyRestriction.collectAsStateWithLifecycle( diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt index f0988ba96bcd..16a1dcc0aef5 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt @@ -18,14 +18,18 @@ package com.android.systemui.brightness.ui.viewmodel import com.android.systemui.brightness.domain.interactor.BrightnessPolicyEnforcementInteractor import com.android.systemui.brightness.domain.interactor.ScreenBrightnessInteractor -import com.android.systemui.brightness.shared.GammaBrightness +import com.android.systemui.brightness.shared.model.GammaBrightness import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R import com.android.systemui.utils.PolicyRestriction import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn @SysUISingleton class BrightnessSliderViewModel @@ -33,8 +37,14 @@ class BrightnessSliderViewModel constructor( private val screenBrightnessInteractor: ScreenBrightnessInteractor, private val brightnessPolicyEnforcementInteractor: BrightnessPolicyEnforcementInteractor, + @Application private val applicationScope: CoroutineScope, ) { - val currentBrightness = screenBrightnessInteractor.gammaBrightness + val currentBrightness = + screenBrightnessInteractor.gammaBrightness.stateIn( + applicationScope, + SharingStarted.WhileSubscribed(), + GammaBrightness(0) + ) val maxBrightness = screenBrightnessInteractor.maxGammaBrightness val minBrightness = screenBrightnessInteractor.minGammaBrightness diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt index d6d08b4f1208..260dcbad6201 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt @@ -22,6 +22,7 @@ import com.android.compose.animation.scene.TransitionKey import com.android.systemui.communal.dagger.Communal import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.shared.model.SceneDataSource import javax.inject.Inject @@ -34,6 +35,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** Encapsulates the state of communal mode. */ interface CommunalSceneRepository { @@ -64,6 +66,7 @@ interface CommunalSceneRepository { class CommunalSceneRepositoryImpl @Inject constructor( + @Application private val applicationScope: CoroutineScope, @Background backgroundScope: CoroutineScope, @Communal private val sceneDataSource: SceneDataSource, ) : CommunalSceneRepository { @@ -82,11 +85,19 @@ constructor( ) override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { - sceneDataSource.changeScene(toScene, transitionKey) + applicationScope.launch { + // SceneTransitionLayout state updates must be triggered on the thread the STL was + // created on. + sceneDataSource.changeScene(toScene, transitionKey) + } } override fun snapToScene(toScene: SceneKey) { - sceneDataSource.snapToScene(toScene) + applicationScope.launch { + // SceneTransitionLayout state updates must be triggered on the thread the STL was + // created on. + sceneDataSource.snapToScene(toScene) + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/dock/DockManagerExtensions.kt b/packages/SystemUI/src/com/android/systemui/dock/DockManagerExtensions.kt index 4dbb32da62c2..1bbdfcd88548 100644 --- a/packages/SystemUI/src/com/android/systemui/dock/DockManagerExtensions.kt +++ b/packages/SystemUI/src/com/android/systemui/dock/DockManagerExtensions.kt @@ -19,16 +19,18 @@ package com.android.systemui.dock import com.android.systemui.common.coroutine.ConflatedCallbackFlow import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged /** - * Retrieves whether or not the device is docked according to DockManager. Emits a starting value - * of isDocked. + * Retrieves whether or not the device is docked according to DockManager. Emits a starting value of + * isDocked. */ fun DockManager.retrieveIsDocked(): Flow<Boolean> = ConflatedCallbackFlow.conflatedCallbackFlow { - val callback = DockManager.DockEventListener { trySend(isDocked) } - addListener(callback) - trySend(isDocked) + val callback = DockManager.DockEventListener { trySend(isDocked) } + addListener(callback) + trySend(isDocked) - awaitClose { removeListener(callback) } - }
\ No newline at end of file + awaitClose { removeListener(callback) } + } + .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index aa7a7dae8f5e..96e708fca451 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -543,7 +543,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mStateController.setEntryAnimationsFinished(false); mDreamOverlayContainerViewController = null; - mTouchMonitor = null; + + if (mTouchMonitor != null) { + mTouchMonitor.destroy(); + mTouchMonitor = null; + } mWindow = null; mStarted = false; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java index b0d134f5f15f..f6ac7a579140 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java @@ -33,8 +33,8 @@ import com.android.systemui.dreams.DreamOverlayNotificationCountProvider; import com.android.systemui.dreams.DreamOverlayService; import com.android.systemui.dreams.SystemDialogsCloser; import com.android.systemui.dreams.complication.dagger.ComplicationComponent; -import com.android.systemui.dreams.homecontrols.DreamActivityProvider; -import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl; +import com.android.systemui.dreams.homecontrols.DreamServiceDelegate; +import com.android.systemui.dreams.homecontrols.DreamServiceDelegateImpl; import com.android.systemui.dreams.homecontrols.HomeControlsDreamService; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.pipeline.shared.TileSpec; @@ -202,8 +202,8 @@ public interface DreamModule { } - /** Provides activity for dream service */ + /** Provides delegate to allow for testing of dream service */ @Binds - DreamActivityProvider bindActivityProvider(DreamActivityProviderImpl impl); + DreamServiceDelegate bindDreamDelegate(DreamServiceDelegateImpl impl); } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt index b35b7f5debb3..2cfb02eadd19 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt @@ -18,10 +18,17 @@ package com.android.systemui.dreams.homecontrols import android.app.Activity import android.service.dreams.DreamService -fun interface DreamActivityProvider { - /** - * Provides abstraction for getting the activity associated with a dream service, so that the - * activity can be mocked in tests. - */ +/** Provides abstraction for [DreamService] methods, so they can be mocked in tests. */ +interface DreamServiceDelegate { + /** Wrapper for [DreamService.getActivity] which can be mocked in tests. */ fun getActivity(dreamService: DreamService): Activity? + + /** Wrapper for [DreamService.wakeUp] which can be mocked in tests. */ + fun wakeUp(dreamService: DreamService) + + /** Wrapper for [DreamService.finish] which can be mocked in tests. */ + fun finish(dreamService: DreamService) + + /** Wrapper for [DreamService.getRedirectWake] which can be mocked in tests. */ + fun redirectWake(dreamService: DreamService): Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt index 0854e939645b..7dc5434c595e 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt @@ -19,8 +19,20 @@ import android.app.Activity import android.service.dreams.DreamService import javax.inject.Inject -class DreamActivityProviderImpl @Inject constructor() : DreamActivityProvider { +class DreamServiceDelegateImpl @Inject constructor() : DreamServiceDelegate { override fun getActivity(dreamService: DreamService): Activity { return dreamService.activity } + + override fun finish(dreamService: DreamService) { + dreamService.finish() + } + + override fun wakeUp(dreamService: DreamService) { + dreamService.wakeUp() + } + + override fun redirectWake(dreamService: DreamService): Boolean { + return dreamService.redirectWake + } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt index 76187c614b5d..77c54ec1eac3 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt @@ -31,6 +31,7 @@ import com.android.systemui.log.dagger.DreamLog import com.android.systemui.util.wakelock.WakeLock import com.android.systemui.util.wakelock.WakeLock.Builder.NO_TIMEOUT import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -46,7 +47,7 @@ constructor( private val taskFragmentFactory: TaskFragmentComponent.Factory, private val homeControlsComponentInteractor: HomeControlsComponentInteractor, private val wakeLockBuilder: WakeLock.Builder, - private val dreamActivityProvider: DreamActivityProvider, + private val dreamServiceDelegate: DreamServiceDelegate, @Background private val bgDispatcher: CoroutineDispatcher, @DreamLog logBuffer: LogBuffer ) : DreamService() { @@ -65,7 +66,7 @@ constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - val activity = dreamActivityProvider.getActivity(this) + val activity = dreamServiceDelegate.getActivity(this) if (activity == null) { finish() return @@ -79,9 +80,9 @@ constructor( taskFragmentFactory .create( activity = activity, - onCreateCallback = this::onTaskFragmentCreated, + onCreateCallback = { launchActivity() }, onInfoChangedCallback = this::onTaskFragmentInfoChanged, - hide = { endDream() } + hide = { endDream(false) } ) .apply { createTaskFragment() } @@ -91,16 +92,24 @@ constructor( private fun onTaskFragmentInfoChanged(taskFragmentInfo: TaskFragmentInfo) { if (taskFragmentInfo.isEmpty) { logger.d("Finishing dream due to TaskFragment being empty") - endDream() + endDream(true) } } - private fun endDream() { + private fun endDream(handleRedirect: Boolean) { homeControlsComponentInteractor.onDreamEndUnexpectedly() - finish() + if (handleRedirect && dreamServiceDelegate.redirectWake(this)) { + dreamServiceDelegate.wakeUp(this) + serviceScope.launch { + delay(ACTIVITY_RESTART_DELAY) + launchActivity() + } + } else { + dreamServiceDelegate.finish(this) + } } - private fun onTaskFragmentCreated(taskFragmentInfo: TaskFragmentInfo) { + private fun launchActivity() { val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value val componentName = homeControlsComponentInteractor.panelComponent.value logger.d("Starting embedding $componentName") @@ -134,6 +143,14 @@ constructor( * complete. */ val CANCELLATION_DELAY_AFTER_DETACHED = 5.seconds + + /** + * Defines the delay after wakeup where we should attempt to restart the embedded activity. + * When a wakeup is redirected, the dream service may keep running. In this case, we should + * restart the activity if it finished. This delays ensures the activity is only restarted + * after the wakeup transition has played. + */ + val ACTIVITY_RESTART_DELAY = 334.milliseconds const val TAG = "HomeControlsDreamService" } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index c08434015ab1..f4f8796ebffc 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -459,14 +459,6 @@ object Flags { @JvmField val ENABLE_CLOCK_KEYGUARD_PRESENTATION = releasedFlag("enable_clock_keyguard_presentation") - /** Enable the Compose implementation of the PeopleSpaceActivity. */ - @JvmField - val COMPOSE_PEOPLE_SPACE = releasedFlag("compose_people_space") - - /** Enable the Compose implementation of the Quick Settings footer actions. */ - @JvmField - val COMPOSE_QS_FOOTER_ACTIONS = releasedFlag("compose_qs_footer_actions") - /** Enable the share wifi button in Quick Settings internet dialog. */ @JvmField val SHARE_WIFI_QS_BUTTON = releasedFlag("share_wifi_qs_button") diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt new file mode 100644 index 000000000000..52ccc219353e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -0,0 +1,413 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.ui.composable + +import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.Accessibility +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Keyboard +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Tv +import androidx.compose.material.icons.filled.VerticalSplit +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationDrawerItemColors +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed +import com.android.compose.windowsizeclass.LocalWindowSizeClass +import com.android.systemui.res.R + +@Composable +fun ShortcutHelper(modifier: Modifier = Modifier, onKeyboardSettingsClicked: () -> Unit) { + if (shouldUseSinglePane()) { + ShortcutHelperSinglePane(modifier, categories, onKeyboardSettingsClicked) + } else { + ShortcutHelperTwoPane(modifier, categories, onKeyboardSettingsClicked) + } +} + +@Composable +private fun shouldUseSinglePane() = + LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact + +@Composable +private fun ShortcutHelperSinglePane( + modifier: Modifier = Modifier, + categories: List<ShortcutHelperCategory>, + onKeyboardSettingsClicked: () -> Unit, +) { + Column( + modifier = + modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(start = 16.dp, end = 16.dp, top = 26.dp) + ) { + TitleBar() + Spacer(modifier = Modifier.height(6.dp)) + ShortcutsSearchBar() + Spacer(modifier = Modifier.height(16.dp)) + CategoriesPanelSinglePane(categories) + Spacer(modifier = Modifier.weight(1f)) + KeyboardSettings(onClick = onKeyboardSettingsClicked) + } +} + +@Composable +private fun CategoriesPanelSinglePane( + categories: List<ShortcutHelperCategory>, +) { + var expandedCategory by remember { mutableStateOf<ShortcutHelperCategory?>(null) } + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + categories.fastForEachIndexed { index, category -> + val isExpanded = expandedCategory == category + val itemShape = + if (index == 0) { + ShortcutHelper.Shapes.singlePaneFirstCategory + } else if (index == categories.lastIndex) { + ShortcutHelper.Shapes.singlePaneLastCategory + } else { + ShortcutHelper.Shapes.singlePaneCategory + } + CategoryItemSinglePane( + category = category, + isExpanded = isExpanded, + onClick = { + expandedCategory = + if (isExpanded) { + null + } else { + category + } + }, + shape = itemShape, + ) + } + } +} + +@Composable +private fun CategoryItemSinglePane( + category: ShortcutHelperCategory, + isExpanded: Boolean, + onClick: () -> Unit, + shape: Shape, +) { + Surface( + color = MaterialTheme.colorScheme.surfaceBright, + shape = shape, + onClick = onClick, + ) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp) + ) { + Icon(category.icon, contentDescription = null) + Spacer(modifier = Modifier.width(16.dp)) + Text(stringResource(category.labelResId)) + Spacer(modifier = Modifier.weight(1f)) + RotatingExpandCollapseIcon(isExpanded) + } + AnimatedVisibility(visible = isExpanded) { ShortcutCategoryDetailsSinglePane(category) } + } + } +} + +@Composable +private fun RotatingExpandCollapseIcon(isExpanded: Boolean) { + val expandIconRotationDegrees by + animateFloatAsState( + targetValue = + if (isExpanded) { + 180f + } else { + 0f + }, + label = "Expand icon rotation animation" + ) + Icon( + modifier = + Modifier.background( + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = CircleShape + ) + .graphicsLayer { rotationZ = expandIconRotationDegrees }, + imageVector = Icons.Default.ExpandMore, + contentDescription = + if (isExpanded) { + stringResource(R.string.shortcut_helper_content_description_collapse_icon) + } else { + stringResource(R.string.shortcut_helper_content_description_expand_icon) + }, + tint = MaterialTheme.colorScheme.onSurface + ) +} + +@Composable +private fun ShortcutCategoryDetailsSinglePane(category: ShortcutHelperCategory) { + Box(modifier = Modifier.fillMaxWidth().heightIn(min = 300.dp)) { + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource(category.labelResId), + ) + } +} + +@Composable +private fun ShortcutHelperTwoPane( + modifier: Modifier = Modifier, + categories: List<ShortcutHelperCategory>, + onKeyboardSettingsClicked: () -> Unit, +) { + Column(modifier = modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 26.dp)) { + TitleBar() + Spacer(modifier = Modifier.height(12.dp)) + Row(Modifier.fillMaxWidth()) { + StartSidePanel( + modifier = Modifier.fillMaxWidth(fraction = 0.32f), + categories = categories, + onKeyboardSettingsClicked = onKeyboardSettingsClicked, + ) + Spacer(modifier = Modifier.width(24.dp)) + EndSidePanel(Modifier.fillMaxSize()) + } + } +} + +@Composable +private fun StartSidePanel( + modifier: Modifier, + categories: List<ShortcutHelperCategory>, + onKeyboardSettingsClicked: () -> Unit, +) { + Column(modifier) { + ShortcutsSearchBar() + Spacer(modifier = Modifier.heightIn(16.dp)) + CategoriesPanelTwoPane(categories) + Spacer(modifier = Modifier.weight(1f)) + KeyboardSettings(onKeyboardSettingsClicked) + } +} + +@Composable +private fun CategoriesPanelTwoPane(categories: List<ShortcutHelperCategory>) { + var selected by remember { mutableStateOf(categories.first()) } + Column { + categories.fastForEach { + CategoryItemTwoPane( + label = stringResource(it.labelResId), + icon = it.icon, + selected = selected == it, + onClick = { selected = it } + ) + } + } +} + +@Composable +private fun CategoryItemTwoPane( + label: String, + icon: ImageVector, + selected: Boolean, + onClick: () -> Unit, + colors: NavigationDrawerItemColors = + NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent), +) { + Surface( + selected = selected, + onClick = onClick, + modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 72.dp).fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + color = colors.containerColor(selected).value, + ) { + Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = icon, + contentDescription = null, + tint = colors.iconColor(selected).value + ) + Spacer(Modifier.width(12.dp)) + Box(Modifier.weight(1f)) { + Text( + fontSize = 18.sp, + color = colors.textColor(selected).value, + style = MaterialTheme.typography.headlineSmall, + text = label + ) + } + } + } +} + +@Composable +fun EndSidePanel(modifier: Modifier) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceBright + ) {} +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun TitleBar() { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent), + title = { + Text( + text = stringResource(R.string.shortcut_helper_title), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineSmall + ) + } + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun ShortcutsSearchBar() { + var query by remember { mutableStateOf("") } + SearchBar( + colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright), + query = query, + active = false, + onActiveChange = {}, + onQueryChange = { query = it }, + onSearch = {}, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) }, + content = {} + ) +} + +@Composable +private fun KeyboardSettings(onClick: () -> Unit) { + Surface( + onClick = onClick, + shape = RoundedCornerShape(24.dp), + color = Color.Transparent, + modifier = Modifier.semantics { role = Role.Button }.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Keyboard Settings", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 16.sp + ) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.AutoMirrored.Default.OpenInNew, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +/** Temporary data class just to populate the UI. */ +private data class ShortcutHelperCategory( + @StringRes val labelResId: Int, + val icon: ImageVector, +) + +// Temporarily populating the categories directly in the UI. +private val categories = + listOf( + ShortcutHelperCategory(R.string.shortcut_helper_category_system, Icons.Default.Tv), + ShortcutHelperCategory( + R.string.shortcut_helper_category_multitasking, + Icons.Default.VerticalSplit + ), + ShortcutHelperCategory(R.string.shortcut_helper_category_input, Icons.Default.Keyboard), + ShortcutHelperCategory(R.string.shortcut_helper_category_app_shortcuts, Icons.Default.Apps), + ShortcutHelperCategory(R.string.shortcut_helper_category_a11y, Icons.Default.Accessibility), + ) + +object ShortcutHelper { + + object Shapes { + val singlePaneFirstCategory = + RoundedCornerShape( + topStart = Dimensions.SinglePaneCategoryCornerRadius, + topEnd = Dimensions.SinglePaneCategoryCornerRadius + ) + val singlePaneLastCategory = + RoundedCornerShape( + bottomStart = Dimensions.SinglePaneCategoryCornerRadius, + bottomEnd = Dimensions.SinglePaneCategoryCornerRadius + ) + val singlePaneCategory = RectangleShape + } + + object Dimensions { + val SinglePaneCategoryCornerRadius = 28.dp + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt index ef4156da4f7b..1e8d23918964 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt @@ -23,9 +23,12 @@ import android.view.WindowInsets import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback +import androidx.compose.ui.platform.ComposeView import androidx.core.view.updatePadding import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import com.android.compose.theme.PlatformTheme +import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel import com.android.systemui.res.R import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -58,14 +61,30 @@ constructor( super.onCreate(savedInstanceState) setContentView(R.layout.activity_keyboard_shortcut_helper) setUpBottomSheetWidth() + expandBottomSheet() setUpInsets() setUpPredictiveBack() setUpSheetDismissListener() setUpDismissOnTouchOutside() + setUpComposeView() observeFinishRequired() viewModel.onViewOpened() } + private fun setUpComposeView() { + requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply { + setContent { + PlatformTheme { + ShortcutHelper( + onKeyboardSettingsClicked = ::onKeyboardSettingsClicked, + ) + } + } + } + } + + private fun onKeyboardSettingsClicked() {} + override fun onDestroy() { super.onDestroy() if (isFinishing) { @@ -101,7 +120,8 @@ constructor( bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets -> val safeDrawingInsets = insets.safeDrawing // Make sure the bottom sheet is not covered by the status bar. - bottomSheetContainer.updatePadding(top = safeDrawingInsets.top) + bottomSheetBehavior.maxHeight = + resources.displayMetrics.heightPixels - safeDrawingInsets.top // Make sure the contents inside of the bottom sheet are not hidden by system bars, or // cutouts. bottomSheet.updatePadding( @@ -171,7 +191,6 @@ constructor( private val WindowInsets.safeDrawing get() = getInsets(WindowInsets.Type.systemBars()) - .union(getInsets(WindowInsets.Type.ime())) .union(getInsets(WindowInsets.Type.displayCutout())) private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index a50cc8fbacb3..306f4ffa2a54 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -103,7 +103,6 @@ constructor( private val smartspaceViewModel: KeyguardSmartspaceViewModel, private val lockscreenContentViewModel: LockscreenContentViewModel, private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>, - private val keyguardBlueprintViewBinder: KeyguardBlueprintViewBinder, private val clockInteractor: KeyguardClockInteractor, private val keyguardViewMediator: KeyguardViewMediator, ) : CoreStartable { @@ -150,7 +149,7 @@ constructor( cs.connect(composeView.id, BOTTOM, PARENT_ID, BOTTOM) keyguardRootView.addView(composeView) } else { - keyguardBlueprintViewBinder.bind( + KeyguardBlueprintViewBinder.bind( keyguardRootView, keyguardBlueprintViewModel, keyguardClockViewModel, @@ -197,12 +196,14 @@ constructor( KeyguardRootViewBinder.bind( keyguardRootView, keyguardRootViewModel, + keyguardBlueprintViewModel, configuration, occludingAppDeviceEntryMessageViewModel, chipbarCoordinator, screenOffAnimationController, shadeInteractor, clockInteractor, + keyguardClockViewModel, interactionJankMonitor, deviceEntryHapticsInteractor, vibratorHelper, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt index 9b07675f672c..756c6c20e58d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt @@ -57,7 +57,7 @@ constructor( @Background private val scope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, private val communalInteractor: CommunalInteractor, powerInteractor: PowerInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, @@ -70,6 +70,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 01109af79c1d..2a9ee9fb8779 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -48,7 +48,7 @@ constructor( @Background private val scope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, val deviceEntryRepository: DeviceEntryRepository, @@ -60,6 +60,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 7d3de306d621..f5e98f1fedfe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -47,7 +47,7 @@ constructor( @Background private val scope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, @@ -60,6 +60,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt index 63294f7609a2..47aa02a0be52 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt @@ -45,7 +45,7 @@ constructor( @Background private val scope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, ) : @@ -56,6 +56,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 7961b45830d4..25c3b0d395c0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -51,7 +51,7 @@ constructor( @Background private val scope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, private val glanceableHubTransitions: GlanceableHubTransitions, powerInteractor: PowerInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, @@ -63,6 +63,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index ca6ab3ef52d8..e516fa3c44bb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -48,7 +48,7 @@ constructor( @Main mainDispatcher: CoroutineDispatcher, @Background bgDispatcher: CoroutineDispatcher, private val glanceableHubTransitions: GlanceableHubTransitions, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, override val transitionRepository: KeyguardTransitionRepository, transitionInteractor: KeyguardTransitionInteractor, powerInteractor: PowerInteractor, @@ -61,6 +61,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 8ca29c80c2e9..a540d761c38f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -49,7 +49,7 @@ constructor( @Background private val scope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, @@ -64,6 +64,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index f1e98f3bbe6d..8cab3cd35dcf 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -58,7 +58,7 @@ constructor( @Background private val scope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, private val flags: FeatureFlags, private val shadeRepository: ShadeRepository, powerInteractor: PowerInteractor, @@ -73,6 +73,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index 2603aab2781b..86d4cfb916ed 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -45,7 +45,7 @@ constructor( @Background private val scope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, powerInteractor: PowerInteractor, private val communalInteractor: CommunalInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, @@ -57,6 +57,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index 76a822369b0c..19b2b81c4b27 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -52,7 +52,7 @@ constructor( @Background private val scope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, - private val keyguardInteractor: KeyguardInteractor, + keyguardInteractor: KeyguardInteractor, private val communalInteractor: CommunalInteractor, private val flags: FeatureFlags, private val keyguardSecurityModel: KeyguardSecurityModel, @@ -67,6 +67,7 @@ constructor( bgDispatcher = bgDispatcher, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, + keyguardInteractor = keyguardInteractor, ) { override fun start() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index ccce3bf1397c..8ffa4bb8e4fb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -105,33 +105,35 @@ constructor( } return combine( - quickAffordanceAlwaysVisible(position), - keyguardInteractor.isDozing, - if (SceneContainerFlag.isEnabled) { - sceneInteractor - .get() - .transitionState - .map { - when (it) { - is ObservableTransitionState.Idle -> - it.currentScene == Scenes.Lockscreen - is ObservableTransitionState.Transition -> - it.fromScene == Scenes.Lockscreen || it.toScene == Scenes.Lockscreen + quickAffordanceAlwaysVisible(position), + keyguardInteractor.isDozing, + if (SceneContainerFlag.isEnabled) { + sceneInteractor + .get() + .transitionState + .map { + when (it) { + is ObservableTransitionState.Idle -> + it.currentScene == Scenes.Lockscreen + is ObservableTransitionState.Transition -> + it.fromScene == Scenes.Lockscreen || + it.toScene == Scenes.Lockscreen + } } - } - .distinctUntilChanged() - } else { - keyguardInteractor.isKeyguardShowing - }, - shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(), - biometricSettingsRepository.isCurrentUserInLockdown, - ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown -> - if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) { - affordance - } else { - KeyguardQuickAffordanceModel.Hidden + .distinctUntilChanged() + } else { + keyguardInteractor.isKeyguardShowing + }, + shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(), + biometricSettingsRepository.isCurrentUserInLockdown, + ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown -> + if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) { + affordance + } else { + KeyguardQuickAffordanceModel.Hidden + } } - } + .distinctUntilChanged() } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt index 323ceef06a97..e14820714c9b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -53,6 +53,7 @@ sealed class TransitionInteractor( val bgDispatcher: CoroutineDispatcher, val powerInteractor: PowerInteractor, val keyguardOcclusionInteractor: KeyguardOcclusionInteractor, + val keyguardInteractor: KeyguardInteractor, ) { val name = this::class.simpleName ?: "UnknownTransitionInteractor" abstract val transitionRepository: KeyguardTransitionRepository @@ -164,14 +165,10 @@ sealed class TransitionInteractor( @Deprecated("Will be merged into maybeStartTransitionToOccludedOrInsecureCamera") suspend fun maybeHandleInsecurePowerGesture(): Boolean { if (keyguardOcclusionInteractor.shouldTransitionFromPowerButtonGesture()) { - if (transitionInteractor.getCurrentState() == KeyguardState.GONE) { - // If the current state is GONE when the launch gesture is triggered, it means we - // were in transition from GONE -> DOZING/AOD due to the first power button tap. The - // second tap indicates that the user's intent was actually to launch the unlocked - // (insecure) camera, so we should transition back to GONE. + if (keyguardInteractor.isKeyguardDismissible.value) { startTransitionTo( KeyguardState.GONE, - ownerReason = "Power button gesture while GONE" + ownerReason = "Power button gesture while keyguard is dismissible" ) return true diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 816033561e94..bec8f3da9999 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -17,17 +17,12 @@ package com.android.systemui.keyguard.ui.binder -import android.os.Handler -import android.transition.Transition -import android.transition.TransitionManager import android.util.Log import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launch -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition @@ -40,47 +35,9 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.shared.R as sharedR import com.android.systemui.util.kotlin.pairwise -import javax.inject.Inject -import kotlin.math.max - -@SysUISingleton -class KeyguardBlueprintViewBinder -@Inject -constructor( - @Main private val handler: Handler, -) { - private var runningPriority = -1 - private val runningTransitions = mutableSetOf<Transition>() - private val isTransitionRunning: Boolean - get() = runningTransitions.size > 0 - private val transitionListener = - object : Transition.TransitionListener { - override fun onTransitionCancel(transition: Transition) { - if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}") - runningTransitions.remove(transition) - } - - override fun onTransitionEnd(transition: Transition) { - if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}") - runningTransitions.remove(transition) - } - - override fun onTransitionPause(transition: Transition) { - if (DEBUG) Log.i(TAG, "onTransitionPause: ${transition::class.simpleName}") - runningTransitions.remove(transition) - } - - override fun onTransitionResume(transition: Transition) { - if (DEBUG) Log.i(TAG, "onTransitionResume: ${transition::class.simpleName}") - runningTransitions.add(transition) - } - - override fun onTransitionStart(transition: Transition) { - if (DEBUG) Log.i(TAG, "onTransitionStart: ${transition::class.simpleName}") - runningTransitions.add(transition) - } - } +object KeyguardBlueprintViewBinder { + @JvmStatic fun bind( constraintLayout: ConstraintLayout, viewModel: KeyguardBlueprintViewModel, @@ -118,7 +75,7 @@ constructor( ) } - runTransition(constraintLayout, transition, config) { + viewModel.runTransition(constraintLayout, transition, config) { // Replace sections from the previous blueprint with the new ones blueprint.replaceViews( constraintLayout, @@ -146,7 +103,7 @@ constructor( viewModel.refreshTransition.collect { config -> val blueprint = viewModel.blueprint.value - runTransition( + viewModel.runTransition( constraintLayout, IntraBlueprintTransition(config, clockViewModel, smartspaceViewModel), config, @@ -167,50 +124,6 @@ constructor( } } - private fun runTransition( - constraintLayout: ConstraintLayout, - transition: Transition, - config: Config, - apply: () -> Unit, - ) { - val currentPriority = if (isTransitionRunning) runningPriority else -1 - if (config.checkPriority && config.type.priority < currentPriority) { - if (DEBUG) { - Log.w( - TAG, - "runTransition: skipping ${transition::class.simpleName}: " + - "currentPriority=$currentPriority; config=$config" - ) - } - apply() - return - } - - if (DEBUG) { - Log.i( - TAG, - "runTransition: running ${transition::class.simpleName}: " + - "currentPriority=$currentPriority; config=$config" - ) - } - - // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to - // the running set until the copy is started by the handler. - runningTransitions.add(transition) - transition.addListener(transitionListener) - runningPriority = max(currentPriority, config.type.priority) - - handler.post { - if (config.terminatePrevious) { - TransitionManager.endTransitions(constraintLayout) - } - - TransitionManager.beginDelayedTransition(constraintLayout, transition) - runningTransitions.remove(transition) - apply() - } - } - private fun logAlphaVisibilityOfAppliedConstraintSet( cs: ConstraintSet, viewModel: KeyguardClockViewModel @@ -237,8 +150,6 @@ constructor( ) } - companion object { - private const val TAG = "KeyguardBlueprintViewBinder" - private const val DEBUG = false - } + private const val TAG = "KeyguardBlueprintViewBinder" + private const val DEBUG = false } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index 23c2491813f7..807c322cc566 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -65,7 +65,7 @@ object KeyguardIndicationAreaBinder { val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) val disposableHandle = view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch("$TAG#viewModel.alpha") { // Do not independently apply alpha, as [KeyguardRootViewModel] should work // for this and all its children diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt index b9a79dccf76b..1cf009d13e9b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt @@ -30,6 +30,7 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launch import com.android.settingslib.Utils import com.android.systemui.animation.Expandable import com.android.systemui.animation.view.LaunchableImageView @@ -80,8 +81,8 @@ object KeyguardQuickAffordanceViewBinder { val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) val disposableHandle = view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch("$TAG#viewModel.collect") { viewModel.collect { buttonModel -> updateButton( view = button, @@ -93,7 +94,7 @@ object KeyguardQuickAffordanceViewBinder { } } - launch { + launch("$TAG#updateButtonAlpha") { updateButtonAlpha( view = button, viewModel = viewModel, @@ -101,7 +102,7 @@ object KeyguardQuickAffordanceViewBinder { ) } - launch { + launch("$TAG#configurationBasedDimensions") { configurationBasedDimensions.collect { dimensions -> button.updateLayoutParams<ViewGroup.LayoutParams> { width = dimensions.buttonSizePx.width @@ -323,4 +324,6 @@ object KeyguardQuickAffordanceViewBinder { private data class ConfigurationBasedDimensions( val buttonSizePx: Size, ) + + private const val TAG = "KeyguardQuickAffordanceViewBinder" } 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 39db22d5616d..fc92afe17eff 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 @@ -22,6 +22,7 @@ import android.annotation.DrawableRes import android.annotation.SuppressLint import android.graphics.Point import android.graphics.Rect +import android.util.Log import android.view.HapticFeedbackConstants import android.view.View import android.view.View.OnLayoutChangeListener @@ -56,8 +57,11 @@ import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel +import com.android.systemui.keyguard.ui.viewmodel.TransitionData import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager @@ -93,12 +97,14 @@ object KeyguardRootViewBinder { fun bind( view: ViewGroup, viewModel: KeyguardRootViewModel, + blueprintViewModel: KeyguardBlueprintViewModel, configuration: ConfigurationState, occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel?, chipbarCoordinator: ChipbarCoordinator?, screenOffAnimationController: ScreenOffAnimationController, shadeInteractor: ShadeInteractor, clockInteractor: KeyguardClockInteractor, + clockViewModel: KeyguardClockViewModel, interactionJankMonitor: InteractionJankMonitor?, deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, vibratorHelper: VibratorHelper?, @@ -348,7 +354,16 @@ object KeyguardRootViewBinder { } } - disposables += view.onLayoutChanged(OnLayoutChange(viewModel, childViews, burnInParams)) + disposables += + view.onLayoutChanged( + OnLayoutChange( + viewModel, + blueprintViewModel, + clockViewModel, + childViews, + burnInParams + ) + ) // Views will be added or removed after the call to bind(). This is needed to avoid many // calls to findViewById @@ -404,9 +419,13 @@ object KeyguardRootViewBinder { private class OnLayoutChange( private val viewModel: KeyguardRootViewModel, + private val blueprintViewModel: KeyguardBlueprintViewModel, + private val clockViewModel: KeyguardClockViewModel, private val childViews: Map<Int, View>, private val burnInParams: MutableStateFlow<BurnInParameters>, ) : OnLayoutChangeListener { + var prevTransition: TransitionData? = null + override fun onLayoutChange( view: View, left: Int, @@ -418,11 +437,21 @@ object KeyguardRootViewBinder { oldRight: Int, oldBottom: Int ) { + // After layout, ensure the notifications are positioned correctly childViews[nsslPlaceholderId]?.let { notificationListPlaceholder -> - // After layout, ensure the notifications are positioned correctly + // Do not update a second time while a blueprint transition is running + val transition = blueprintViewModel.currentTransition.value + val shouldAnimate = transition != null && transition.config.type.animateNotifChanges + if (prevTransition == transition && shouldAnimate) { + if (DEBUG) Log.w(TAG, "Skipping; layout during transition") + return + } + + prevTransition = transition viewModel.onNotificationContainerBoundsChanged( notificationListPlaceholder.top.toFloat(), notificationListPlaceholder.bottom.toFloat(), + animate = shouldAnimate ) } @@ -585,4 +614,6 @@ object KeyguardRootViewBinder { private const val ID = "occluding_app_device_entry_unlock_msg" private const val AOD_ICONS_APPEAR_DURATION: Long = 200 + private const val TAG = "KeyguardRootViewBinder" + private const val DEBUG = false } 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 fb1853f13ce9..777c873e47f0 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 @@ -68,7 +68,9 @@ import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection +import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewSmartspaceViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel @@ -134,6 +136,7 @@ constructor( private val vibratorHelper: VibratorHelper, private val indicationController: KeyguardIndicationController, private val keyguardRootViewModel: KeyguardRootViewModel, + private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel, @Assisted bundle: Bundle, private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, private val chipbarCoordinator: ChipbarCoordinator, @@ -143,6 +146,7 @@ constructor( private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel, private val defaultShortcutsSection: DefaultShortcutsSection, private val keyguardClockInteractor: KeyguardClockInteractor, + private val keyguardClockViewModel: KeyguardClockViewModel, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) private val width: Int = bundle.getInt(KEY_VIEW_WIDTH) @@ -379,12 +383,14 @@ constructor( KeyguardRootViewBinder.bind( keyguardRootView, keyguardRootViewModel, + keyguardBlueprintViewModel, configuration, occludingAppDeviceEntryMessageViewModel, chipbarCoordinator, screenOffAnimationController, shadeInteractor, keyguardClockInteractor, + keyguardClockViewModel, 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 diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt index 02e9ca5b6821..39f1ebe25299 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt @@ -31,16 +31,16 @@ class IntraBlueprintTransition( enum class Type( val priority: Int, + val animateNotifChanges: Boolean, ) { - ClockSize(100), - ClockCenter(99), - DefaultClockStepping(98), - AodNotifIconsTransition(97), - SmartspaceVisibility(2), - DefaultTransition(1), + ClockSize(100, true), + ClockCenter(99, false), + DefaultClockStepping(98, false), + SmartspaceVisibility(2, true), + DefaultTransition(1, false), // When transition between blueprint, we don't need any duration or interpolator but we need // all elements go to correct state - NoTransition(0), + NoTransition(0, false), } data class Config( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index 2832e9d8a35e..d77b54825664 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -19,6 +19,8 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM @@ -29,6 +31,7 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.res.R import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder @@ -38,6 +41,7 @@ import com.android.systemui.statusbar.notification.shared.NotificationIconContai import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.statusbar.phone.NotificationIconContainer import com.android.systemui.statusbar.ui.SystemBarUtilsState +import com.android.systemui.util.ui.value import javax.inject.Inject import kotlinx.coroutines.DisposableHandle @@ -51,6 +55,7 @@ constructor( private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore, private val notificationIconAreaController: NotificationIconAreaController, private val systemBarUtilsState: SystemBarUtilsState, + private val rootViewModel: KeyguardRootViewModel, ) : KeyguardSection() { private var nicBindingDisposable: DisposableHandle? = null @@ -101,20 +106,14 @@ constructor( if (!MigrateClocksToBlueprint.isEnabled) { return } + val bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin) - - val useSplitShade = context.resources.getBoolean(R.bool.config_use_split_notification_shade) - - val topAlignment = - if (useSplitShade) { - TOP - } else { - BOTTOM - } + val isVisible = rootViewModel.isNotifIconContainerVisible.value constraintSet.apply { connect(nicId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin) setGoneMargin(nicId, BOTTOM, bottomMargin) + setVisibility(nicId, if (isVisible.value) VISIBLE else GONE) connect( nicId, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt index c5fab8f57822..e01f0a152b37 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt @@ -147,8 +147,9 @@ constructor( deviceEntryIconViewModel.get().udfpsLocation.value?.let { udfpsLocation -> Log.d( "DeviceEntrySection", - "udfpsLocation=$udfpsLocation" + - " unusedAuthController=${authController.udfpsLocation}" + "udfpsLocation=$udfpsLocation, " + + "scaledLocation=(${udfpsLocation.centerX},${udfpsLocation.centerY}), " + + "unusedAuthController=${authController.udfpsLocation}" ) centerIcon( Point(udfpsLocation.centerX.toInt(), udfpsLocation.centerY.toInt()), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 7c745bc227f0..f17dbd24cf25 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -369,6 +369,21 @@ class ClockSizeTransition( addTarget(R.id.status_view_media_container) } + override fun mutateBounds( + view: View, + fromIsVis: Boolean, + toIsVis: Boolean, + fromBounds: Rect, + toBounds: Rect, + fromSSBounds: Rect?, + toSSBounds: Rect? + ) { + // If view is changing visibility, hold it in place + if (fromIsVis == toIsVis) return + if (DEBUG) Log.i(TAG, "Holding position of ${view.id}") + toBounds.set(fromBounds) + } + companion object { const val STATUS_AREA_MOVE_UP_MILLIS = 967L const val STATUS_AREA_MOVE_DOWN_MILLIS = 467L diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt index b1f189836903..7ac03bffd4f3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt @@ -17,15 +17,119 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.os.Handler +import android.transition.Transition +import android.transition.TransitionManager +import android.util.Log +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +data class TransitionData( + val config: Config, + val start: Long = System.currentTimeMillis(), +) class KeyguardBlueprintViewModel @Inject constructor( + @Main private val handler: Handler, keyguardBlueprintInteractor: KeyguardBlueprintInteractor, ) { val blueprint = keyguardBlueprintInteractor.blueprint val blueprintId = keyguardBlueprintInteractor.blueprintId val refreshTransition = keyguardBlueprintInteractor.refreshTransition + + private val _currentTransition = MutableStateFlow<TransitionData?>(null) + val currentTransition = _currentTransition.asStateFlow() + + private val runningTransitions = mutableSetOf<Transition>() + private val transitionListener = + object : Transition.TransitionListener { + override fun onTransitionCancel(transition: Transition) { + if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}") + updateTransitions(null) { remove(transition) } + } + + override fun onTransitionEnd(transition: Transition) { + if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}") + updateTransitions(null) { remove(transition) } + } + + override fun onTransitionPause(transition: Transition) { + if (DEBUG) Log.i(TAG, "onTransitionPause: ${transition::class.simpleName}") + updateTransitions(null) { remove(transition) } + } + + override fun onTransitionResume(transition: Transition) { + if (DEBUG) Log.i(TAG, "onTransitionResume: ${transition::class.simpleName}") + updateTransitions(null) { add(transition) } + } + + override fun onTransitionStart(transition: Transition) { + if (DEBUG) Log.i(TAG, "onTransitionStart: ${transition::class.simpleName}") + updateTransitions(null) { add(transition) } + } + } + + fun updateTransitions(data: TransitionData?, mutate: MutableSet<Transition>.() -> Unit) { + runningTransitions.mutate() + + if (runningTransitions.size <= 0) _currentTransition.value = null + else if (data != null) _currentTransition.value = data + } + + fun runTransition( + constraintLayout: ConstraintLayout, + transition: Transition, + config: Config, + apply: () -> Unit, + ) { + val currentPriority = currentTransition.value?.let { it.config.type.priority } ?: -1 + if (config.checkPriority && config.type.priority < currentPriority) { + if (DEBUG) { + Log.w( + TAG, + "runTransition: skipping ${transition::class.simpleName}: " + + "currentPriority=$currentPriority; config=$config" + ) + } + apply() + return + } + + if (DEBUG) { + Log.i( + TAG, + "runTransition: running ${transition::class.simpleName}: " + + "currentPriority=$currentPriority; config=$config" + ) + } + + // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to + // the running set until the copy is started by the handler. + updateTransitions(TransitionData(config)) { add(transition) } + transition.addListener(transitionListener) + + handler.post { + if (config.terminatePrevious) { + TransitionManager.endTransitions(constraintLayout) + } + + TransitionManager.beginDelayedTransition(constraintLayout, transition) + apply() + + // Delay removal until after copied transition has started + handler.post { updateTransitions(null) { remove(transition) } } + } + } + + companion object { + private const val TAG = "KeyguardBlueprintViewModel" + private const val DEBUG = true + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt index 8409f15dca81..448a71c36a99 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt @@ -23,9 +23,12 @@ import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.BurnInModel +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.res.R import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -42,6 +45,7 @@ constructor( private val burnInInteractor: BurnInInteractor, private val shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel, configurationInteractor: ConfigurationInteractor, + keyguardTransitionInteractor: KeyguardTransitionInteractor, ) { /** Notifies when a new configuration is set */ @@ -69,12 +73,22 @@ constructor( .distinctUntilChanged() } + @OptIn(ExperimentalCoroutinesApi::class) private val burnIn: Flow<BurnInModel> = - burnInInteractor - .burnIn( - xDimenResourceId = R.dimen.burn_in_prevention_offset_x, - yDimenResourceId = R.dimen.default_burn_in_prevention_offset, - ) + combine( + burnInInteractor.burnIn( + xDimenResourceId = R.dimen.burn_in_prevention_offset_x, + yDimenResourceId = R.dimen.default_burn_in_prevention_offset, + ), + keyguardTransitionInteractor.transitionValue(KeyguardState.AOD), + ) { burnIn, aodTransitionValue -> + BurnInModel( + (burnIn.translationX * aodTransitionValue).toInt(), + (burnIn.translationY * aodTransitionValue).toInt(), + burnIn.scale, + burnIn.scaleClockOnly, + ) + } .distinctUntilChanged() /** An observable for the x-offset by which the indication area should be translated. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt index c4383fc0857d..244d842b7073 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -28,19 +29,23 @@ import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAfforda import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalCoroutinesApi::class) class KeyguardQuickAffordancesCombinedViewModel @Inject constructor( + @Application applicationScope: CoroutineScope, private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, private val keyguardInteractor: KeyguardInteractor, shadeInteractor: ShadeInteractor, @@ -84,15 +89,20 @@ constructor( /** The only time the expansion is important is while lockscreen is actively displayed */ private val shadeExpansionAlpha = combine( - showingLockscreen, - shadeInteractor.anyExpansion, - ) { showingLockscreen, expansion -> - if (showingLockscreen) { - 1 - expansion - } else { - 0f + showingLockscreen, + shadeInteractor.anyExpansion, + ) { showingLockscreen, expansion -> + if (showingLockscreen) { + 1 - expansion + } else { + 0f + } } - } + .stateIn( + scope = applicationScope, + started = SharingStarted.Lazily, + initialValue = 0f, + ) /** * ID of the slot that's currently selected in the preview that renders exclusively in the 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 aaec69f4f022..1ec2a4969f0d 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 @@ -58,6 +58,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combineTransform @@ -67,13 +69,14 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardRootViewModel @Inject constructor( - @Application private val scope: CoroutineScope, + @Application private val applicationScope: CoroutineScope, private val deviceEntryInteractor: DeviceEntryInteractor, private val dozeParameters: DozeParameters, private val keyguardInteractor: KeyguardInteractor, @@ -280,7 +283,7 @@ constructor( burnInJob?.cancel() burnInJob = - scope.launch("$TAG#aodBurnInViewModel") { + applicationScope.launch("$TAG#aodBurnInViewModel") { aodBurnInViewModel.movement(params).collect { _burnInModel.value = it } } } @@ -294,7 +297,7 @@ constructor( } /** Is the notification icon container visible? */ - val isNotifIconContainerVisible: Flow<AnimatedValue<Boolean>> = + val isNotifIconContainerVisible: StateFlow<AnimatedValue<Boolean>> = combine( goneToAodTransitionRunning, keyguardTransitionInteractor.finishedKeyguardState.map { @@ -336,11 +339,15 @@ constructor( } } } - .distinctUntilChanged() + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = AnimatedValue.NotAnimating(false), + ) - fun onNotificationContainerBoundsChanged(top: Float, bottom: Float) { + fun onNotificationContainerBoundsChanged(top: Float, bottom: Float, animate: Boolean = false) { keyguardInteractor.setNotificationContainerBounds( - NotificationContainerBounds(top = top, bottom = bottom) + NotificationContainerBounds(top = top, bottom = bottom, isAnimated = animate) ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt index e0c54190283a..9c29bab80d14 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt @@ -43,6 +43,7 @@ interface MediaDomainModule { @IntoMap @ClassKey(MediaDataProcessor::class) fun bindMediaDataProcessor(interactor: MediaDataProcessor): CoreStartable + companion object { @Provides @@ -52,7 +53,7 @@ interface MediaDomainModule { newProvider: Provider<MediaCarouselInteractor>, mediaFlags: MediaFlags, ): MediaDataManager { - return if (mediaFlags.isMediaControlsRefactorEnabled()) { + return if (mediaFlags.isSceneContainerEnabled()) { newProvider.get() } else { legacyProvider.get() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index eed775242d1f..8e985e11732f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -269,7 +269,7 @@ class MediaDataProcessor( } override fun start() { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { return } @@ -746,8 +746,7 @@ class MediaDataProcessor( notif.extras.getParcelable( Notification.EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo::class.java - ) - ?: getAppInfoFromPackage(sbn.packageName) + ) ?: getAppInfoFromPackage(sbn.packageName) // App name val appName = getAppName(sbn, appInfo) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt index 9e6230012760..b4bd4fd2c266 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt @@ -36,8 +36,8 @@ import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilt import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener import com.android.systemui.media.controls.domain.resume.MediaResumeListener import com.android.systemui.media.controls.shared.model.MediaCommonModel -import com.android.systemui.media.controls.util.MediaControlsRefactorFlag import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.scene.shared.flag.SceneContainerFlag import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -127,7 +127,7 @@ constructor( val currentMedia: StateFlow<List<MediaCommonModel>> = mediaFilterRepository.currentMedia override fun start() { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { return } @@ -256,8 +256,6 @@ constructor( companion object { val unsupported: Nothing get() = - error( - "Code path not supported when ${MediaControlsRefactorFlag.FLAG_NAME} is enabled" - ) + error("Code path not supported when ${SceneContainerFlag.DESCRIPTION} is enabled") } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 19e3e0715989..8316b3aba73e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -217,7 +217,7 @@ constructor( private val animationScaleObserver: ContentObserver = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() } } else { controllerByViewModel.values.forEach { it.updateAnimatorDurationScale() } @@ -347,7 +347,7 @@ constructor( inflateSettingsButton() mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) configurationController.addCallback(configListener) - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { setUpListeners() } else { val visualStabilityCallback = OnReorderingAllowedListener { @@ -389,7 +389,7 @@ constructor( listenForAnyStateToLockscreenTransition(this) listenForLockscreenSettingChanges(this) - if (!mediaFlags.isMediaControlsRefactorEnabled()) return@repeatOnLifecycle + if (!mediaFlags.isSceneContainerEnabled()) return@repeatOnLifecycle listenForMediaItemsChanges(this) } } @@ -882,8 +882,7 @@ constructor( val previousVisibleIndex = MediaPlayerData.playerKeys().indexOfFirst { key -> it == key } mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex) - } - ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex) + } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex) } } else if (isRtl && mediaContent.childCount > 0) { // In RTL, Scroll to the first player as it is the rightmost player in media carousel. @@ -1092,7 +1091,7 @@ constructor( } private fun updatePlayers(recreateMedia: Boolean) { - if (mediaFlags.isMediaControlsRefactorEnabled()) { + if (mediaFlags.isSceneContainerEnabled()) { updateMediaPlayers(recreateMedia) return } @@ -1192,7 +1191,7 @@ constructor( currentStartLocation = startLocation currentEndLocation = endLocation currentTransitionProgress = progress - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { for (mediaPlayer in MediaPlayerData.players()) { updateViewControllerToState(mediaPlayer.mediaViewController, immediately) } @@ -1254,7 +1253,7 @@ constructor( /** Update listening to seekbar. */ private fun updateSeekbarListening(visibleToUser: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { for (player in MediaPlayerData.players()) { player.setListening(visibleToUser && currentlyExpanded) } @@ -1269,7 +1268,7 @@ constructor( private fun updateCarouselDimensions() { var width = 0 var height = 0 - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { for (mediaPlayer in MediaPlayerData.players()) { val controller = mediaPlayer.mediaViewController // When transitioning the view to gone, the view gets smaller, but the translation @@ -1361,7 +1360,7 @@ constructor( !mediaManager.hasActiveMediaOrRecommendation() && desiredHostState.showsOnlyActiveMedia - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { for (mediaPlayer in MediaPlayerData.players()) { if (animate) { mediaPlayer.mediaViewController.animatePendingStateChange( @@ -1401,7 +1400,7 @@ constructor( } fun closeGuts(immediate: Boolean = true) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) { + if (!mediaFlags.isSceneContainerEnabled()) { MediaPlayerData.players().forEach { it.closeGuts(immediate) } } else { controllerByViewModel.values.forEach { it.closeGuts(immediate) } @@ -1544,7 +1543,7 @@ constructor( @VisibleForTesting fun onSwipeToDismiss() { - if (mediaFlags.isMediaControlsRefactorEnabled()) { + if (mediaFlags.isSceneContainerEnabled()) { mediaCarouselViewModel.onSwipeToDismiss() return } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt index a4f3e2174791..6589038d7096 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt @@ -42,6 +42,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.util.MediaFlags @@ -61,6 +62,11 @@ import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch private val TAG: String = MediaHierarchyManager::class.java.simpleName @@ -89,6 +95,7 @@ val View.isShownNotFaded: Boolean * This manager is responsible for placement of the unique media view between the different hosts * and animate the positions of the views to achieve seamless transitions. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class MediaHierarchyManager @Inject @@ -101,6 +108,7 @@ constructor( private val mediaManager: MediaDataManager, private val keyguardViewController: KeyguardViewController, private val dreamOverlayStateController: DreamOverlayStateController, + private val keyguardInteractor: KeyguardInteractor, communalTransitionViewModel: CommunalTransitionViewModel, configurationController: ConfigurationController, wakefulnessLifecycle: WakefulnessLifecycle, @@ -236,6 +244,15 @@ constructor( private var inSplitShade = false + /** + * Whether we are transitioning to the hub or from the hub to the shade. If so, use fade as the + * transformation type and skip calculating state with the bounds and the transition progress. + */ + private val isHubTransition + get() = + desiredLocation == LOCATION_COMMUNAL_HUB || + (previousLocation == LOCATION_COMMUNAL_HUB && desiredLocation == LOCATION_QS) + /** Is there any active media or recommendation in the carousel? */ private var hasActiveMediaOrRecommendation: Boolean = false get() = mediaManager.hasActiveMediaOrRecommendation() @@ -413,6 +430,12 @@ constructor( /** Is the communal UI showing */ private var isCommunalShowing: Boolean = false + /** Is the communal UI showing and not dreaming */ + private var onCommunalNotDreaming: Boolean = false + + /** Is the communal UI showing, dreaming and shade expanding */ + private var onCommunalDreamingAndShadeExpanding: Boolean = false + /** * The current cross fade progress. 0.5f means it's just switching between the start and the end * location and the content is fully faded, while 0.75f means that we're halfway faded in again @@ -585,11 +608,26 @@ constructor( // Listen to the communal UI state. Make sure that communal UI is showing and hub itself is // available, ie. not disabled and able to be shown. + // When dreaming, qs expansion is immediately set to 1f, so we listen to shade expansion to + // calculate the new location. coroutineScope.launch { - communalTransitionViewModel.isUmoOnCommunal.collect { value -> - isCommunalShowing = value - updateDesiredLocation(forceNoAnimation = true) - } + combine( + communalTransitionViewModel.isUmoOnCommunal, + keyguardInteractor.isDreaming, + // keep on communal before the shade is expanded enough to show the elements in + // QS + shadeInteractor.shadeExpansion + .mapLatest { it < EXPANSION_THRESHOLD } + .distinctUntilChanged(), + ::Triple + ) + .collectLatest { (communalShowing, isDreaming, isShadeExpanding) -> + isCommunalShowing = communalShowing + onCommunalDreamingAndShadeExpanding = + communalShowing && isDreaming && isShadeExpanding + onCommunalNotDreaming = communalShowing && !isDreaming + updateDesiredLocation(forceNoAnimation = true) + } } } @@ -805,6 +843,9 @@ constructor( if (skipQqsOnExpansion) { return false } + if (isHubTransition) { + return false + } // This is an invalid transition, and can happen when using the camera gesture from the // lock screen. Disallow. if ( @@ -947,6 +988,9 @@ constructor( @VisibleForTesting @TransformationType fun calculateTransformationType(): Int { + if (isHubTransition) { + return TRANSFORMATION_TYPE_FADE + } if (isTransitioningToFullShade) { if (inSplitShade && areGuidedTransitionHostsVisible()) { return TRANSFORMATION_TYPE_TRANSITION @@ -977,7 +1021,7 @@ constructor( * otherwise */ private fun getTransformationProgress(): Float { - if (skipQqsOnExpansion) { + if (skipQqsOnExpansion || isHubTransition) { return -1.0f } val progress = getQSTransformationProgress() @@ -1147,15 +1191,18 @@ constructor( } val onLockscreen = (!bypassController.bypassEnabled && (statusbarState == StatusBarState.KEYGUARD)) + + // UMO should show on hub unless the qs is expanding when not dreaming, or shade is + // expanding when dreaming + val onCommunal = + (onCommunalNotDreaming && qsExpansion == 0.0f) || onCommunalDreamingAndShadeExpanding val location = when { mediaFlags.isSceneContainerEnabled() -> desiredLocation dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY - - // UMO should show in communal unless the shade is expanding or visible. - isCommunalShowing && qsExpansion == 0.0f -> LOCATION_COMMUNAL_HUB + onCommunal -> LOCATION_COMMUNAL_HUB (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS - qsExpansion > 0.4f && onLockscreen -> LOCATION_QS + qsExpansion > EXPANSION_THRESHOLD && onLockscreen -> LOCATION_QS onLockscreen && isSplitShadeExpanding() -> LOCATION_QS onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS @@ -1190,6 +1237,9 @@ constructor( // reattach it without an animation return LOCATION_LOCKSCREEN } + // When communal showing while dreaming, skipQqsOnExpansion is also true but we want to + // return the calculated location, so it won't disappear as soon as shade is pulled down. + if (isCommunalShowing) return location if (skipQqsOnExpansion) { // When doing an immediate expand or collapse, we want to keep it in QS. return LOCATION_QS @@ -1288,6 +1338,9 @@ constructor( * transitioning */ const val TRANSFORMATION_TYPE_FADE = 1 + + /** Expansion amount value at which elements start to become visible in the QS panel. */ + const val EXPANSION_THRESHOLD = 0.4f } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index 38377088a2d7..9d0723211d4b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -203,7 +203,7 @@ constructor( private val scrubbingChangeListener = object : SeekBarViewModel.ScrubbingChangeListener { override fun onScrubbingChanged(scrubbing: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return if (isScrubbing == scrubbing) return isScrubbing = scrubbing updateDisplayForScrubbingChange() @@ -213,7 +213,7 @@ constructor( private val enabledChangeListener = object : SeekBarViewModel.EnabledChangeListener { override fun onEnabledChanged(enabled: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return if (isSeekBarEnabled == enabled) return isSeekBarEnabled = enabled MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled) @@ -229,7 +229,7 @@ constructor( * @param listening True when player should be active. Otherwise, false. */ fun setListening(listening: Boolean) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return seekBarViewModel.listening = listening } @@ -263,7 +263,7 @@ constructor( ) ) } - if (mediaFlags.isMediaControlsRefactorEnabled()) { + if (mediaFlags.isSceneContainerEnabled()) { if ( this@MediaViewController::recsConfigurationChangeListener.isInitialized ) { @@ -305,6 +305,7 @@ constructor( */ var collapsedLayout = ConstraintSet() @VisibleForTesting set + /** * The expanded constraint set used to render a collapsed player. If it is modified, make sure * to call [refreshState] @@ -334,7 +335,7 @@ constructor( * Notify this controller that the view has been removed and all listeners should be destroyed */ fun onDestroy() { - if (mediaFlags.isMediaControlsRefactorEnabled()) { + if (mediaFlags.isSceneContainerEnabled()) { if (this::seekBarObserver.isInitialized) { seekBarViewModel.progress.removeObserver(seekBarObserver) } @@ -657,7 +658,7 @@ constructor( } fun attachPlayer(mediaViewHolder: MediaViewHolder) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return this.mediaViewHolder = mediaViewHolder // Setting up seek bar. @@ -731,7 +732,7 @@ constructor( } fun updateAnimatorDurationScale() { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return if (this::seekBarObserver.isInitialized) { seekBarObserver.animationEnabled = globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f @@ -787,7 +788,7 @@ constructor( } fun attachRecommendations(recommendationViewHolder: RecommendationViewHolder) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return this.recommendationViewHolder = recommendationViewHolder attach(recommendationViewHolder.recommendations, TYPE.RECOMMENDATION) @@ -796,13 +797,13 @@ constructor( } fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return seekBarViewModel.logSeek = onSeek onBindSeekBar.invoke(seekBarViewModel) } fun setUpTurbulenceNoise() { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return if (!this::turbulenceNoiseAnimationConfig.isInitialized) { turbulenceNoiseAnimationConfig = createTurbulenceNoiseConfig( @@ -1153,13 +1154,13 @@ constructor( } fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return isPrevButtonAvailable = isAvailable prevNotVisibleValue = notVisibleValue } fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { - if (!mediaFlags.isMediaControlsRefactorEnabled()) return + if (!mediaFlags.isSceneContainerEnabled()) return isNextButtonAvailable = isAvailable nextNotVisibleValue = notVisibleValue } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt deleted file mode 100644 index 2850b4bb2358..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.util - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the media_controls_refactor flag state. */ -@Suppress("NOTHING_TO_INLINE") -object MediaControlsRefactorFlag { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_MEDIA_CONTROLS_REFACTOR - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the flag enabled? */ - @JvmStatic - inline val isEnabled - get() = Flags.mediaControlsRefactor() - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an eng - * build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 1e7bc0cacf1d..21c311191710 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -52,8 +52,4 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlagsClass /** Check whether to use scene framework */ fun isSceneContainerEnabled() = SceneContainerFlag.isEnabled - - /** Check whether to use media refactor code */ - fun isMediaControlsRefactorEnabled() = - MediaControlsRefactorFlag.isEnabled && SceneContainerFlag.isEnabled } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt index a256b59ac076..e931f8f5398a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt @@ -18,10 +18,7 @@ import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOn private const val TAG = "BackPanel" private const val DEBUG = false -class BackPanel( - context: Context, - private val latencyTracker: LatencyTracker -) : View(context) { +class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) { var arrowsPointLeft = false set(value) { @@ -42,39 +39,39 @@ class BackPanel( // True if the panel is currently on the left of the screen var isLeftPanel = false - /** - * Used to track back arrow latency from [android.view.MotionEvent.ACTION_DOWN] to [onDraw] - */ + /** Used to track back arrow latency from [android.view.MotionEvent.ACTION_DOWN] to [onDraw] */ private var trackingBackArrowLatency = false - /** - * The length of the arrow measured horizontally. Used for animating [arrowPath] - */ - private var arrowLength = AnimatedFloat( + /** The length of the arrow measured horizontally. Used for animating [arrowPath] */ + private var arrowLength = + AnimatedFloat( name = "arrowLength", minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS - ) + ) /** * The height of the arrow measured vertically from its center to its top (i.e. half the total * height). Used for animating [arrowPath] */ - var arrowHeight = AnimatedFloat( + var arrowHeight = + AnimatedFloat( name = "arrowHeight", minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES - ) + ) - val backgroundWidth = AnimatedFloat( + val backgroundWidth = + AnimatedFloat( name = "backgroundWidth", minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS, minimumValue = 0f, - ) + ) - val backgroundHeight = AnimatedFloat( + val backgroundHeight = + AnimatedFloat( name = "backgroundHeight", minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS, minimumValue = 0f, - ) + ) /** * Corners of the background closer to the edge of the screen (where the arrow appeared from). @@ -88,17 +85,19 @@ class BackPanel( */ val backgroundFarCornerRadius = AnimatedFloat("backgroundFarCornerRadius") - var scale = AnimatedFloat( + var scale = + AnimatedFloat( name = "scale", minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_SCALE, minimumValue = 0f - ) + ) - val scalePivotX = AnimatedFloat( + val scalePivotX = + AnimatedFloat( name = "scalePivotX", minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS, minimumValue = backgroundWidth.pos / 2, - ) + ) /** * Left/right position of the background relative to the canvas. Also corresponds with the @@ -107,21 +106,24 @@ class BackPanel( */ var horizontalTranslation = AnimatedFloat(name = "horizontalTranslation") - var arrowAlpha = AnimatedFloat( + var arrowAlpha = + AnimatedFloat( name = "arrowAlpha", minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA, minimumValue = 0f, maximumValue = 1f - ) + ) - val backgroundAlpha = AnimatedFloat( + val backgroundAlpha = + AnimatedFloat( name = "backgroundAlpha", minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA, minimumValue = 0f, maximumValue = 1f - ) + ) - private val allAnimatedFloat = setOf( + private val allAnimatedFloat = + setOf( arrowLength, arrowHeight, backgroundWidth, @@ -132,7 +134,7 @@ class BackPanel( horizontalTranslation, arrowAlpha, backgroundAlpha - ) + ) /** * Canvas vertical translation. How far up/down the arrow and background appear relative to the @@ -140,43 +142,45 @@ class BackPanel( */ var verticalTranslation = AnimatedFloat("verticalTranslation") - /** - * Use for drawing debug info. Can only be set if [DEBUG]=true - */ + /** Use for drawing debug info. Can only be set if [DEBUG]=true */ var drawDebugInfo: ((canvas: Canvas) -> Unit)? = null set(value) { if (DEBUG) field = value } internal fun updateArrowPaint(arrowThickness: Float) { - arrowPaint.strokeWidth = arrowThickness - val isDeviceInNightTheme = resources.configuration.uiMode and - Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + val isDeviceInNightTheme = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES - arrowPaint.color = Utils.getColorAttrDefaultColor(context, + arrowPaint.color = + Utils.getColorAttrDefaultColor( + context, if (isDeviceInNightTheme) { com.android.internal.R.attr.materialColorOnSecondaryContainer } else { com.android.internal.R.attr.materialColorOnSecondaryFixed } - ) + ) - arrowBackgroundPaint.color = Utils.getColorAttrDefaultColor(context, + arrowBackgroundPaint.color = + Utils.getColorAttrDefaultColor( + context, if (isDeviceInNightTheme) { com.android.internal.R.attr.materialColorSecondaryContainer } else { com.android.internal.R.attr.materialColorSecondaryFixedDim } - ) + ) } inner class AnimatedFloat( - name: String, - private val minimumVisibleChange: Float? = null, - private val minimumValue: Float? = null, - private val maximumValue: Float? = null, + name: String, + private val minimumVisibleChange: Float? = null, + private val minimumValue: Float? = null, + private val maximumValue: Float? = null, ) { // The resting position when not stretched by a touch drag @@ -207,19 +211,21 @@ class BackPanel( } init { - val floatProp = object : FloatPropertyCompat<AnimatedFloat>(name) { - override fun setValue(animatedFloat: AnimatedFloat, value: Float) { - animatedFloat.pos = value - } + val floatProp = + object : FloatPropertyCompat<AnimatedFloat>(name) { + override fun setValue(animatedFloat: AnimatedFloat, value: Float) { + animatedFloat.pos = value + } - override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos - } - animation = SpringAnimation(this, floatProp).apply { - spring = SpringForce() - this@AnimatedFloat.minimumValue?.let { setMinValue(it) } - this@AnimatedFloat.maximumValue?.let { setMaxValue(it) } - this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it } - } + override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos + } + animation = + SpringAnimation(this, floatProp).apply { + spring = SpringForce() + this@AnimatedFloat.minimumValue?.let { setMinValue(it) } + this@AnimatedFloat.maximumValue?.let { setMaxValue(it) } + this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it } + } } fun snapTo(newPosition: Float) { @@ -233,11 +239,10 @@ class BackPanel( snapTo(restingPosition) } - fun stretchTo( - stretchAmount: Float, - startingVelocity: Float? = null, - springForce: SpringForce? = null + stretchAmount: Float, + startingVelocity: Float? = null, + springForce: SpringForce? = null ) { animation.apply { startingVelocity?.let { @@ -297,8 +302,8 @@ class BackPanel( } fun addAnimationEndListener( - animatedFloat: AnimatedFloat, - endListener: DelayedOnAnimationEndListener + animatedFloat: AnimatedFloat, + endListener: DelayedOnAnimationEndListener ): Boolean { return if (animatedFloat.isRunning) { animatedFloat.addEndListener(endListener) @@ -314,51 +319,51 @@ class BackPanel( } fun setStretch( - horizontalTranslationStretchAmount: Float, - arrowStretchAmount: Float, - arrowAlphaStretchAmount: Float, - backgroundAlphaStretchAmount: Float, - backgroundWidthStretchAmount: Float, - backgroundHeightStretchAmount: Float, - edgeCornerStretchAmount: Float, - farCornerStretchAmount: Float, - fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens + horizontalTranslationStretchAmount: Float, + arrowStretchAmount: Float, + arrowAlphaStretchAmount: Float, + backgroundAlphaStretchAmount: Float, + backgroundWidthStretchAmount: Float, + backgroundHeightStretchAmount: Float, + edgeCornerStretchAmount: Float, + farCornerStretchAmount: Float, + fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens ) { horizontalTranslation.stretchBy( - finalPosition = fullyStretchedDimens.horizontalTranslation, - amount = horizontalTranslationStretchAmount + finalPosition = fullyStretchedDimens.horizontalTranslation, + amount = horizontalTranslationStretchAmount ) arrowLength.stretchBy( - finalPosition = fullyStretchedDimens.arrowDimens.length, - amount = arrowStretchAmount + finalPosition = fullyStretchedDimens.arrowDimens.length, + amount = arrowStretchAmount ) arrowHeight.stretchBy( - finalPosition = fullyStretchedDimens.arrowDimens.height, - amount = arrowStretchAmount + finalPosition = fullyStretchedDimens.arrowDimens.height, + amount = arrowStretchAmount ) arrowAlpha.stretchBy( - finalPosition = fullyStretchedDimens.arrowDimens.alpha, - amount = arrowAlphaStretchAmount + finalPosition = fullyStretchedDimens.arrowDimens.alpha, + amount = arrowAlphaStretchAmount ) backgroundAlpha.stretchBy( - finalPosition = fullyStretchedDimens.backgroundDimens.alpha, - amount = backgroundAlphaStretchAmount + finalPosition = fullyStretchedDimens.backgroundDimens.alpha, + amount = backgroundAlphaStretchAmount ) backgroundWidth.stretchBy( - finalPosition = fullyStretchedDimens.backgroundDimens.width, - amount = backgroundWidthStretchAmount + finalPosition = fullyStretchedDimens.backgroundDimens.width, + amount = backgroundWidthStretchAmount ) backgroundHeight.stretchBy( - finalPosition = fullyStretchedDimens.backgroundDimens.height, - amount = backgroundHeightStretchAmount + finalPosition = fullyStretchedDimens.backgroundDimens.height, + amount = backgroundHeightStretchAmount ) backgroundEdgeCornerRadius.stretchBy( - finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius, - amount = edgeCornerStretchAmount + finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius, + amount = edgeCornerStretchAmount ) backgroundFarCornerRadius.stretchBy( - finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius, - amount = farCornerStretchAmount + finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius, + amount = farCornerStretchAmount ) } @@ -373,8 +378,11 @@ class BackPanel( } fun popArrowAlpha(startingVelocity: Float, springForce: SpringForce? = null) { - arrowAlpha.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity, - springForce = springForce) + arrowAlpha.stretchTo( + stretchAmount = 0f, + startingVelocity = startingVelocity, + springForce = springForce + ) } fun resetStretch() { @@ -392,12 +400,10 @@ class BackPanel( backgroundFarCornerRadius.snapToRestingPosition() } - /** - * Updates resting arrow and background size not accounting for stretch - */ + /** Updates resting arrow and background size not accounting for stretch */ internal fun setRestingDimens( - restingParams: EdgePanelParams.BackIndicatorDimens, - animate: Boolean = true + restingParams: EdgePanelParams.BackIndicatorDimens, + animate: Boolean = true ) { horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation) scale.updateRestingPosition(restingParams.scale) @@ -410,27 +416,29 @@ class BackPanel( backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate) backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate) backgroundEdgeCornerRadius.updateRestingPosition( - restingParams.backgroundDimens.edgeCornerRadius, animate + restingParams.backgroundDimens.edgeCornerRadius, + animate ) backgroundFarCornerRadius.updateRestingPosition( - restingParams.backgroundDimens.farCornerRadius, animate + restingParams.backgroundDimens.farCornerRadius, + animate ) } fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos) fun setSpring( - horizontalTranslation: SpringForce? = null, - verticalTranslation: SpringForce? = null, - scale: SpringForce? = null, - arrowLength: SpringForce? = null, - arrowHeight: SpringForce? = null, - arrowAlpha: SpringForce? = null, - backgroundAlpha: SpringForce? = null, - backgroundFarCornerRadius: SpringForce? = null, - backgroundEdgeCornerRadius: SpringForce? = null, - backgroundWidth: SpringForce? = null, - backgroundHeight: SpringForce? = null, + horizontalTranslation: SpringForce? = null, + verticalTranslation: SpringForce? = null, + scale: SpringForce? = null, + arrowLength: SpringForce? = null, + arrowHeight: SpringForce? = null, + arrowAlpha: SpringForce? = null, + backgroundAlpha: SpringForce? = null, + backgroundFarCornerRadius: SpringForce? = null, + backgroundEdgeCornerRadius: SpringForce? = null, + backgroundWidth: SpringForce? = null, + backgroundHeight: SpringForce? = null, ) { arrowLength?.let { this.arrowLength.spring = it } arrowHeight?.let { this.arrowHeight.spring = it } @@ -459,26 +467,28 @@ class BackPanel( if (!isLeftPanel) canvas.scale(-1f, 1f, canvasWidth / 2.0f, 0f) - canvas.translate( - horizontalTranslation.pos, - height * 0.5f + verticalTranslation.pos - ) + canvas.translate(horizontalTranslation.pos, height * 0.5f + verticalTranslation.pos) canvas.scale(scale.pos, scale.pos, scalePivotX, 0f) - val arrowBackground = arrowBackgroundRect.apply { - left = 0f - top = -halfHeight - right = backgroundWidth - bottom = halfHeight - }.toPathWithRoundCorners( - topLeft = edgeCorner, - bottomLeft = edgeCorner, - topRight = farCorner, - bottomRight = farCorner + val arrowBackground = + arrowBackgroundRect + .apply { + left = 0f + top = -halfHeight + right = backgroundWidth + bottom = halfHeight + } + .toPathWithRoundCorners( + topLeft = edgeCorner, + bottomLeft = edgeCorner, + topRight = farCorner, + bottomRight = farCorner + ) + canvas.drawPath( + arrowBackground, + arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() } ) - canvas.drawPath(arrowBackground, - arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() }) val dx = arrowLength.pos val dy = arrowHeight.pos @@ -487,8 +497,8 @@ class BackPanel( // either the tip or the back of the arrow, whichever is closer val arrowOffset = (backgroundWidth - dx) / 2 canvas.translate( - /* dx= */ arrowOffset, - /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */ + /* dx= */ arrowOffset, + /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */ ) val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel) @@ -500,8 +510,8 @@ class BackPanel( } val arrowPath = calculateArrowPath(dx = dx, dy = dy) - val arrowPaint = arrowPaint - .apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() } + val arrowPaint = + arrowPaint.apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() } canvas.drawPath(arrowPath, arrowPaint) canvas.restore() @@ -519,17 +529,23 @@ class BackPanel( } private fun RectF.toPathWithRoundCorners( - topLeft: Float = 0f, - topRight: Float = 0f, - bottomRight: Float = 0f, - bottomLeft: Float = 0f - ): Path = Path().apply { - val corners = floatArrayOf( - topLeft, topLeft, - topRight, topRight, - bottomRight, bottomRight, - bottomLeft, bottomLeft - ) - addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW) - } -}
\ No newline at end of file + topLeft: Float = 0f, + topRight: Float = 0f, + bottomRight: Float = 0f, + bottomLeft: Float = 0f + ): Path = + Path().apply { + val corners = + floatArrayOf( + topLeft, + topLeft, + topRight, + topRight, + bottomRight, + bottomRight, + bottomLeft, + bottomLeft + ) + addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index f8086f5f6fb4..18358a79cbca 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -27,7 +27,6 @@ import android.view.Gravity import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.VelocityTracker -import android.view.View import android.view.ViewConfiguration import android.view.WindowManager import androidx.annotation.VisibleForTesting @@ -37,11 +36,12 @@ import androidx.dynamicanimation.animation.DynamicAnimation import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.ViewController +import com.android.systemui.util.concurrency.BackPanelUiThread +import com.android.systemui.util.concurrency.UiThreadContext import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import javax.inject.Inject @@ -85,11 +85,11 @@ internal constructor( context: Context, private val windowManager: WindowManager, private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, + private val mainHandler: Handler, private val systemClock: SystemClock, private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, - private val latencyTracker: LatencyTracker, + latencyTracker: LatencyTracker, private val interactionJankMonitor: InteractionJankMonitor, ) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin { @@ -104,7 +104,7 @@ internal constructor( constructor( private val windowManager: WindowManager, private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, + @BackPanelUiThread private val uiThreadContext: UiThreadContext, private val systemClock: SystemClock, private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, @@ -113,20 +113,19 @@ internal constructor( ) { /** Construct a [BackPanelController]. */ fun create(context: Context): BackPanelController { - val backPanelController = - BackPanelController( + uiThreadContext.isCurrentThread() + return BackPanelController( context, windowManager, viewConfiguration, - mainHandler, + uiThreadContext.handler, systemClock, vibratorHelper, configurationController, latencyTracker, interactionJankMonitor ) - backPanelController.init() - return backPanelController + .also { it.init() } } } @@ -164,6 +163,7 @@ internal constructor( private val elapsedTimeSinceInactive get() = systemClock.uptimeMillis() - gestureInactiveTime + private val elapsedTimeSinceEntry get() = systemClock.uptimeMillis() - gestureEntryTime @@ -612,6 +612,7 @@ internal constructor( } private var previousPreThresholdWidthInterpolator = params.entryWidthInterpolator + private fun preThresholdWidthStretchAmount(progress: Float): Float { val interpolator = run { val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop @@ -677,8 +678,7 @@ internal constructor( velocityTracker?.run { computeCurrentVelocity(PX_PER_SEC) xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1) - } - ?: 0f + } ?: 0f val isPastFlingVelocityThreshold = flingVelocity > viewConfiguration.scaledMinimumFlingVelocity return flingDistance > minFlingDistance && isPastFlingVelocityThreshold @@ -1006,15 +1006,15 @@ internal constructor( private fun performDeactivatedHapticFeedback() { vibratorHelper.performHapticFeedback( - mView, - HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE + mView, + HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE ) } private fun performActivatedHapticFeedback() { vibratorHelper.performHapticFeedback( - mView, - HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE + mView, + HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE ) } @@ -1028,8 +1028,7 @@ internal constructor( velocityTracker?.run { computeCurrentVelocity(PX_PER_MS) MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity)) - } - ?: valueOnFastVelocity + } ?: valueOnFastVelocity return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor) } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index d0f8412c85b2..2dc09e5ab478 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -44,8 +44,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; import android.icu.text.SimpleDateFormat; -import android.os.Handler; -import android.os.Looper; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; @@ -55,7 +53,6 @@ import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; -import android.view.Choreographer; import android.view.ISystemGestureExclusionListener; import android.view.IWindowManager; import android.view.InputDevice; @@ -75,7 +72,6 @@ import androidx.annotation.DimenRes; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.policy.GestureNavigationSettingsObserver; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.FalsingManager; @@ -94,7 +90,8 @@ import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.phone.LightBarController; -import com.android.systemui.util.Assert; +import com.android.systemui.util.concurrency.BackPanelUiThread; +import com.android.systemui.util.concurrency.UiThreadContext; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.pip.Pip; @@ -136,7 +133,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack public void onSystemGestureExclusionChanged(int displayId, Region systemGestureExclusion, Region unrestrictedOrNull) { if (displayId == mDisplayId) { - mMainExecutor.execute(() -> { + mUiThreadContext.getExecutor().execute(() -> { mExcludeRegion.set(systemGestureExclusion); mUnrestrictedExcludeRegion.set(unrestrictedOrNull != null ? unrestrictedOrNull : systemGestureExclusion); @@ -215,8 +212,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final Point mDisplaySize = new Point(); private final int mDisplayId; - private final Executor mMainExecutor; - private final Handler mMainHandler; + private final UiThreadContext mUiThreadContext; private final Executor mBackgroundExecutor; private final Rect mPipExcludedBounds = new Rect(); @@ -411,8 +407,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack OverviewProxyService overviewProxyService, SysUiState sysUiState, PluginManager pluginManager, - @Main Executor executor, - @Main Handler handler, + @BackPanelUiThread UiThreadContext uiThreadContext, @Background Executor backgroundExecutor, UserTracker userTracker, NavigationModeController navigationModeController, @@ -428,8 +423,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack Provider<LightBarController> lightBarControllerProvider) { mContext = context; mDisplayId = context.getDisplayId(); - mMainExecutor = executor; - mMainHandler = handler; + mUiThreadContext = uiThreadContext; mBackgroundExecutor = backgroundExecutor; mUserTracker = userTracker; mOverviewProxyService = overviewProxyService; @@ -478,7 +472,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack ViewConfiguration.getLongPressTimeout()); mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver( - mMainHandler, mContext, this::onNavigationSettingsChanged); + mUiThreadContext.getHandler(), mContext, this::onNavigationSettingsChanged); updateCurrentUserResources(); } @@ -564,13 +558,15 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mIsAttached = true; mOverviewProxyService.addCallback(mQuickSwitchListener); mSysUiState.addCallback(mSysUiStateCallback); - mInputManager.registerInputDeviceListener(mInputDeviceListener, mMainHandler); - int [] inputDevices = mInputManager.getInputDeviceIds(); + mInputManager.registerInputDeviceListener( + mInputDeviceListener, + mUiThreadContext.getHandler()); + int[] inputDevices = mInputManager.getInputDeviceIds(); for (int inputDeviceId : inputDevices) { mInputDeviceListener.onInputDeviceAdded(inputDeviceId); } updateIsEnabled(); - mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); + mUserTracker.addCallback(mUserChangedCallback, mUiThreadContext.getExecutor()); } /** @@ -617,6 +613,10 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private void updateIsEnabled() { + mUiThreadContext.runWithScissors(this::updateIsEnabledInner); + } + + private void updateIsEnabledInner() { try { Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled"); @@ -661,12 +661,12 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack TaskStackChangeListeners.getInstance().registerTaskStackListener( mTaskStackListener); DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, - mMainExecutor::execute, mOnPropertiesChangedListener); + mUiThreadContext.getExecutor()::execute, mOnPropertiesChangedListener); mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener( mOnIsInPipStateChangedListener)); mDesktopModeOptional.ifPresent( dm -> dm.addDesktopGestureExclusionRegionListener( - mDesktopCornersChangedListener, mMainExecutor)); + mDesktopCornersChangedListener, mUiThreadContext.getExecutor())); try { mWindowManagerService.registerSystemGestureExclusionListener( @@ -677,8 +677,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // Register input event receiver mInputMonitor = new InputMonitorCompat("edge-swipe", mDisplayId); - mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(), - Choreographer.getInstance(), this::onInputEvent); + mInputEventReceiver = mInputMonitor.getInputReceiver(mUiThreadContext.getLooper(), + mUiThreadContext.getChoreographer(), this::onInputEvent); // Add a nav bar panel window resetEdgeBackPlugin(); @@ -773,7 +773,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mUseMLModel = newState; if (mUseMLModel) { - Assert.isMainThread(); + mUiThreadContext.isCurrentThread(); if (mMLModelIsLoading) { Log.d(TAG, "Model tried to load while already loading."); return; @@ -804,12 +804,13 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } BackGestureTfClassifierProvider finalProvider = provider; Map<String, Integer> finalVocab = vocab; - mMainExecutor.execute(() -> onMLModelLoadFinished(finalProvider, finalVocab, threshold)); + mUiThreadContext.getExecutor().execute( + () -> onMLModelLoadFinished(finalProvider, finalVocab, threshold)); } private void onMLModelLoadFinished(BackGestureTfClassifierProvider provider, Map<String, Integer> vocab, float threshold) { - Assert.isMainThread(); + mUiThreadContext.isCurrentThread(); mMLModelIsLoading = false; if (!mUseMLModel) { // This can happen if the user disables Gesture Nav while the model is loading. @@ -1291,7 +1292,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack updateBackAnimationThresholds(); if (mLightBarControllerProvider.get() != null) { mBackAnimation.setStatusBarCustomizer((appearance) -> { - mMainExecutor.execute(() -> + mUiThreadContext.getExecutor().execute(() -> mLightBarControllerProvider.get() .customizeStatusBarAppearance(appearance)); }); @@ -1308,8 +1309,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final OverviewProxyService mOverviewProxyService; private final SysUiState mSysUiState; private final PluginManager mPluginManager; - private final Executor mExecutor; - private final Handler mHandler; + private final UiThreadContext mUiThreadContext; private final Executor mBackgroundExecutor; private final UserTracker mUserTracker; private final NavigationModeController mNavigationModeController; @@ -1327,29 +1327,27 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack @Inject public Factory(OverviewProxyService overviewProxyService, - SysUiState sysUiState, - PluginManager pluginManager, - @Main Executor executor, - @Main Handler handler, - @Background Executor backgroundExecutor, - UserTracker userTracker, - NavigationModeController navigationModeController, - BackPanelController.Factory backPanelControllerFactory, - ViewConfiguration viewConfiguration, - WindowManager windowManager, - IWindowManager windowManagerService, - InputManager inputManager, - Optional<Pip> pipOptional, - Optional<DesktopMode> desktopModeOptional, - FalsingManager falsingManager, - Provider<BackGestureTfClassifierProvider> - backGestureTfClassifierProviderProvider, - Provider<LightBarController> lightBarControllerProvider) { + SysUiState sysUiState, + PluginManager pluginManager, + @BackPanelUiThread UiThreadContext uiThreadContext, + @Background Executor backgroundExecutor, + UserTracker userTracker, + NavigationModeController navigationModeController, + BackPanelController.Factory backPanelControllerFactory, + ViewConfiguration viewConfiguration, + WindowManager windowManager, + IWindowManager windowManagerService, + InputManager inputManager, + Optional<Pip> pipOptional, + Optional<DesktopMode> desktopModeOptional, + FalsingManager falsingManager, + Provider<BackGestureTfClassifierProvider> + backGestureTfClassifierProviderProvider, + Provider<LightBarController> lightBarControllerProvider) { mOverviewProxyService = overviewProxyService; mSysUiState = sysUiState; mPluginManager = pluginManager; - mExecutor = executor; - mHandler = handler; + mUiThreadContext = uiThreadContext; mBackgroundExecutor = backgroundExecutor; mUserTracker = userTracker; mNavigationModeController = navigationModeController; @@ -1367,26 +1365,26 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack /** Construct a {@link EdgeBackGestureHandler}. */ public EdgeBackGestureHandler create(Context context) { - return new EdgeBackGestureHandler( - context, - mOverviewProxyService, - mSysUiState, - mPluginManager, - mExecutor, - mHandler, - mBackgroundExecutor, - mUserTracker, - mNavigationModeController, - mBackPanelControllerFactory, - mViewConfiguration, - mWindowManager, - mWindowManagerService, - mInputManager, - mPipOptional, - mDesktopModeOptional, - mFalsingManager, - mBackGestureTfClassifierProviderProvider, - mLightBarControllerProvider); + return mUiThreadContext.runWithScissors( + () -> new EdgeBackGestureHandler( + context, + mOverviewProxyService, + mSysUiState, + mPluginManager, + mUiThreadContext, + mBackgroundExecutor, + mUserTracker, + mNavigationModeController, + mBackPanelControllerFactory, + mViewConfiguration, + mWindowManager, + mWindowManagerService, + mInputManager, + mPipOptional, + mDesktopModeOptional, + mFalsingManager, + mBackGestureTfClassifierProviderProvider, + mLightBarControllerProvider)); } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt index 439b7e18e0df..db8749f59d9c 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt @@ -10,92 +10,114 @@ import com.android.systemui.res.R data class EdgePanelParams(private var resources: Resources) { data class ArrowDimens( - val length: Float? = 0f, - val height: Float? = 0f, - val alpha: Float = 0f, - val heightSpring: SpringForce? = null, - val lengthSpring: SpringForce? = null, - var alphaSpring: Step<SpringForce>? = null, - var alphaInterpolator: Step<Float>? = null + val length: Float? = 0f, + val height: Float? = 0f, + val alpha: Float = 0f, + val heightSpring: SpringForce? = null, + val lengthSpring: SpringForce? = null, + var alphaSpring: Step<SpringForce>? = null, + var alphaInterpolator: Step<Float>? = null ) data class BackgroundDimens( - val width: Float? = 0f, - val height: Float = 0f, - val edgeCornerRadius: Float = 0f, - val farCornerRadius: Float = 0f, - val alpha: Float = 0f, - val widthSpring: SpringForce? = null, - val heightSpring: SpringForce? = null, - val farCornerRadiusSpring: SpringForce? = null, - val edgeCornerRadiusSpring: SpringForce? = null, - val alphaSpring: SpringForce? = null, + val width: Float? = 0f, + val height: Float = 0f, + val edgeCornerRadius: Float = 0f, + val farCornerRadius: Float = 0f, + val alpha: Float = 0f, + val widthSpring: SpringForce? = null, + val heightSpring: SpringForce? = null, + val farCornerRadiusSpring: SpringForce? = null, + val edgeCornerRadiusSpring: SpringForce? = null, + val alphaSpring: SpringForce? = null, ) data class BackIndicatorDimens( - val horizontalTranslation: Float? = 0f, - val scale: Float = 0f, - val scalePivotX: Float? = null, - val arrowDimens: ArrowDimens, - val backgroundDimens: BackgroundDimens, - val verticalTranslationSpring: SpringForce? = null, - val horizontalTranslationSpring: SpringForce? = null, - val scaleSpring: SpringForce? = null, + val horizontalTranslation: Float? = 0f, + val scale: Float = 0f, + val scalePivotX: Float? = null, + val arrowDimens: ArrowDimens, + val backgroundDimens: BackgroundDimens, + val verticalTranslationSpring: SpringForce? = null, + val horizontalTranslationSpring: SpringForce? = null, + val scaleSpring: SpringForce? = null, ) lateinit var entryIndicator: BackIndicatorDimens private set + lateinit var activeIndicator: BackIndicatorDimens private set + lateinit var cancelledIndicator: BackIndicatorDimens private set + lateinit var flungIndicator: BackIndicatorDimens private set + lateinit var committedIndicator: BackIndicatorDimens private set + lateinit var preThresholdIndicator: BackIndicatorDimens private set + lateinit var fullyStretchedIndicator: BackIndicatorDimens private set // navigation bar edge constants var arrowPaddingEnd: Int = 0 private set + var arrowThickness: Float = 0f private set + // The closest to y var minArrowYPosition: Int = 0 private set + var fingerOffset: Int = 0 private set + var staticTriggerThreshold: Float = 0f private set + var reactivationTriggerThreshold: Float = 0f private set + var deactivationTriggerThreshold: Float = 0f get() = -field private set + lateinit var dynamicTriggerThresholdRange: ClosedRange<Float> private set + var swipeProgressThreshold: Float = 0f private set lateinit var entryWidthInterpolator: Interpolator private set + lateinit var entryWidthTowardsEdgeInterpolator: Interpolator private set + lateinit var activeWidthInterpolator: Interpolator private set + lateinit var arrowAngleInterpolator: Interpolator private set + lateinit var horizontalTranslationInterpolator: Interpolator private set + lateinit var verticalTranslationInterpolator: Interpolator private set + lateinit var farCornerInterpolator: Interpolator private set + lateinit var edgeCornerInterpolator: Interpolator private set + lateinit var heightInterpolator: Interpolator private set @@ -108,7 +130,10 @@ data class EdgePanelParams(private var resources: Resources) { } private fun getDimenFloat(id: Int): Float { - return TypedValue().run { resources.getValue(id, this, true); float } + return TypedValue().run { + resources.getValue(id, this, true) + float + } } private fun getPx(id: Int): Int { @@ -123,11 +148,10 @@ data class EdgePanelParams(private var resources: Resources) { fingerOffset = getPx(R.dimen.navigation_edge_finger_offset) staticTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold) reactivationTriggerThreshold = - getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold) + getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold) deactivationTriggerThreshold = - getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold) - dynamicTriggerThresholdRange = - reactivationTriggerThreshold..deactivationTriggerThreshold + getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold) + dynamicTriggerThresholdRange = reactivationTriggerThreshold..deactivationTriggerThreshold swipeProgressThreshold = getDimen(R.dimen.navigation_edge_action_progress_threshold) entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f) @@ -149,27 +173,31 @@ data class EdgePanelParams(private var resources: Resources) { val commonArrowDimensAlphaThreshold = .165f val commonArrowDimensAlphaFactor = 1.05f - val commonArrowDimensAlphaSpring = Step( - threshold = commonArrowDimensAlphaThreshold, - factor = commonArrowDimensAlphaFactor, - postThreshold = createSpring(180f, 0.9f), - preThreshold = createSpring(2000f, 0.6f) - ) - val commonArrowDimensAlphaSpringInterpolator = Step( - threshold = commonArrowDimensAlphaThreshold, - factor = commonArrowDimensAlphaFactor, - postThreshold = 1f, - preThreshold = 0f - ) - - entryIndicator = BackIndicatorDimens( + val commonArrowDimensAlphaSpring = + Step( + threshold = commonArrowDimensAlphaThreshold, + factor = commonArrowDimensAlphaFactor, + postThreshold = createSpring(180f, 0.9f), + preThreshold = createSpring(2000f, 0.6f) + ) + val commonArrowDimensAlphaSpringInterpolator = + Step( + threshold = commonArrowDimensAlphaThreshold, + factor = commonArrowDimensAlphaFactor, + postThreshold = 1f, + preThreshold = 0f + ) + + entryIndicator = + BackIndicatorDimens( horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin), scale = getDimenFloat(R.dimen.navigation_edge_entry_scale), scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width), horizontalTranslationSpring = createSpring(800f, 0.76f), verticalTranslationSpring = createSpring(30000f, 1f), scaleSpring = createSpring(120f, 0.8f), - arrowDimens = ArrowDimens( + arrowDimens = + ArrowDimens( length = getDimen(R.dimen.navigation_edge_entry_arrow_length), height = getDimen(R.dimen.navigation_edge_entry_arrow_height), alpha = 0f, @@ -177,8 +205,9 @@ data class EdgePanelParams(private var resources: Resources) { heightSpring = createSpring(600f, 0.4f), alphaSpring = commonArrowDimensAlphaSpring, alphaInterpolator = commonArrowDimensAlphaSpringInterpolator - ), - backgroundDimens = BackgroundDimens( + ), + backgroundDimens = + BackgroundDimens( alpha = 1f, width = getDimen(R.dimen.navigation_edge_entry_background_width), height = getDimen(R.dimen.navigation_edge_entry_background_height), @@ -188,16 +217,18 @@ data class EdgePanelParams(private var resources: Resources) { heightSpring = createSpring(1500f, 0.45f), farCornerRadiusSpring = createSpring(300f, 0.5f), edgeCornerRadiusSpring = createSpring(150f, 0.5f), - ) - ) + ) + ) - activeIndicator = BackIndicatorDimens( + activeIndicator = + BackIndicatorDimens( horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin), scale = getDimenFloat(R.dimen.navigation_edge_active_scale), horizontalTranslationSpring = createSpring(1000f, 0.8f), scaleSpring = createSpring(325f, 0.55f), scalePivotX = getDimen(R.dimen.navigation_edge_active_background_width), - arrowDimens = ArrowDimens( + arrowDimens = + ArrowDimens( length = getDimen(R.dimen.navigation_edge_active_arrow_length), height = getDimen(R.dimen.navigation_edge_active_arrow_height), alpha = 1f, @@ -205,8 +236,9 @@ data class EdgePanelParams(private var resources: Resources) { heightSpring = activeCommittedArrowHeightSpring, alphaSpring = commonArrowDimensAlphaSpring, alphaInterpolator = commonArrowDimensAlphaSpringInterpolator - ), - backgroundDimens = BackgroundDimens( + ), + backgroundDimens = + BackgroundDimens( alpha = 1f, width = getDimen(R.dimen.navigation_edge_active_background_width), height = getDimen(R.dimen.navigation_edge_active_background_height), @@ -216,16 +248,18 @@ data class EdgePanelParams(private var resources: Resources) { heightSpring = createSpring(10000f, 1f), edgeCornerRadiusSpring = createSpring(2600f, 0.855f), farCornerRadiusSpring = createSpring(1200f, 0.30f), - ) - ) + ) + ) - preThresholdIndicator = BackIndicatorDimens( + preThresholdIndicator = + BackIndicatorDimens( horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin), scale = getDimenFloat(R.dimen.navigation_edge_pre_threshold_scale), scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width), scaleSpring = createSpring(120f, 0.8f), horizontalTranslationSpring = createSpring(6000f, 1f), - arrowDimens = ArrowDimens( + arrowDimens = + ArrowDimens( length = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_length), height = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_height), alpha = 1f, @@ -233,32 +267,36 @@ data class EdgePanelParams(private var resources: Resources) { heightSpring = createSpring(100f, 0.6f), alphaSpring = commonArrowDimensAlphaSpring, alphaInterpolator = commonArrowDimensAlphaSpringInterpolator - ), - backgroundDimens = BackgroundDimens( + ), + backgroundDimens = + BackgroundDimens( alpha = 1f, width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width), height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height), edgeCornerRadius = - getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners), + getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners), farCornerRadius = - getDimen(R.dimen.navigation_edge_pre_threshold_far_corners), + getDimen(R.dimen.navigation_edge_pre_threshold_far_corners), widthSpring = createSpring(650f, 1f), heightSpring = createSpring(1500f, 0.45f), farCornerRadiusSpring = createSpring(300f, 1f), edgeCornerRadiusSpring = createSpring(250f, 0.5f), - ) - ) + ) + ) - committedIndicator = activeIndicator.copy( + committedIndicator = + activeIndicator.copy( horizontalTranslation = null, scalePivotX = null, - arrowDimens = activeIndicator.arrowDimens.copy( + arrowDimens = + activeIndicator.arrowDimens.copy( lengthSpring = activeCommittedArrowLengthSpring, heightSpring = activeCommittedArrowHeightSpring, length = null, height = null, - ), - backgroundDimens = activeIndicator.backgroundDimens.copy( + ), + backgroundDimens = + activeIndicator.backgroundDimens.copy( alpha = 0f, // explicitly set to null to preserve previous width upon state change width = null, @@ -267,49 +305,57 @@ data class EdgePanelParams(private var resources: Resources) { edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring, farCornerRadiusSpring = flungCommittedFarCornerSpring, alphaSpring = createSpring(1400f, 1f), - ), + ), scale = 0.86f, scaleSpring = createSpring(5700f, 1f), - ) + ) - flungIndicator = committedIndicator.copy( - arrowDimens = committedIndicator.arrowDimens.copy( + flungIndicator = + committedIndicator.copy( + arrowDimens = + committedIndicator.arrowDimens.copy( lengthSpring = createSpring(850f, 0.46f), heightSpring = createSpring(850f, 0.46f), length = activeIndicator.arrowDimens.length, height = activeIndicator.arrowDimens.height - ), - backgroundDimens = committedIndicator.backgroundDimens.copy( + ), + backgroundDimens = + committedIndicator.backgroundDimens.copy( widthSpring = flungCommittedWidthSpring, heightSpring = flungCommittedHeightSpring, edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring, farCornerRadiusSpring = flungCommittedFarCornerSpring, - ) - ) + ) + ) - cancelledIndicator = entryIndicator.copy( - backgroundDimens = entryIndicator.backgroundDimens.copy( + cancelledIndicator = + entryIndicator.copy( + backgroundDimens = + entryIndicator.backgroundDimens.copy( width = 0f, alpha = 0f, alphaSpring = createSpring(450f, 1f) - ) - ) + ) + ) - fullyStretchedIndicator = BackIndicatorDimens( + fullyStretchedIndicator = + BackIndicatorDimens( horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin), scale = getDimenFloat(R.dimen.navigation_edge_stretch_scale), horizontalTranslationSpring = null, verticalTranslationSpring = null, scaleSpring = null, - arrowDimens = ArrowDimens( + arrowDimens = + ArrowDimens( length = getDimen(R.dimen.navigation_edge_stretched_arrow_length), height = getDimen(R.dimen.navigation_edge_stretched_arrow_height), alpha = 1f, alphaSpring = null, heightSpring = null, lengthSpring = null, - ), - backgroundDimens = BackgroundDimens( + ), + backgroundDimens = + BackgroundDimens( alpha = 1f, width = getDimen(R.dimen.navigation_edge_stretch_background_width), height = getDimen(R.dimen.navigation_edge_stretch_background_height), @@ -320,11 +366,11 @@ data class EdgePanelParams(private var resources: Resources) { heightSpring = null, edgeCornerRadiusSpring = null, farCornerRadiusSpring = null, - ) - ) + ) + ) } } fun createSpring(stiffness: Float, dampingRatio: Float): SpringForce { return SpringForce().setStiffness(stiffness).setDampingRatio(dampingRatio) -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.kt b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.kt index deb0fed0ffc8..954e94af1c1a 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.kt @@ -26,11 +26,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.people.ui.compose.PeopleScreen -import com.android.systemui.people.ui.view.PeopleViewBinder -import com.android.systemui.people.ui.view.PeopleViewBinder.bind import com.android.systemui.people.ui.viewmodel.PeopleViewModel import javax.inject.Inject import kotlinx.coroutines.launch @@ -38,10 +34,7 @@ import kotlinx.coroutines.launch /** People Tile Widget configuration activity that shows the user their conversation tiles. */ class PeopleSpaceActivity @Inject -constructor( - private val viewModelFactory: PeopleViewModel.Factory, - private val featureFlags: FeatureFlags, -) : ComponentActivity() { +constructor(private val viewModelFactory: PeopleViewModel.Factory) : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setResult(RESULT_CANCELED) @@ -66,17 +59,7 @@ constructor( } // Set the content of the activity, using either the View or Compose implementation. - if (featureFlags.isEnabled(Flags.COMPOSE_PEOPLE_SPACE)) { - Log.d(TAG, "Using the Compose implementation of the PeopleSpaceActivity") - setContent { - PlatformTheme { PeopleScreen(viewModel, onResult = { finishActivity(it) }) } - } - } else { - Log.d(TAG, "Using the View implementation of the PeopleSpaceActivity") - val view = PeopleViewBinder.create(this) - bind(view, viewModel, lifecycleOwner = this, onResult = { finishActivity(it) }) - setContentView(view) - } + setContent { PlatformTheme { PeopleScreen(viewModel, onResult = { finishActivity(it) }) } } } private fun finishActivity(result: PeopleViewModel.Result) { diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java deleted file mode 100644 index 59c76adb721b..000000000000 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.people; - -import android.app.people.PeopleSpaceTile; -import android.content.Context; -import android.content.pm.LauncherApps; -import android.graphics.Bitmap; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.systemui.res.R; - -/** - * PeopleSpaceTileView renders an individual person's tile with associated status. - */ -public class PeopleSpaceTileView extends LinearLayout { - - private View mTileView; - private TextView mNameView; - private ImageView mPersonIconView; - - public PeopleSpaceTileView(Context context, ViewGroup view, String shortcutId, boolean isLast) { - super(context); - mTileView = view.findViewWithTag(shortcutId); - if (mTileView == null) { - LayoutInflater inflater = LayoutInflater.from(context); - mTileView = inflater.inflate(R.layout.people_space_tile_view, view, false); - view.addView(mTileView, LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); - mTileView.setTag(shortcutId); - - // If it's not the last conversation in this section, add a divider. - if (!isLast) { - inflater.inflate(R.layout.people_space_activity_list_divider, view, true); - } - } - mNameView = mTileView.findViewById(R.id.tile_view_name); - mPersonIconView = mTileView.findViewById(R.id.tile_view_person_icon); - } - - /** Sets the name text on the tile. */ - public void setName(String name) { - mNameView.setText(name); - } - - /** Sets the person and package drawable on the tile. */ - public void setPersonIcon(Bitmap bitmap) { - mPersonIconView.setImageBitmap(bitmap); - } - - /** Sets the click listener of the tile. */ - public void setOnClickListener(LauncherApps launcherApps, PeopleSpaceTile tile) { - mTileView.setOnClickListener(v -> - launcherApps.startShortcut(tile.getPackageName(), tile.getId(), null, null, - tile.getUserHandle())); - } - - /** Sets the click listener of the tile directly. */ - public void setOnClickListener(OnClickListener onClickListener) { - mTileView.setOnClickListener(onClickListener); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt deleted file mode 100644 index 10a2b3ce7b85..000000000000 --- a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.people.ui.view - -import android.content.Context -import android.graphics.Color -import android.graphics.Outline -import android.graphics.drawable.GradientDrawable -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.ViewOutlineProvider -import android.widget.LinearLayout -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.Lifecycle.State.CREATED -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.people.PeopleSpaceTileView -import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel -import com.android.systemui.people.ui.viewmodel.PeopleViewModel -import com.android.systemui.res.R -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.launch - -/** A ViewBinder for [PeopleViewModel]. */ -object PeopleViewBinder { - private const val TAG = "PeopleViewBinder" - - /** - * The [ViewOutlineProvider] used to clip the corner radius of the recent and priority lists. - */ - private val ViewOutlineProvider = - object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect( - 0, - 0, - view.width, - view.height, - view.context.resources.getDimension(R.dimen.people_space_widget_radius), - ) - } - } - - /** Create a [View] that can later be [bound][bind] to a [PeopleViewModel]. */ - @JvmStatic - fun create(context: Context): ViewGroup { - return LayoutInflater.from(context) - .inflate(R.layout.people_space_activity, /* root= */ null) as ViewGroup - } - - /** Bind [view] to [viewModel]. */ - @JvmStatic - fun bind( - view: ViewGroup, - viewModel: PeopleViewModel, - lifecycleOwner: LifecycleOwner, - onResult: (PeopleViewModel.Result) -> Unit, - ) { - // Call [onResult] as soon as a result is available. - lifecycleOwner.lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(CREATED) { - viewModel.result.collect { result -> - if (result != null) { - viewModel.clearResult() - onResult(result) - } - } - } - } - - // Start collecting the UI data once the Activity is STARTED. - lifecycleOwner.lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - combine( - viewModel.priorityTiles, - viewModel.recentTiles, - ) { priority, recent -> - priority to recent - } - .collect { (priorityTiles, recentTiles) -> - if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) { - setConversationsContent( - view, - priorityTiles, - recentTiles, - viewModel.onTileClicked, - ) - } else { - setNoConversationsContent(view, viewModel.onUserJourneyCancelled) - } - } - } - } - } - - private fun setNoConversationsContent(view: ViewGroup, onGotItClicked: () -> Unit) { - // This should never happen. - if (view.childCount > 1) { - error("view has ${view.childCount} children, it should have maximum 1") - } - - // The static content for no conversations is already shown. - if (view.findViewById<View>(R.id.top_level_no_conversations) != null) { - return - } - - // If we were showing the content with conversations earlier, remove it. - if (view.childCount == 1) { - view.removeViewAt(0) - } - - val context = view.context - val noConversationsView = - LayoutInflater.from(context) - .inflate(R.layout.people_space_activity_no_conversations, /* root= */ view) - - noConversationsView.requireViewById<View>(R.id.got_it_button).setOnClickListener { - onGotItClicked() - } - - // The Tile preview has colorBackground as its background. Change it so it's different than - // the activity's background. - val item = noConversationsView.requireViewById<LinearLayout>(android.R.id.background) - val shape = item.background as GradientDrawable - val ta = - context.theme.obtainStyledAttributes( - intArrayOf(com.android.internal.R.attr.colorSurface) - ) - shape.setColor(ta.getColor(0, Color.WHITE)) - ta.recycle() - } - - private fun setConversationsContent( - view: ViewGroup, - priorityTiles: List<PeopleTileViewModel>, - recentTiles: List<PeopleTileViewModel>, - onTileClicked: (PeopleTileViewModel) -> Unit, - ) { - // This should never happen. - if (view.childCount > 1) { - error("view has ${view.childCount} children, it should have maximum 1") - } - - // Inflate the content with conversations, if it's not already. - if (view.findViewById<View>(R.id.top_level_with_conversations) == null) { - // If we were showing the content without conversations earlier, remove it. - if (view.childCount == 1) { - view.removeViewAt(0) - } - - LayoutInflater.from(view.context) - .inflate(R.layout.people_space_activity_with_conversations, /* root= */ view) - } - - // TODO(b/193782241): Replace the NestedScrollView + 2x LinearLayout from this layout into a - // single RecyclerView once this screen is tested by screenshot tests. Introduce a - // PeopleSpaceTileViewBinder that will properly create and bind the View associated to a - // PeopleSpaceTileViewModel (and remove the PeopleSpaceTileView class). - val conversationsView = view.requireViewById<View>(R.id.top_level_with_conversations) - setTileViews( - conversationsView, - R.id.priority, - R.id.priority_tiles, - priorityTiles, - onTileClicked, - ) - - setTileViews( - conversationsView, - R.id.recent, - R.id.recent_tiles, - recentTiles, - onTileClicked, - ) - } - - /** Sets a [PeopleSpaceTileView]s for each conversation. */ - private fun setTileViews( - root: View, - tilesListId: Int, - tilesId: Int, - tiles: List<PeopleTileViewModel>, - onTileClicked: (PeopleTileViewModel) -> Unit, - ) { - // Remove any previously added tile. - // TODO(b/193782241): Once this list is a big RecyclerView, set the current list and use - // DiffUtil to do as less addView/removeView as possible. - val layout = root.requireViewById<ViewGroup>(tilesId) - layout.removeAllViews() - layout.outlineProvider = ViewOutlineProvider - - val tilesListView = root.requireViewById<LinearLayout>(tilesListId) - if (tiles.isEmpty()) { - tilesListView.visibility = View.GONE - return - } - tilesListView.visibility = View.VISIBLE - - // Add each tile. - tiles.forEachIndexed { i, tile -> - val tileView = - PeopleSpaceTileView(root.context, layout, tile.key.shortcutId, i == tiles.size - 1) - bindTileView(tileView, tile, onTileClicked) - } - } - - /** Sets [tileView] with the data in [conversation]. */ - private fun bindTileView( - tileView: PeopleSpaceTileView, - tile: PeopleTileViewModel, - onTileClicked: (PeopleTileViewModel) -> Unit, - ) { - try { - tileView.setName(tile.username) - tileView.setPersonIcon(tile.icon) - tileView.setOnClickListener { onTileClicked(tile) } - } catch (e: Exception) { - Log.e(TAG, "Couldn't retrieve shortcut information", e) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 4ee2db796aef..cc0901fca822 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -307,7 +307,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { } else { // Set the horizontal paddings unless the view is the Compose implementation of the // footer actions. - if (view.getTag(R.id.tag_compose_qs_footer_actions) == null) { + if (view.getId() != R.id.qs_footer_actions) { view.setPaddingRelative( mContentHorizontalPadding, view.getPaddingTop(), diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java index e4249757d737..38d7290fc3bc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java @@ -219,6 +219,13 @@ public class QSFragmentLegacy extends LifecycleFragment implements QS { } @Override + public void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) { + if (mQsImpl != null) { + mQsImpl.setShouldUpdateSquishinessOnMedia(shouldUpdate); + } + } + + @Override public void setListening(boolean listening) { if (mQsImpl != null) { mQsImpl.setListening(listening); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java index 1f4838e85e79..8c0d122e5c00 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java @@ -34,11 +34,11 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; import androidx.annotation.FloatRange; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.compose.ui.platform.ComposeView; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; @@ -48,15 +48,12 @@ import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.systemui.Dumpable; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.media.controls.ui.view.MediaHost; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.qs.QSContainerController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSComponent; -import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.res.R; @@ -117,11 +114,9 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl private final MediaHost mQqsMediaHost; private final QSDisableFlagsLogger mQsDisableFlagsLogger; private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; - private final FeatureFlags mFeatureFlags; private final QSLogger mLogger; private final FooterActionsController mFooterActionsController; private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory; - private final FooterActionsViewBinder mFooterActionsViewBinder; private final ListeningAndVisibilityLifecycleOwner mListeningAndVisibilityLifecycleOwner; private boolean mShowCollapsedOnKeyguard; private boolean mLastKeyguardAndExpanded; @@ -168,11 +163,14 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl private boolean mIsSmallScreen; + /** Should the squishiness fraction be updated on the media host. */ + private boolean mShouldUpdateMediaSquishiness; + private CommandQueue mCommandQueue; private View mRootView; @Nullable - private View mFooterActionsView; + private ComposeView mFooterActionsView; @Inject public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, @@ -184,23 +182,19 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl DumpManager dumpManager, QSLogger qsLogger, FooterActionsController footerActionsController, FooterActionsViewModel.Factory footerActionsViewModelFactory, - FooterActionsViewBinder footerActionsViewBinder, - LargeScreenShadeInterpolator largeScreenShadeInterpolator, - FeatureFlags featureFlags) { + LargeScreenShadeInterpolator largeScreenShadeInterpolator) { mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler; mQsMediaHost = qsMediaHost; mQqsMediaHost = qqsMediaHost; mQsDisableFlagsLogger = qsDisableFlagsLogger; mLogger = qsLogger; mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; - mFeatureFlags = featureFlags; mCommandQueue = commandQueue; mBypassController = keyguardBypassController; mStatusBarStateController = statusBarStateController; mDumpManager = dumpManager; mFooterActionsController = footerActionsController; mFooterActionsViewModelFactory = footerActionsViewModelFactory; - mFooterActionsViewBinder = footerActionsViewBinder; mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner(); if (SceneContainerFlag.isEnabled()) { mStatusBarState = StatusBarState.SHADE; @@ -294,43 +288,9 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl } private void bindFooterActionsView(View root) { - LinearLayout footerActionsView = root.findViewById(R.id.qs_footer_actions); - - if (!mFeatureFlags.isEnabled(Flags.COMPOSE_QS_FOOTER_ACTIONS)) { - Log.d(TAG, "Binding the View implementation of the QS footer actions"); - mFooterActionsView = footerActionsView; - mFooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel, - mListeningAndVisibilityLifecycleOwner); - return; - } - - // Compose is available, so let's use the Compose implementation of the footer actions. - Log.d(TAG, "Binding the Compose implementation of the QS footer actions"); - View composeView = QSUtils.createFooterActionsView(root.getContext(), + mFooterActionsView = root.findViewById(R.id.qs_footer_actions); + QSUtils.setFooterActionsViewContent(mFooterActionsView, mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner); - mFooterActionsView = composeView; - - // The id R.id.qs_footer_actions is used by QSContainerImpl to set the horizontal margin - // to all views except for qs_footer_actions, so we set it to the Compose view. - composeView.setId(R.id.qs_footer_actions); - - // Set this tag so that QSContainerImpl does not add horizontal paddings to this Compose - // implementation of the footer actions. They will be set in Compose instead so that the - // background fills the full screen width. - composeView.setTag(R.id.tag_compose_qs_footer_actions, true); - - // Set the same elevation as the View implementation, otherwise the footer actions will be - // drawn below the scroll view with QS grid and clicks won't get through on small devices - // where there isn't enough vertical space to show all the tiles and the footer actions. - composeView.setElevation( - composeView.getContext().getResources().getDimension(R.dimen.qs_panel_elevation)); - - // Replace the View by the Compose provided one. - ViewGroup parent = (ViewGroup) footerActionsView.getParent(); - ViewGroup.LayoutParams layoutParams = footerActionsView.getLayoutParams(); - int index = parent.indexOfChild(footerActionsView); - parent.removeViewAt(index); - parent.addView(composeView, index, layoutParams); } @Override @@ -662,6 +622,12 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl } @Override + public void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) { + if (DEBUG) Log.d(TAG, "setShouldUpdateSquishinessOnMedia " + shouldUpdate); + mShouldUpdateMediaSquishiness = shouldUpdate; + } + + @Override public void setQsExpansion(float expansion, float panelExpansionFraction, float proposedTranslation, float squishinessFraction) { float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation; @@ -740,9 +706,11 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl if (mQSAnimator != null) { mQSAnimator.setPosition(expansion); } - if (!mInSplitShade + if (!mShouldUpdateMediaSquishiness + && (!mInSplitShade || mStatusBarStateController.getState() == KEYGUARD - || mStatusBarStateController.getState() == SHADE_LOCKED) { + || mStatusBarStateController.getState() == SHADE_LOCKED) + ) { // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen // and media player expect no change by squishiness in lock screen shade. Don't bother // squishing mQsMediaHost when not in split shade to prevent problems with stale state. @@ -1038,6 +1006,7 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade); indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress); indentingPw.println("mOverScrolling: " + mOverScrolling); + indentingPw.println("mShouldUpdateMediaSquishiness: " + mShouldUpdateMediaSquishiness); indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing()); View view = getView(); if (view != null) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSUtils.kt b/packages/SystemUI/src/com/android/systemui/qs/QSUtils.kt index 15c3f271469d..5482e6da9a57 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSUtils.kt @@ -1,10 +1,9 @@ package com.android.systemui.qs import android.content.Context -import android.view.View +import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.LifecycleOwner import com.android.compose.theme.PlatformTheme -import com.android.compose.ui.platform.DensityAwareComposeView import com.android.internal.policy.SystemBarUtils import com.android.systemui.qs.footer.ui.compose.FooterActions import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel @@ -29,13 +28,11 @@ object QSUtils { } @JvmStatic - fun createFooterActionsView( - context: Context, + fun setFooterActionsViewContent( + view: ComposeView, viewModel: FooterActionsViewModel, qsVisibilityLifecycleOwner: LifecycleOwner, - ): View { - return DensityAwareComposeView(context).apply { - setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } } - } + ) { + view.setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt deleted file mode 100644 index 0995dd4e592e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.footer.ui.binder - -import android.content.Context -import android.graphics.PorterDuff -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.animation.Expandable -import com.android.systemui.common.ui.binder.IconViewBinder -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.people.ui.view.PeopleViewBinder.bind -import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel -import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel -import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel -import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel -import com.android.systemui.res.R -import javax.inject.Inject -import kotlin.math.roundToInt -import kotlinx.coroutines.launch - -/** A ViewBinder for [FooterActionsViewBinder]. */ -@SysUISingleton -class FooterActionsViewBinder @Inject constructor() { - /** Create a view that can later be [bound][bind] to a [FooterActionsViewModel]. */ - fun create(context: Context): LinearLayout { - return LayoutInflater.from(context).inflate(R.layout.footer_actions, /* root= */ null) - as LinearLayout - } - - /** Bind [view] to [viewModel]. */ - fun bind( - view: LinearLayout, - viewModel: FooterActionsViewModel, - qsVisibilityLifecycleOwner: LifecycleOwner, - ) { - view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES - - // Add the views used by this new implementation. - val context = view.context - val inflater = LayoutInflater.from(context) - - val securityHolder = TextButtonViewHolder.createAndAdd(inflater, view) - val foregroundServicesWithTextHolder = TextButtonViewHolder.createAndAdd(inflater, view) - val foregroundServicesWithNumberHolder = NumberButtonViewHolder.createAndAdd(inflater, view) - val userSwitcherHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = false) - val settingsHolder = - IconButtonViewHolder.createAndAdd(inflater, view, isLast = viewModel.power == null) - - // Bind the static power and settings buttons. - bindButton(settingsHolder, viewModel.settings) - - if (viewModel.power != null) { - val powerHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = true) - bindButton(powerHolder, viewModel.power) - } - - // There are 2 lifecycle scopes we are using here: - // 1) The scope created by [repeatWhenAttached] when [view] is attached, and destroyed - // when the [view] is detached. We use this as the parent scope for all our [viewModel] - // state collection, given that we don't want to do any work when [view] is detached. - // 2) The scope owned by [lifecycleOwner], which should be RESUMED only when Quick - // Settings are visible. We use this to make sure we collect UI state only when the - // View is visible. - // - // Given that we start our collection when the Quick Settings become visible, which happens - // every time the user swipes down the shade, we remember our previous UI state already - // bound to the UI to avoid binding the same values over and over for nothing. - - // TODO(b/242040009): Look into using only a single scope. - - var previousSecurity: FooterActionsSecurityButtonViewModel? = null - var previousForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null - var previousUserSwitcher: FooterActionsButtonViewModel? = null - - // Listen for ViewModel updates when the View is attached. - view.repeatWhenAttached { - val attachedScope = this.lifecycleScope - - attachedScope.launch { - // Listen for dialog requests as soon as we are attached, even when not visible. - // TODO(b/242040009): Should this move somewhere else? - launch { viewModel.observeDeviceMonitoringDialogRequests(view.context) } - - // Make sure we set the correct alphas even when QS are not currently shown. - launch { viewModel.alpha.collect { view.alpha = it } } - launch { - viewModel.backgroundAlpha.collect { - view.background?.alpha = (it * 255).roundToInt() - } - } - } - - // Listen for model changes only when QS are visible. - qsVisibilityLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { - // Security. - launch { - viewModel.security.collect { security -> - if (previousSecurity != security) { - bindSecurity(view.context, securityHolder, security) - previousSecurity = security - } - } - } - - // Foreground services. - launch { - viewModel.foregroundServices.collect { foregroundServices -> - if (previousForegroundServices != foregroundServices) { - bindForegroundService( - foregroundServicesWithNumberHolder, - foregroundServicesWithTextHolder, - foregroundServices, - ) - previousForegroundServices = foregroundServices - } - } - } - - // User switcher. - launch { - viewModel.userSwitcher.collect { userSwitcher -> - if (previousUserSwitcher != userSwitcher) { - bindButton(userSwitcherHolder, userSwitcher) - previousUserSwitcher = userSwitcher - } - } - } - } - } - } - - private fun bindSecurity( - quickSettingsContext: Context, - securityHolder: TextButtonViewHolder, - security: FooterActionsSecurityButtonViewModel?, - ) { - val securityView = securityHolder.view - securityView.isVisible = security != null - if (security == null) { - return - } - - // Make sure that the chevron is visible and that the button is clickable if there is a - // listener. - val chevron = securityHolder.chevron - val onClick = security.onClick - if (onClick != null) { - securityView.isClickable = true - securityView.setOnClickListener { - onClick(quickSettingsContext, Expandable.fromView(securityView)) - } - chevron.isVisible = true - } else { - securityView.isClickable = false - securityView.setOnClickListener(null) - chevron.isVisible = false - } - - securityHolder.text.text = security.text - securityHolder.newDot.isVisible = false - IconViewBinder.bind(security.icon, securityHolder.icon) - } - - private fun bindForegroundService( - foregroundServicesWithNumberHolder: NumberButtonViewHolder, - foregroundServicesWithTextHolder: TextButtonViewHolder, - foregroundServices: FooterActionsForegroundServicesButtonViewModel?, - ) { - val foregroundServicesWithNumberView = foregroundServicesWithNumberHolder.view - val foregroundServicesWithTextView = foregroundServicesWithTextHolder.view - if (foregroundServices == null) { - foregroundServicesWithNumberView.isVisible = false - foregroundServicesWithTextView.isVisible = false - return - } - - val foregroundServicesCount = foregroundServices.foregroundServicesCount - if (foregroundServices.displayText) { - // Button with text, icon and chevron. - foregroundServicesWithNumberView.isVisible = false - - foregroundServicesWithTextView.isVisible = true - foregroundServicesWithTextView.setOnClickListener { - foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) - } - foregroundServicesWithTextHolder.text.text = foregroundServices.text - foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges - } else { - // Small button with the number only. - foregroundServicesWithTextView.isVisible = false - - foregroundServicesWithNumberView.isVisible = true - foregroundServicesWithNumberView.setOnClickListener { - foregroundServices.onClick(Expandable.fromView(foregroundServicesWithNumberView)) - } - foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString() - foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text - foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges - } - } - - private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) { - val buttonView = button.view - buttonView.id = model?.id ?: View.NO_ID - buttonView.isVisible = model != null - if (model == null) { - return - } - - val backgroundResource = - when (model.backgroundColor) { - R.attr.shadeInactive -> R.drawable.qs_footer_action_circle - R.attr.shadeActive -> R.drawable.qs_footer_action_circle_color - else -> error("Unsupported icon background resource ${model.backgroundColor}") - } - buttonView.setBackgroundResource(backgroundResource) - buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) } - - val icon = model.icon - val iconView = button.icon - - IconViewBinder.bind(icon, iconView) - if (model.iconTint != null) { - iconView.setColorFilter(model.iconTint, PorterDuff.Mode.SRC_IN) - } else { - iconView.clearColorFilter() - } - } -} - -private class TextButtonViewHolder(val view: View) { - val icon = view.requireViewById<ImageView>(R.id.icon) - val text = view.requireViewById<TextView>(R.id.text) - val newDot = view.requireViewById<ImageView>(R.id.new_dot) - val chevron = view.requireViewById<ImageView>(R.id.chevron_icon) - - companion object { - fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): TextButtonViewHolder { - val view = - inflater.inflate( - R.layout.footer_actions_text_button, - /* root= */ root, - /* attachToRoot= */ false, - ) - root.addView(view) - return TextButtonViewHolder(view) - } - } -} - -private class NumberButtonViewHolder(val view: View) { - val number = view.requireViewById<TextView>(R.id.number) - val newDot = view.requireViewById<ImageView>(R.id.new_dot) - - companion object { - fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): NumberButtonViewHolder { - val view = - inflater.inflate( - R.layout.footer_actions_number_button, - /* root= */ root, - /* attachToRoot= */ false, - ) - root.addView(view) - return NumberButtonViewHolder(view) - } - } -} - -private class IconButtonViewHolder(val view: View) { - val icon = view.requireViewById<ImageView>(R.id.icon) - - companion object { - fun createAndAdd( - inflater: LayoutInflater, - root: ViewGroup, - isLast: Boolean, - ): IconButtonViewHolder { - val view = - inflater.inflate( - R.layout.footer_actions_icon_button, - /* root= */ root, - /* attachToRoot= */ false, - ) - - // All buttons have a background with an inset of qs_footer_action_inset, so the last - // button must have a negative inset of -qs_footer_action_inset to compensate and be - // aligned with its parent. - val marginEnd = - if (isLast) { - -view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) - } else { - 0 - } - - val size = - view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_button_size) - root.addView( - view, - LinearLayout.LayoutParams(size, size).apply { this.marginEnd = marginEnd }, - ) - return IconButtonViewHolder(view) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt index e581bfceb18f..095bdf2ff5bd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt @@ -19,38 +19,26 @@ package com.android.systemui.qs.panels.data.repository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -/** Repository for retrieving the list of [TileSpec] to be displayed as icons. */ +/** Repository for checking if a tile should be displayed as an icon. */ interface IconTilesRepository { - val iconTilesSpecs: StateFlow<Set<TileSpec>> + fun isIconTile(spec: TileSpec): Boolean } @SysUISingleton class IconTilesRepositoryImpl @Inject constructor() : IconTilesRepository { - private val _iconTilesSpecs = - MutableStateFlow( + override fun isIconTile(spec: TileSpec): Boolean { + return !LARGE_TILES.contains(spec) + } + + companion object { + private val LARGE_TILES = setOf( - TileSpec.create("airplane"), - TileSpec.create("battery"), - TileSpec.create("cameratoggle"), - TileSpec.create("cast"), - TileSpec.create("color_correction"), - TileSpec.create("inversion"), - TileSpec.create("saver"), + TileSpec.create("internet"), + TileSpec.create("bt"), TileSpec.create("dnd"), - TileSpec.create("flashlight"), - TileSpec.create("location"), - TileSpec.create("mictoggle"), - TileSpec.create("nfc"), - TileSpec.create("night"), - TileSpec.create("rotation") + TileSpec.create("cast"), ) - ) - - /** Set of toggleable tiles that are suitable for being shown as an icon. */ - override val iconTilesSpecs: StateFlow<Set<TileSpec>> = _iconTilesSpecs.asStateFlow() + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt index ccc1c6e9135c..524ea8b70100 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt @@ -20,10 +20,9 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.panels.data.repository.IconTilesRepository import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject -import kotlinx.coroutines.flow.StateFlow /** Interactor for retrieving the list of [TileSpec] to be displayed as icons. */ @SysUISingleton -class IconTilesInteractor @Inject constructor(repo: IconTilesRepository) { - val iconTilesSpecs: StateFlow<Set<TileSpec>> = repo.iconTilesSpecs +class IconTilesInteractor @Inject constructor(private val repo: IconTilesRepository) { + fun isIconTile(spec: TileSpec): Boolean = repo.isIconTile(spec) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt index b437f645d4bc..e99c64c8c1f3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt @@ -38,14 +38,13 @@ constructor( override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> { val newTiles: MutableList<TileSpec> = mutableListOf() val row = TileRow<TileSpec>(columns = gridSizeInteractor.columns.value) - val iconTilesSet = iconTilesInteractor.iconTilesSpecs.value val tilesQueue = ArrayDeque( tiles.map { SizedTile( it, width = - if (iconTilesSet.contains(it)) { + if (iconTilesInteractor.isIconTile(it)) { 1 } else { 2 diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt index 4aeaa7d23771..2f0fe221a2b4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt @@ -52,15 +52,13 @@ constructor( tiles.forEach { it.startListening(token) } onDispose { tiles.forEach { it.stopListening(token) } } } - val iconTilesSpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle() val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle() TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) { items( tiles.size, span = { index -> - val iconOnly = iconTilesSpecs.contains(tiles[index].spec) - if (iconOnly) { + if (iconTilesViewModel.isIconTile(tiles[index].spec)) { GridItemSpan(1) } else { GridItemSpan(2) @@ -69,7 +67,7 @@ constructor( ) { index -> Tile( tile = tiles[index], - iconOnly = iconTilesSpecs.contains(tiles[index].spec), + iconOnly = iconTilesViewModel.isIconTile(tiles[index].spec), modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)) ) } @@ -83,12 +81,11 @@ constructor( onAddTile: (TileSpec, Int) -> Unit, onRemoveTile: (TileSpec) -> Unit, ) { - val iconOnlySpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle() val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle() DefaultEditTileGrid( tiles = tiles, - iconOnlySpecs = iconOnlySpecs, + isIconOnly = iconTilesViewModel::isIconTile, columns = GridCells.Fixed(columns), modifier = modifier, onAddTile = onAddTile, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt index 708ef0dd7ff6..d60076745a78 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt @@ -38,7 +38,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource @@ -66,12 +65,11 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition tiles.forEach { it.startListening(token) } onDispose { tiles.forEach { it.stopListening(token) } } } - val iconTilesSpecs by viewModel.iconTilesSpecs.collectAsStateWithLifecycle() val columns by viewModel.columns.collectAsStateWithLifecycle() val showLabels by viewModel.showLabels.collectAsStateWithLifecycle() val largeTileHeight = tileHeight() val iconTileHeight = tileHeight(showLabels) - val (smallTiles, largeTiles) = tiles.partition { iconTilesSpecs.contains(it.spec) } + val (smallTiles, largeTiles) = tiles.partition { viewModel.isIconTile(it.spec) } TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) { // Large tiles @@ -103,7 +101,6 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition onAddTile: (TileSpec, Int) -> Unit, onRemoveTile: (TileSpec) -> Unit ) { - val iconOnlySpecs by viewModel.iconTilesSpecs.collectAsStateWithLifecycle() val columns by viewModel.columns.collectAsStateWithLifecycle() val showLabels by viewModel.showLabels.collectAsStateWithLifecycle() @@ -111,8 +108,6 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState { onAddTile(it, CurrentTilesInteractor.POSITION_AT_END) } - val isIconOnly: (TileSpec) -> Boolean = - remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } } val largeTileHeight = tileHeight() val iconTileHeight = tileHeight(showLabels) val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical) @@ -151,7 +146,7 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition iconTileHeight = iconTileHeight, tilePadding = tilePadding, onRemoveTile = onRemoveTile, - isIconOnly = isIconOnly, + isIconOnly = viewModel::isIconTile, columns = columns, showLabels = showLabels, ) @@ -161,7 +156,7 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition iconTileHeight = iconTileHeight, tilePadding = tilePadding, addTileToEnd = addTileToEnd, - isIconOnly = isIconOnly, + isIconOnly = viewModel::isIconTile, showLabels = showLabels, columns = columns, ) @@ -232,7 +227,7 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition val largeGridHeight = gridHeight(largeTiles.size, largeTileHeight, columns / 2, tilePadding) val smallGridHeight = gridHeight(smallTiles.size, iconTileHeight, columns, tilePadding) val largeGridHeightCustom = - gridHeight(tilesCustom.size, largeTileHeight, columns / 2, tilePadding) + gridHeight(tilesCustom.size, iconTileHeight, columns, tilePadding) // Add up the height of all three grids and add padding in between val gridHeight = @@ -257,8 +252,14 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition ) fillUpRow(nTiles = smallTiles.size, columns = columns) - // Custom tiles, all large - editTiles(tilesCustom, ClickAction.ADD, addTileToEnd, isIconOnly) + // Custom tiles, all icons + editTiles( + tilesCustom, + ClickAction.ADD, + addTileToEnd, + isIconOnly, + showLabels = showLabels + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt index 70d629fa7c70..7f4e0a7047b8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt @@ -60,14 +60,13 @@ constructor( // Icon [3 | 4] // Large [6 | 8] val columns = 12 - val iconTilesSpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle() val stretchedTiles = remember(tiles) { val sizedTiles = tiles.map { SizedTile( it, - if (iconTilesSpecs.contains(it.spec)) { + if (iconTilesViewModel.isIconTile(it.spec)) { 3 } else { 6 @@ -81,7 +80,7 @@ constructor( items(stretchedTiles.size, span = { GridItemSpan(stretchedTiles[it].width) }) { index -> Tile( tile = stretchedTiles[index].tile, - iconOnly = iconTilesSpecs.contains(stretchedTiles[index].tile.spec), + iconOnly = iconTilesViewModel.isIconTile(stretchedTiles[index].tile.spec), modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)) ) } @@ -95,12 +94,11 @@ constructor( onAddTile: (TileSpec, Int) -> Unit, onRemoveTile: (TileSpec) -> Unit ) { - val iconOnlySpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle() val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle() DefaultEditTileGrid( tiles = tiles, - iconOnlySpecs = iconOnlySpecs, + isIconOnly = iconTilesViewModel::isIconTile, columns = GridCells.Fixed(columns), modifier = modifier, onAddTile = onAddTile, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt index a6838c0c06a2..f776bf08c9e4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt @@ -165,7 +165,7 @@ fun TileLazyGrid( @Composable fun DefaultEditTileGrid( tiles: List<EditTileViewModel>, - iconOnlySpecs: Set<TileSpec>, + isIconOnly: (TileSpec) -> Boolean, columns: GridCells, modifier: Modifier, onAddTile: (TileSpec, Int) -> Unit, @@ -176,8 +176,6 @@ fun DefaultEditTileGrid( val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState { onAddTile(it, CurrentTilesInteractor.POSITION_AT_END) } - val isIconOnly: (TileSpec) -> Boolean = - remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } } TileLazyGrid(modifier = modifier, columns = columns) { // These Text are just placeholders to see the different sections. Not final UI. diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt index 9ad00c8d3cfa..117c85c9c3ba 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt @@ -20,14 +20,13 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject -import kotlinx.coroutines.flow.StateFlow interface IconTilesViewModel { - val iconTilesSpecs: StateFlow<Set<TileSpec>> + fun isIconTile(spec: TileSpec): Boolean } @SysUISingleton -class IconTilesViewModelImpl @Inject constructor(interactor: IconTilesInteractor) : +class IconTilesViewModelImpl @Inject constructor(private val interactor: IconTilesInteractor) : IconTilesViewModel { - override val iconTilesSpecs = interactor.iconTilesSpecs + override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt index 214e9f097642..24b80b8b069a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt @@ -158,6 +158,9 @@ constructor( override suspend fun prependDefault( userId: Int, ) { + if (retailModeRepository.inRetailMode) { + return + } userTileRepositories.get(userId)?.prependDefault() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt index 8ad5cb2c0a34..aca8733bff7b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt @@ -60,15 +60,21 @@ constructor( _tiles = changeEvents .scan(loadTilesFromSettingsAndParse(userId)) { current, change -> - change.apply(current).also { - if (current != it) { - if (change is RestoreTiles) { - logger.logTilesRestoredAndReconciled(current, it, userId) - } else { - logger.logProcessTileChange(change, it, userId) + change + .apply(current) + .also { + if (current != it) { + if (change is RestoreTiles) { + logger.logTilesRestoredAndReconciled(current, it, userId) + } else { + logger.logProcessTileChange(change, it, userId) + } } } - } + // Distinct preserves the order of the elements removing later + // duplicates, + // all tiles should be different + .distinct() } .flowOn(backgroundDispatcher) .stateIn(applicationScope) diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt index b7fcef4376ea..97b5e87d7167 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -43,6 +43,7 @@ import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.qs.tiles.di.NewQSTileFactory import com.android.systemui.qs.toProto +import com.android.systemui.retail.data.repository.RetailModeRepository import com.android.systemui.settings.UserTracker import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.pairwise @@ -137,6 +138,7 @@ constructor( private val installedTilesComponentRepository: InstalledTilesComponentRepository, private val userRepository: UserRepository, private val minimumTilesRepository: MinimumTilesRepository, + private val retailModeRepository: RetailModeRepository, private val customTileStatePersister: CustomTileStatePersister, private val newQSTileFactory: Lazy<NewQSTileFactory>, private val tileFactory: QSFactory, @@ -178,6 +180,14 @@ constructor( installedTilesComponentRepository.getInstalledTilesComponents(it) } + private val minTiles: Int + get() = + if (retailModeRepository.inRetailMode) { + 1 + } else { + minimumTilesRepository.minNumberOfTiles + } + init { if (featureFlags.pipelineEnabled) { startTileCollection() @@ -273,7 +283,7 @@ constructor( newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys, newUser ) - if (newResolvedTiles.size < minimumTilesRepository.minNumberOfTiles) { + if (newResolvedTiles.size < minTiles) { // We ended up with not enough tiles (some may be not installed). // Prepend the default set of tiles launch { tileSpecRepository.prependDefault(currentUser.value) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index c6dfdd5c137b..1143c304f594 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -332,6 +332,21 @@ open class QSTileViewImpl @JvmOverloads constructor( override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { super.onLayout(changed, l, t, r, b) updateHeight() + maybeUpdateLongPressEffectDimensions() + } + + private fun maybeUpdateLongPressEffectDimensions() { + if (!isLongClickable || longPressEffect == null) return + + val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) { + heightOverride + } else { + measuredHeight + } + initialLongPressProperties?.height = actualHeight.toFloat() + initialLongPressProperties?.width = measuredWidth.toFloat() + finalLongPressProperties?.height = LONG_PRESS_EFFECT_HEIGHT_SCALE * actualHeight + finalLongPressProperties?.width = LONG_PRESS_EFFECT_WIDTH_SCALE * measuredWidth } override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 1d8b7e5b6155..bf0843b8fa4e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -20,10 +20,12 @@ import android.content.Context import android.graphics.Rect import android.os.PowerManager import android.os.SystemClock +import android.util.ArraySet import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner @@ -35,6 +37,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.Flags.glanceableHubFullscreenSwipe import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.communal.dagger.Communal @@ -52,10 +55,12 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.collectFlow +import java.util.function.Consumer import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @@ -77,11 +82,38 @@ constructor( private val communalColors: CommunalColors, private val ambientTouchComponentFactory: AmbientTouchComponent.Factory, private val communalContent: CommunalContent, - @Communal private val dataSourceDelegator: SceneDataSourceDelegator + @Communal private val dataSourceDelegator: SceneDataSourceDelegator, + private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController, ) : LifecycleOwner { + + private class CommunalWrapper(context: Context) : FrameLayout(context) { + private val consumers: MutableSet<Consumer<Boolean>> = ArraySet() + + override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { + consumers.forEach { it.accept(disallowIntercept) } + super.requestDisallowInterceptTouchEvent(disallowIntercept) + } + + fun dispatchTouchEvent( + ev: MotionEvent?, + disallowInterceptConsumer: Consumer<Boolean>? + ): Boolean { + disallowInterceptConsumer?.apply { consumers.add(this) } + + try { + return super.dispatchTouchEvent(ev) + } finally { + consumers.clear() + } + } + } + /** The container view for the hub. This will not be initialized until [initView] is called. */ private var communalContainerView: View? = null + /** Wrapper around the communal container to intercept touch events */ + private var communalContainerWrapper: CommunalWrapper? = null + /** * This lifecycle is used to control when the [touchMonitor] listens to touches. The lifecycle * should only be [Lifecycle.State.RESUMED] when the hub is showing and not covered by anything, @@ -271,9 +303,13 @@ constructor( ) collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it }) - communalContainerView = containerView - - return containerView + if (glanceableHubFullscreenSwipe()) { + communalContainerWrapper = CommunalWrapper(containerView.context) + communalContainerWrapper?.addView(communalContainerView) + return communalContainerWrapper!! + } else { + return containerView + } } /** @@ -306,6 +342,11 @@ constructor( lifecycleRegistry.currentState = Lifecycle.State.CREATED communalContainerView = null } + + communalContainerWrapper?.let { + (it.parent as ViewGroup).removeView(it) + communalContainerWrapper = null + } } /** @@ -319,6 +360,18 @@ constructor( */ fun onTouchEvent(ev: MotionEvent): Boolean { SceneContainerFlag.assertInLegacyMode() + + // In the case that we are handling full swipes on the lockscreen, are on the lockscreen, + // and the touch is within the horizontal notification band on the screen, do not process + // the touch. + if ( + glanceableHubFullscreenSwipe() && + !hubShowing && + !notificationStackScrollLayoutController.isBelowLastNotification(ev.x, ev.y) + ) { + return false + } + return communalContainerView?.let { handleTouchEventOnCommunalView(it, ev) } ?: false } @@ -330,12 +383,16 @@ constructor( val hubOccluded = anyBouncerShowing || shadeShowing if (isDown && !hubOccluded) { - val x = ev.rawX - val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth - if (inOpeningSwipeRegion || hubShowing) { - // Steal touch events when the hub is open, or if the touch started in the opening - // gesture region. + if (glanceableHubFullscreenSwipe()) { isTrackingHubTouch = true + } else { + val x = ev.rawX + val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth + if (inOpeningSwipeRegion || hubShowing) { + // Steal touch events when the hub is open, or if the touch started in the + // opening gesture region. + isTrackingHubTouch = true + } } } @@ -343,10 +400,7 @@ constructor( if (isUp || isCancel) { isTrackingHubTouch = false } - dispatchTouchEvent(view, ev) - // Return true regardless of dispatch result as some touches at the start of a gesture - // may return false from dispatchTouchEvent. - return true + return dispatchTouchEvent(view, ev) } return false @@ -356,13 +410,30 @@ constructor( * Dispatches the touch event to the communal container and sends a user activity event to reset * the screen timeout. */ - private fun dispatchTouchEvent(view: View, ev: MotionEvent) { - view.dispatchTouchEvent(ev) - powerManager.userActivity( - SystemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_TOUCH, - 0 - ) + private fun dispatchTouchEvent(view: View, ev: MotionEvent): Boolean { + try { + var handled = false + if (glanceableHubFullscreenSwipe()) { + communalContainerWrapper?.dispatchTouchEvent(ev) { + if (it) { + handled = true + } + } + return handled || hubShowing + } else { + view.dispatchTouchEvent(ev) + // Return true regardless of dispatch result as some touches at the start of a + // gesture + // may return false from dispatchTouchEvent. + return true + } + } finally { + powerManager.userActivity( + SystemClock.uptimeMillis(), + PowerManager.USER_ACTIVITY_EVENT_TOUCH, + 0 + ) + } } override val lifecycle: Lifecycle diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 6df8ac4c2145..4f6a64f043d2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -65,6 +65,7 @@ import com.android.internal.policy.SystemBarUtils; import com.android.systemui.DejankUtils; import com.android.systemui.Dumpable; import com.android.systemui.classifier.Classifier; +import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.dump.DumpManager; @@ -157,6 +158,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum private final ShadeRepository mShadeRepository; private final ShadeInteractor mShadeInteractor; private final ActiveNotificationsInteractor mActiveNotificationsInteractor; + private final Lazy<CommunalTransitionViewModel> mCommunalTransitionViewModelLazy; private final JavaAdapter mJavaAdapter; private final FalsingManager mFalsingManager; private final AccessibilityManager mAccessibilityManager; @@ -334,6 +336,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum JavaAdapter javaAdapter, CastController castController, SplitShadeStateController splitShadeStateController, + Lazy<CommunalTransitionViewModel> communalTransitionViewModelLazy, Lazy<LargeScreenHeaderHelper> largeScreenHeaderHelperLazy ) { SceneContainerFlag.assertInLegacyMode(); @@ -379,6 +382,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mShadeRepository = shadeRepository; mShadeInteractor = shadeInteractor; mActiveNotificationsInteractor = activeNotificationsInteractor; + mCommunalTransitionViewModelLazy = communalTransitionViewModelLazy; mJavaAdapter = javaAdapter; mLockscreenShadeTransitionController.addCallback(new LockscreenShadeTransitionCallback()); @@ -458,6 +462,9 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum initNotificationStackScrollLayoutController(); mJavaAdapter.alwaysCollectFlow( mShadeInteractor.isExpandToQsEnabled(), this::setExpansionEnabledPolicy); + mJavaAdapter.alwaysCollectFlow( + mCommunalTransitionViewModelLazy.get().isUmoOnCommunal(), + this::setShouldUpdateSquishinessOnMedia); } private void initNotificationStackScrollLayoutController() { @@ -892,6 +899,12 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum } } + private void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) { + if (mQs != null) { + mQs.setShouldUpdateSquishinessOnMedia(shouldUpdate); + } + } + void setOverScrollAmount(int overExpansion) { if (mQs != null) { mQs.setOverScrollAmount(overExpansion); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java index d00916a1c1a8..c742f6413022 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java @@ -610,7 +610,7 @@ public final class KeyboardShortcuts { keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo( mContext.getString(R.string.keyboard_shortcut_group_applications_calendar), calendarIcon, - KeyEvent.KEYCODE_L, + KeyEvent.KEYCODE_K, KeyEvent.META_META_ON)); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index 3cf61e211e42..8d3f7284e359 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -362,20 +362,34 @@ public class NotificationGroupingUtil { } protected boolean hasSameIcon(Object parentData, Object childData) { - Icon parentIcon = ((Notification) parentData).getSmallIcon(); - Icon childIcon = ((Notification) childData).getSmallIcon(); + Icon parentIcon = getIcon((Notification) parentData); + Icon childIcon = getIcon((Notification) childData); return parentIcon.sameAs(childIcon); } + private static Icon getIcon(Notification notification) { + if (notification.shouldUseAppIcon()) { + return notification.getAppIcon(); + } + return notification.getSmallIcon(); + } + /** * @return whether two ImageViews have the same colorFilterSet or none at all */ protected boolean hasSameColor(Object parentData, Object childData) { - int parentColor = ((Notification) parentData).color; - int childColor = ((Notification) childData).color; + int parentColor = getColor((Notification) parentData); + int childColor = getColor((Notification) childData); return parentColor == childColor; } + private static int getColor(Notification notification) { + if (notification.shouldUseAppIcon()) { + return 0; // the color filter isn't applied if using the app icon + } + return notification.color; + } + @Override public boolean isEmpty(View view) { return false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 03c667034609..6d34a0fa9c24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar; import static com.android.systemui.Flags.mediaControlsUserInitiatedDeleteintent; +import static com.android.systemui.Flags.notificationMediaManagerBackgroundExecution; import android.annotation.NonNull; import android.annotation.Nullable; @@ -26,12 +27,16 @@ import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.PlaybackState; +import android.os.Handler; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.systemui.Dumpable; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; import com.android.systemui.media.controls.shared.model.MediaData; @@ -48,6 +53,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Executor; @@ -80,13 +86,16 @@ public class NotificationMediaManager implements Dumpable { private final ArrayList<MediaListener> mMediaListeners; private final Executor mBackgroundExecutor; + private final Handler mHandler; protected NotificationPresenter mPresenter; - private MediaController mMediaController; + @VisibleForTesting + MediaController mMediaController; private String mMediaNotificationKey; private MediaMetadata mMediaMetadata; - private final MediaController.Callback mMediaListener = new MediaController.Callback() { + @VisibleForTesting + final MediaController.Callback mMediaListener = new MediaController.Callback() { @Override public void onPlaybackStateChanged(PlaybackState state) { super.onPlaybackStateChanged(state); @@ -107,11 +116,20 @@ public class NotificationMediaManager implements Dumpable { if (DEBUG_MEDIA) { Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata); } - mMediaMetadata = metadata; + if (notificationMediaManagerBackgroundExecution()) { + mBackgroundExecutor.execute(() -> setMediaMetadata(metadata)); + } else { + setMediaMetadata(metadata); + } + dispatchUpdateMediaMetaData(); } }; + private void setMediaMetadata(MediaMetadata metadata) { + mMediaMetadata = metadata; + } + /** * Injected constructor. See {@link CentralSurfacesModule}. */ @@ -122,7 +140,9 @@ public class NotificationMediaManager implements Dumpable { NotifCollection notifCollection, MediaDataManager mediaDataManager, DumpManager dumpManager, - @Background Executor backgroundExecutor) { + @Background Executor backgroundExecutor, + @Main Handler handler + ) { mContext = context; mMediaListeners = new ArrayList<>(); mVisibilityProvider = visibilityProvider; @@ -130,6 +150,7 @@ public class NotificationMediaManager implements Dumpable { mNotifPipeline = notifPipeline; mNotifCollection = notifCollection; mBackgroundExecutor = backgroundExecutor; + mHandler = handler; setupNotifPipeline(); @@ -262,6 +283,14 @@ public class NotificationMediaManager implements Dumpable { public void addCallback(MediaListener callback) { mMediaListeners.add(callback); + if (notificationMediaManagerBackgroundExecution()) { + mBackgroundExecutor.execute(() -> updateMediaMetaData(callback)); + } else { + updateMediaMetaData(callback); + } + } + + private void updateMediaMetaData(MediaListener callback) { callback.onPrimaryMetadataOrStateChanged(mMediaMetadata, getMediaControllerPlaybackState(mMediaController)); } @@ -273,7 +302,11 @@ public class NotificationMediaManager implements Dumpable { public void findAndUpdateMediaNotifications() { // TODO(b/169655907): get the semi-filtered notifications for current user Collection<NotificationEntry> allNotifications = mNotifPipeline.getAllNotifs(); - findPlayingMediaNotification(allNotifications); + if (notificationMediaManagerBackgroundExecution()) { + mBackgroundExecutor.execute(() -> findPlayingMediaNotification(allNotifications)); + } else { + findPlayingMediaNotification(allNotifications); + } dispatchUpdateMediaMetaData(); } @@ -312,7 +345,7 @@ public class NotificationMediaManager implements Dumpable { // We have a new media session clearCurrentMediaNotificationSession(); mMediaController = controller; - mMediaController.registerCallback(mMediaListener); + mMediaController.registerCallback(mMediaListener, mHandler); mMediaMetadata = mMediaController.getMetadata(); if (DEBUG_MEDIA) { Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: " @@ -331,13 +364,29 @@ public class NotificationMediaManager implements Dumpable { } public void clearCurrentMediaNotification() { + if (notificationMediaManagerBackgroundExecution()) { + mBackgroundExecutor.execute(this::clearMediaNotification); + } else { + clearMediaNotification(); + } + } + + private void clearMediaNotification() { mMediaNotificationKey = null; clearCurrentMediaNotificationSession(); } private void dispatchUpdateMediaMetaData() { - @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController); ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners); + if (notificationMediaManagerBackgroundExecution()) { + mBackgroundExecutor.execute(() -> updateMediaMetaData(callbacks)); + } else { + updateMediaMetaData(callbacks); + } + } + + private void updateMediaMetaData(List<MediaListener> callbacks) { + @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController); for (int i = 0; i < callbacks.size(); i++) { callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state); } @@ -393,7 +442,6 @@ public class NotificationMediaManager implements Dumpable { Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: " + mMediaController.getPackageName()); } - // TODO(b/336612071): move to background thread mMediaController.unregisterCallback(mMediaListener); } mMediaController = null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt new file mode 100644 index 000000000000..ac16d26e415c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor + +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor +import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * Interactor for media-projection-related chips in the status bar. + * + * There are two kinds of media projection events that will show chips in the status bar: + * 1) Share-to-app: Sharing your phone screen content to another app on the same device. (Triggered + * from within each individual app.) + * 2) Cast-to-other-device: Sharing your phone screen content to a different device. (Triggered from + * the Quick Settings Cast tile or from the Settings app.) This interactor handles both of those + * event types (though maybe not audio-only casting -- see b/342169876). + */ +@SysUISingleton +class MediaProjectionChipInteractor +@Inject +constructor( + @Application scope: CoroutineScope, + mediaProjectionRepository: MediaProjectionRepository, + val systemClock: SystemClock, +) : OngoingActivityChipInteractor { + override val chip: StateFlow<OngoingActivityChipModel> = + mediaProjectionRepository.mediaProjectionState + .map { state -> + when (state) { + is MediaProjectionState.NotProjecting -> OngoingActivityChipModel.Hidden + is MediaProjectionState.EntireScreen, + is MediaProjectionState.SingleTask -> { + // TODO(b/332662551): Distinguish between cast-to-other-device and + // share-to-app. + OngoingActivityChipModel.Shown( + icon = + Icon.Resource( + R.drawable.ic_cast_connected, + ContentDescription.Resource(R.string.accessibility_casting) + ), + // TODO(b/332662551): See if we can use a MediaProjection API to fetch + // this time. + startTimeMs = systemClock.elapsedRealtime() + ) { + // TODO(b/332662551): Implement the pause dialog. + } + } + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt index bff5686641c2..585ff5f78ff8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.stateIn /** Interactor for the screen recording chip shown in the status bar. */ @SysUISingleton -open class ScreenRecordChipInteractor +class ScreenRecordChipInteractor @Inject constructor( @Application scope: CoroutineScope, diff --git a/core/java/com/android/internal/util/NewlineNormalizer.java b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/ChipChronometerBinder.kt index 0104d1f56f83..2032ec8af78c 100644 --- a/core/java/com/android/internal/util/NewlineNormalizer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/ChipChronometerBinder.kt @@ -14,26 +14,20 @@ * limitations under the License. */ -package com.android.internal.util; +package com.android.systemui.statusbar.chips.ui.binder +import com.android.systemui.statusbar.chips.ui.view.ChipChronometer -import java.util.regex.Pattern; - -/** - * Utility class that replaces consecutive empty lines with single new line. - * @hide - */ -public class NewlineNormalizer { - - private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?"); - - // Private constructor to prevent instantiation - private NewlineNormalizer() {} - +object ChipChronometerBinder { /** - * Replaces consecutive newlines with a single newline in the input text. + * Updates the given [view] chronometer with a new start time and starts it. + * + * @param startTimeMs the time this event started, relative to + * [com.android.systemui.util.time.SystemClock.elapsedRealtime]. See + * [android.widget.Chronometer.setBase]. */ - public static String normalizeNewlines(String text) { - return MULTIPLE_NEWLINES.matcher(text).replaceAll("\n"); + fun bind(startTimeMs: Long, view: ChipChronometer) { + view.base = startTimeMs + view.start() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 208eb50487e3..edb2983720a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -20,6 +20,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -40,6 +41,7 @@ class OngoingActivityChipsViewModel constructor( @Application scope: CoroutineScope, screenRecordChipInteractor: ScreenRecordChipInteractor, + mediaProjectionChipInteractor: MediaProjectionChipInteractor, callChipInteractor: CallChipInteractor, ) { @@ -51,10 +53,19 @@ constructor( * actually displaying the chip. */ val chip: StateFlow<OngoingActivityChipModel> = - combine(screenRecordChipInteractor.chip, callChipInteractor.chip) { screenRecord, call -> + combine( + screenRecordChipInteractor.chip, + mediaProjectionChipInteractor.chip, + callChipInteractor.chip + ) { screenRecord, mediaProjection, call -> // This `when` statement shows the priority order of the chips when { + // Screen recording also activates the media projection APIs, so whenever the + // screen recording chip is active, the media projection chip would also be + // active. We want the screen-recording-specific chip shown in this case, so we + // give the screen recording chip priority. See b/296461748. screenRecord is OngoingActivityChipModel.Shown -> screenRecord + mediaProjection is OngoingActivityChipModel.Shown -> mediaProjection else -> call } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 05245898c161..7df7ef187e26 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.dagger; import static com.android.systemui.Flags.predictiveBackAnimateDialogs; import android.content.Context; +import android.os.Handler; import android.os.RemoteException; import android.service.dreams.IDreamManager; import android.util.Log; @@ -99,7 +100,8 @@ public interface CentralSurfacesDependenciesModule { NotifCollection notifCollection, MediaDataManager mediaDataManager, DumpManager dumpManager, - @Background Executor backgroundExecutor) { + @Background Executor backgroundExecutor, + @Main Handler handler) { return new NotificationMediaManager( context, visibilityProvider, @@ -107,7 +109,8 @@ public interface CentralSurfacesDependenciesModule { notifCollection, mediaDataManager, dumpManager, - backgroundExecutor); + backgroundExecutor, + handler); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java index 5bbd77ee6dd9..60d846ebacac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java @@ -23,6 +23,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.ActivityTaskManager.RootTaskInfo; import android.app.AppGlobals; @@ -271,13 +272,16 @@ public class InstantAppNotifier .addFlags(Intent.FLAG_IGNORE_EPHEMERAL) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ActivityOptions options = ActivityOptions.makeBasic() + .setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); PendingIntent pendingIntent = PendingIntent.getActivityAsUser( mContext, 0 /* requestCode */, browserIntent, PendingIntent.FLAG_IMMUTABLE /* flags */, - null, + options.toBundle(), user); ComponentName aiaComponent = null; try { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt index 319b49972bd2..16d0cc42db7f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt @@ -18,25 +18,27 @@ package com.android.systemui.statusbar.notification.icon import android.app.Notification import android.content.Context +import android.graphics.drawable.Drawable import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.contentDescForNotification import javax.inject.Inject -/** - * Testable wrapper around Context. - */ -class IconBuilder @Inject constructor( - private val context: Context -) { +/** Testable wrapper around Context. */ +class IconBuilder @Inject constructor(private val context: Context) { fun createIconView(entry: NotificationEntry): StatusBarIconView { return StatusBarIconView( - context, - "${entry.sbn.packageName}/0x${Integer.toHexString(entry.sbn.id)}", - entry.sbn) + context, + "${entry.sbn.packageName}/0x${Integer.toHexString(entry.sbn.id)}", + entry.sbn + ) } fun getIconContentDescription(n: Notification): CharSequence { return contentDescForNotification(context, n) } + + fun getAppIcon(n: Notification): Drawable { + return n.loadHeaderAppIcon(context) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index 271b0a86ca12..3df9374da914 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -20,6 +20,8 @@ import android.app.Notification import android.app.Notification.MessagingStyle import android.app.Person import android.content.pm.LauncherApps +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.os.Build import android.os.Bundle @@ -165,7 +167,7 @@ constructor( Log.wtf( TAG, "Updating using the cache is not supported when the " + - "notifications_background_conversation_icons flag is off" + "notifications_background_icons flag is off" ) } if (!usingCache || !Flags.notificationsBackgroundIcons()) { @@ -216,39 +218,85 @@ constructor( @Throws(InflationException::class) private fun getIconDescriptor(entry: NotificationEntry, redact: Boolean): StatusBarIcon { - val n = entry.sbn.notification val showPeopleAvatar = !redact && isImportantConversation(entry) + // If the descriptor is already cached, return it + getCachedIconDescriptor(entry, showPeopleAvatar)?.also { + return it + } + + val n = entry.sbn.notification + var usingMonochromeAppIcon = false + val icon: Icon? + if (showPeopleAvatar) { + icon = createPeopleAvatar(entry) + } else if (android.app.Flags.notificationsUseMonochromeAppIcon()) { + if (n.shouldUseAppIcon()) { + icon = + getMonochromeAppIcon(entry)?.also { usingMonochromeAppIcon = true } + ?: n.smallIcon + } else { + icon = n.smallIcon + } + } else { + icon = n.smallIcon + } + + if (icon == null) { + throw InflationException("No icon in notification from ${entry.sbn.packageName}") + } + + val sbi = icon.toStatusBarIcon(entry) + cacheIconDescriptor(entry, sbi, showPeopleAvatar, usingMonochromeAppIcon) + return sbi + } + + private fun getCachedIconDescriptor( + entry: NotificationEntry, + showPeopleAvatar: Boolean + ): StatusBarIcon? { val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor + val appIconDescriptor = entry.icons.appIconDescriptor val smallIconDescriptor = entry.icons.smallIconDescriptor // If cached, return corresponding cached values - if (showPeopleAvatar && peopleAvatarDescriptor != null) { - return peopleAvatarDescriptor - } else if (!showPeopleAvatar && smallIconDescriptor != null) { - return smallIconDescriptor + return when { + showPeopleAvatar && peopleAvatarDescriptor != null -> peopleAvatarDescriptor + android.app.Flags.notificationsUseMonochromeAppIcon() && appIconDescriptor != null -> + appIconDescriptor + smallIconDescriptor != null -> smallIconDescriptor + else -> null } + } - val icon = - (if (showPeopleAvatar) { - createPeopleAvatar(entry) + private fun cacheIconDescriptor( + entry: NotificationEntry, + descriptor: StatusBarIcon, + showPeopleAvatar: Boolean, + usingMonochromeAppIcon: Boolean + ) { + if (android.app.Flags.notificationsUseAppIcon() || + android.app.Flags.notificationsUseMonochromeAppIcon() + ) { + // If either of the new icon flags is enabled, we cache the icon all the time. + if (showPeopleAvatar) { + entry.icons.peopleAvatarDescriptor = descriptor + } else if (usingMonochromeAppIcon) { + // When notificationsUseMonochromeAppIcon is enabled, we use the appIconDescriptor. + entry.icons.appIconDescriptor = descriptor } else { - n.smallIcon - }) - ?: throw InflationException("No icon in notification from " + entry.sbn.packageName) - - val sbi = icon.toStatusBarIcon(entry) - - // Cache if important conversation or app icon. - if (isImportantConversation(entry) || android.app.Flags.notificationsUseAppIcon()) { + // When notificationsUseAppIcon is enabled, the app icon overrides the small icon. + // But either way, it's a good idea to cache the descriptor. + entry.icons.smallIconDescriptor = descriptor + } + } else if (isImportantConversation(entry)) { + // Old approach: cache only if important conversation. if (showPeopleAvatar) { - entry.icons.peopleAvatarDescriptor = sbi + entry.icons.peopleAvatarDescriptor = descriptor } else { - entry.icons.smallIconDescriptor = sbi + entry.icons.smallIconDescriptor = descriptor } } - - return sbi } @Throws(InflationException::class) @@ -276,6 +324,29 @@ constructor( ) } + // TODO(b/335211019): Should we merge this with the method in GroupHelper? + private fun getMonochromeAppIcon(entry: NotificationEntry): Icon? { + // TODO(b/335211019): This should be done in the background. + var monochromeIcon: Icon? = null + try { + val appIcon: Drawable = iconBuilder.getAppIcon(entry.sbn.notification) + if (appIcon is AdaptiveIconDrawable) { + if (appIcon.monochrome != null) { + monochromeIcon = + Icon.createWithResourceAdaptiveDrawable( + /* resPackage = */ entry.sbn.packageName, + /* resId = */ appIcon.sourceDrawableResId, + /* useMonochrome = */ true, + /* inset = */ -3.0f * AdaptiveIconDrawable.getExtraInsetFraction() + ) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to getAppIcon() in getMonochromeAppIcon()", e) + } + return monochromeIcon + } + private suspend fun getLauncherShortcutIconForPeopleAvatar(entry: NotificationEntry) = withContext(bgCoroutineContext) { var icon: Icon? = null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java index 442c0978fd77..d029ce722af9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java @@ -33,6 +33,7 @@ public final class IconPack { @Nullable private final StatusBarIconView mAodIcon; @Nullable private StatusBarIcon mSmallIconDescriptor; + @Nullable private StatusBarIcon mAppIconDescriptor; @Nullable private StatusBarIcon mPeopleAvatarDescriptor; private boolean mIsImportantConversation; @@ -111,6 +112,15 @@ public final class IconPack { mPeopleAvatarDescriptor = peopleAvatarDescriptor; } + @Nullable + StatusBarIcon getAppIconDescriptor() { + return mAppIconDescriptor; + } + + void setAppIconDescriptor(@Nullable StatusBarIcon appIconDescriptor) { + mAppIconDescriptor = appIconDescriptor; + } + boolean isImportantConversation() { return mIsImportantConversation; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt index c74c396741d7..c29d700396af 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt @@ -21,9 +21,9 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log +import com.android.internal.logging.UiEventLogger import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.util.time.SystemClock import javax.inject.Inject // Class to track avalanche trigger event time. @@ -33,6 +33,7 @@ class AvalancheProvider constructor( private val broadcastDispatcher: BroadcastDispatcher, private val logger: VisualInterruptionDecisionLogger, + private val uiEventLogger: UiEventLogger, ) { val TAG = "AvalancheProvider" val timeoutMs = 120000 @@ -56,6 +57,7 @@ constructor( return } Log.d(TAG, "broadcastReceiver received intent.action=" + intent.action) + uiEventLogger.log(AvalancheSuppressor.AvalancheEvent.START); startTime = System.currentTimeMillis() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 938a71fd7b82..42a5bdf0f19b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -33,6 +33,8 @@ import android.os.PowerManager import android.provider.Settings import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED import android.provider.Settings.Global.HEADS_UP_OFF +import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.UiEvent; import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.settings.UserTracker @@ -241,12 +243,12 @@ class AlertKeyguardVisibilitySuppressor( override fun shouldSuppress(entry: NotificationEntry) = keyguardNotificationVisibilityProvider.shouldHideNotification(entry) } - class AvalancheSuppressor( private val avalancheProvider: AvalancheProvider, private val systemClock: SystemClock, private val systemSettings: SystemSettings, private val packageManager: PackageManager, + private val uiEventLogger: UiEventLogger, ) : VisualInterruptionFilter( types = setOf(PEEK, PULSE), @@ -266,6 +268,44 @@ class AvalancheSuppressor( SUPPRESS } + enum class AvalancheEvent(private val id: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "An avalanche event occurred but this notification was suppressed by a " + + "non-avalanche suppressor.") + START(1802), + + @UiEvent(doc = "HUN was suppressed in avalanche.") + SUPPRESS(1803), + + @UiEvent(doc = "HUN allowed during avalanche because it is high priority.") + ALLOW_CONVERSATION_AFTER_AVALANCHE(1804), + + @UiEvent(doc = "HUN allowed during avalanche because it is a high priority conversation.") + ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME(1805), + + @UiEvent(doc = "HUN allowed during avalanche because it is a call.") + ALLOW_CALLSTYLE(1806), + + @UiEvent(doc = "HUN allowed during avalanche because it is a calendar notification.") + ALLOW_CATEGORY_REMINDER(1807), + + @UiEvent(doc = "HUN allowed during avalanche because it is a calendar notification.") + ALLOW_CATEGORY_EVENT(1808), + + @UiEvent(doc = "HUN allowed during avalanche because it has a full screen intent and " + + "the full screen intent permission is granted.") + ALLOW_FSI_WITH_PERMISSION_ON(1809), + + @UiEvent(doc = "HUN allowed during avalanche because it is colorized.") + ALLOW_COLORIZED(1810), + + @UiEvent(doc = "HUN allowed during avalanche because it is an emergency notification.") + ALLOW_EMERGENCY(1811); + + override fun getId(): Int { + return id + } + } + override fun shouldSuppress(entry: NotificationEntry): Boolean { if (!isCooldownEnabled()) { return false @@ -287,41 +327,46 @@ class AvalancheSuppressor( entry.ranking.isConversation && entry.sbn.notification.getWhen() > avalancheProvider.startTime ) { + uiEventLogger.log(AvalancheEvent.ALLOW_CONVERSATION_AFTER_AVALANCHE) return State.ALLOW_CONVERSATION_AFTER_AVALANCHE } if (entry.channel?.isImportantConversation == true) { + uiEventLogger.log(AvalancheEvent.ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME) return State.ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME } if (entry.sbn.notification.isStyle(Notification.CallStyle::class.java)) { + uiEventLogger.log(AvalancheEvent.ALLOW_CALLSTYLE) return State.ALLOW_CALLSTYLE } if (entry.sbn.notification.category == CATEGORY_REMINDER) { + uiEventLogger.log(AvalancheEvent.ALLOW_CATEGORY_REMINDER) return State.ALLOW_CATEGORY_REMINDER } if (entry.sbn.notification.category == CATEGORY_EVENT) { + uiEventLogger.log(AvalancheEvent.ALLOW_CATEGORY_EVENT) return State.ALLOW_CATEGORY_EVENT } if (entry.sbn.notification.fullScreenIntent != null) { + uiEventLogger.log(AvalancheEvent.ALLOW_FSI_WITH_PERMISSION_ON) return State.ALLOW_FSI_WITH_PERMISSION_ON } - - if (entry.sbn.notification.isColorized) { - return State.ALLOW_COLORIZED - } if (entry.sbn.notification.isColorized) { + uiEventLogger.log(AvalancheEvent.ALLOW_COLORIZED) return State.ALLOW_COLORIZED } if ( packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) == PERMISSION_GRANTED ) { + uiEventLogger.log(AvalancheEvent.ALLOW_EMERGENCY) return State.ALLOW_EMERGENCY } + uiEventLogger.log(AvalancheEvent.SUPPRESS) return State.SUPPRESS } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index 7e16cd5a693f..84f8662f5fee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -178,7 +178,8 @@ constructor( if (NotificationAvalancheSuppression.isEnabled) { addFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) avalancheProvider.register() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index bd7f766c6860..d1fabb168d90 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -191,8 +191,12 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple updateTransformedTypes(); addRemainingTransformTypes(); updateCropToPaddingForImageViews(); - Notification notification = row.getEntry().getSbn().getNotification(); - mIcon.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon()); + Notification n = row.getEntry().getSbn().getNotification(); + if (n.shouldUseAppIcon()) { + mIcon.setTag(ImageTransformState.ICON_TAG, n.getAppIcon()); + } else { + mIcon.setTag(ImageTransformState.ICON_TAG, n.getSmallIcon()); + } // We need to reset all views that are no longer transforming in case a view was previously // transformed, but now we decided to transform its container instead. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt index d6c73a9dda9e..2dccea668916 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt @@ -16,24 +16,22 @@ package com.android.systemui.statusbar.notification.shared -import com.android.systemui.Flags import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the heads-up cycling flag state. */ @Suppress("NOTHING_TO_INLINE") object NotificationHeadsUpCycling { - /** The aconfig flag name - enable this feature when FLAG_NOTIFICATION_THROTTLE_HUN is on. */ - const val FLAG_NAME = Flags.FLAG_NOTIFICATION_THROTTLE_HUN + /** The aconfig flag name */ + const val FLAG_NAME = NotificationThrottleHun.FLAG_NAME /** A token used for dependency declaration */ val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) + get() = NotificationThrottleHun.token /** Is the heads-up cycling animation enabled */ @JvmStatic inline val isEnabled - get() = Flags.notificationThrottleHun() + get() = NotificationThrottleHun.isEnabled /** Whether to animate the bottom line when transiting from a tall HUN to a short HUN */ @JvmStatic @@ -46,13 +44,12 @@ object NotificationHeadsUpCycling { * build to ensure that the refactor author catches issues in testing. */ @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + inline fun isUnexpectedlyInLegacyMode() = NotificationThrottleHun.isUnexpectedlyInLegacyMode() /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if * the flag is enabled to ensure that the refactor author catches issues in testing. */ @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) + inline fun assertInLegacyMode() = NotificationThrottleHun.assertInLegacyMode() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt index dd81d42b58ee..71f0de08ece3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt @@ -24,7 +24,7 @@ import com.android.systemui.flags.RefactorFlagUtils @Suppress("NOTHING_TO_INLINE") object NotificationThrottleHun { /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_NOTIFICATION_THROTTLE_HUN + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN /** A token used for dependency declaration */ val token: FlagToken @@ -33,7 +33,7 @@ object NotificationThrottleHun { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.notificationThrottleHun() + get() = Flags.notificationAvalancheThrottleHun() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt index db544ce59aa1..f6722a4ccff0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt @@ -38,9 +38,6 @@ class NotificationPlaceholderRepository @Inject constructor() { */ val shadeScrimBounds = MutableStateFlow<ShadeScrimBounds?>(null) - /** the y position of the top of the HUN area */ - val headsUpTop = MutableStateFlow(0f) - /** height made available to the notifications in the size-constrained mode of lock screen. */ val constrainedAvailableSpace = MutableStateFlow(0) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index afcf3ae7d5b2..8557afc6ebd3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -79,9 +79,6 @@ constructor( */ val scrolledToTop: StateFlow<Boolean> = placeholderRepository.scrolledToTop.asStateFlow() - /** The y-coordinate in px of bottom of the contents of the HUN. */ - val headsUpTop: StateFlow<Float> = placeholderRepository.headsUpTop.asStateFlow() - /** * The amount in px that the notification stack should scroll due to internal expansion. This * should only happen when a notification expansion hits the bottom of the screen, so it is @@ -125,8 +122,4 @@ constructor( fun setConstrainedAvailableSpace(height: Int) { placeholderRepository.constrainedAvailableSpace.value = height } - - fun setHeadsUpTop(headsUpTop: Float) { - placeholderRepository.headsUpTop.value = headsUpTop - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index a99fbfcc7907..e90a64a32fba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -139,8 +139,6 @@ constructor( */ val scrolledToTop: Flow<Boolean> = stackAppearanceInteractor.scrolledToTop.dumpValue("scrolledToTop") - /** The y-coordinate in px of bottom of the contents of the HUN. */ - val headsUpTop: Flow<Float> = stackAppearanceInteractor.headsUpTop.dumpValue("headsUpTop") /** Receives the amount (px) that the stack should scroll due to internal expansion. */ val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index ea33be0ea4ed..634bd7e4cd41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -60,10 +60,6 @@ constructor( interactor.setConstrainedAvailableSpace(height) } - fun onHeadsUpTopChanged(headsUpTop: Float) { - interactor.setHeadsUpTop(headsUpTop) - } - /** Sets the content alpha for the current state of the brightness mirror */ fun setAlphaForBrightnessMirror(alpha: Float) { interactor.setAlphaForBrightnessMirror(alpha) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index 97f9e066ded5..aac211ae6b46 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -39,6 +39,7 @@ import com.android.app.animation.Interpolators; import com.android.app.animation.InterpolatorsAndroidX; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Dumpable; +import com.android.systemui.Flags; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; @@ -51,7 +52,6 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.OperatorNameView; import com.android.systemui.statusbar.OperatorNameViewController; import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState; import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; @@ -135,8 +135,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger; private final OperatorNameViewController.Factory mOperatorNameViewControllerFactory; private final OngoingCallController mOngoingCallController; - // TODO(b/332662551): Use this view model to show the ongoing activity chips. - private final OngoingActivityChipsViewModel mOngoingActivityChipsViewModel; private final SystemStatusAnimationScheduler mAnimationScheduler; private final StatusBarLocationPublisher mLocationPublisher; private final NotificationIconAreaController mNotificationIconAreaController; @@ -207,6 +205,11 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private boolean mTransitionFromLockscreenToDreamStarted = false; /** + * True if there's an active ongoing activity that should be showing a chip and false otherwise. + */ + private boolean mHasOngoingActivity; + + /** * Listener that updates {@link #mWaitingForWindowStateChangeAfterCameraLaunch} when it receives * a new status bar window state. */ @@ -216,11 +219,12 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue }; private DisposableHandle mNicBindingDisposable; + private boolean mAnimationsEnabled = true; + @Inject public CollapsedStatusBarFragment( StatusBarFragmentComponent.Factory statusBarFragmentComponentFactory, OngoingCallController ongoingCallController, - OngoingActivityChipsViewModel ongoingActivityChipsViewModel, SystemStatusAnimationScheduler animationScheduler, StatusBarLocationPublisher locationPublisher, NotificationIconAreaController notificationIconAreaController, @@ -246,7 +250,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue DemoModeController demoModeController) { mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory; mOngoingCallController = ongoingCallController; - mOngoingActivityChipsViewModel = ongoingActivityChipsViewModel; mAnimationScheduler = animationScheduler; mLocationPublisher = locationPublisher; mNotificationIconAreaController = notificationIconAreaController; @@ -401,6 +404,17 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue return mBlockedIcons; } + + @VisibleForTesting + void enableAnimationsForTesting() { + mAnimationsEnabled = true; + } + + @VisibleForTesting + void disableAnimationsForTesting() { + mAnimationsEnabled = false; + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -476,7 +490,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue notificationIconArea.addView(mNotificationIconAreaInner); } - updateNotificationIconAreaAndCallChip(/* animate= */ false); + updateNotificationIconAreaAndOngoingActivityChip(/* animate= */ false); Trace.endSection(); } @@ -493,15 +507,21 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private StatusBarVisibilityChangeListener mStatusBarVisibilityChangeListener = new StatusBarVisibilityChangeListener() { - @Override - public void onStatusBarVisibilityMaybeChanged() { - updateStatusBarVisibilities(/* animate= */ true); - } + @Override + public void onStatusBarVisibilityMaybeChanged() { + updateStatusBarVisibilities(/* animate= */ true); + } - @Override - public void onTransitionFromLockscreenToDreamStarted() { - mTransitionFromLockscreenToDreamStarted = true; - } + @Override + public void onTransitionFromLockscreenToDreamStarted() { + mTransitionFromLockscreenToDreamStarted = true; + } + + @Override + public void onOngoingActivityStatusChanged(boolean hasOngoingActivity) { + mHasOngoingActivity = hasOngoingActivity; + updateStatusBarVisibilities(/* animate= */ true); + } }; @Override @@ -532,11 +552,14 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } } - // The ongoing call chip and notification icon visibilities are intertwined, so update both - // if either change. - if (newModel.getShowNotificationIcons() != previousModel.getShowNotificationIcons() - || newModel.getShowOngoingCallChip() != previousModel.getShowOngoingCallChip()) { - updateNotificationIconAreaAndCallChip(animate); + // The ongoing activity chip and notification icon visibilities are intertwined, so update + // both if either change. + boolean notifsChanged = + newModel.getShowNotificationIcons() != previousModel.getShowNotificationIcons(); + boolean ongoingActivityChanged = + newModel.getShowOngoingActivityChip() != previousModel.getShowOngoingActivityChip(); + if (notifsChanged || ongoingActivityChanged) { + updateNotificationIconAreaAndOngoingActivityChip(animate); } // The clock may have already been hidden, but we might want to shift its @@ -566,45 +589,58 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue return new StatusBarVisibilityModel( /* showClock= */ false, /* showNotificationIcons= */ false, - /* showOngoingCallChip= */ false, + /* showOngoingActivityChip= */ false, /* showSystemInfo= */ false); } boolean showClock = externalModel.getShowClock() && !headsUpVisible; - boolean showOngoingCallChip = mOngoingCallController.hasOngoingCall() && !headsUpVisible; + + boolean showOngoingActivityChip; + if (Flags.statusBarScreenSharingChips()) { + // If this flag is on, the ongoing activity status comes from + // CollapsedStatusBarViewBinder, which updates the mHasOngoingActivity variable. + showOngoingActivityChip = mHasOngoingActivity; + } else { + // If this flag is off, the only ongoing activity is the ongoing call, and we pull it + // from the controller directly. + showOngoingActivityChip = mOngoingCallController.hasOngoingCall(); + } + return new StatusBarVisibilityModel( showClock, externalModel.getShowNotificationIcons(), - showOngoingCallChip, + showOngoingActivityChip && !headsUpVisible, externalModel.getShowSystemInfo()); } /** - * Updates the visibility of the notification icon area and ongoing call chip based on disabled1 - * state. + * Updates the visibility of the notification icon area and ongoing activity chip based on + * mLastModifiedVisibility. */ - private void updateNotificationIconAreaAndCallChip(boolean animate) { + private void updateNotificationIconAreaAndOngoingActivityChip(boolean animate) { StatusBarVisibilityModel visibilityModel = mLastModifiedVisibility; boolean disableNotifications = !visibilityModel.getShowNotificationIcons(); - boolean hasOngoingCall = visibilityModel.getShowOngoingCallChip(); + boolean hasOngoingActivity = visibilityModel.getShowOngoingActivityChip(); - // Hide notifications if the disable flag is set or we have an ongoing call. - if (disableNotifications || hasOngoingCall) { - hideNotificationIconArea(animate && !hasOngoingCall); + // Hide notifications if the disable flag is set or we have an ongoing activity. + if (disableNotifications || hasOngoingActivity) { + hideNotificationIconArea(animate && !hasOngoingActivity); } else { showNotificationIconArea(animate); } - // Show the ongoing call chip only if there is an ongoing call *and* notification icons - // are allowed. (The ongoing call chip occupies the same area as the notification icons, - // so if the icons are disabled then the call chip should be, too.) - boolean showOngoingCallChip = hasOngoingCall && !disableNotifications; - if (showOngoingCallChip) { + // Show the ongoing activity chip only if there is an ongoing activity *and* notification + // icons are allowed. (The ongoing activity chip occupies the same area as the notification, + // icons so if the icons are disabled then the activity chip should be, too.) + boolean showOngoingActivityChip = hasOngoingActivity && !disableNotifications; + if (showOngoingActivityChip) { showOngoingActivityChip(animate); } else { hideOngoingActivityChip(animate); } - mOngoingCallController.notifyChipVisibilityChanged(showOngoingCallChip); + if (!Flags.statusBarScreenSharingChips()) { + mOngoingCallController.notifyChipVisibilityChanged(showOngoingActivityChip); + } } private boolean shouldHideStatusBar() { @@ -702,8 +738,9 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue /** * Displays the ongoing activity chip. * - * For now, this chip will only ever contain the ongoing call information, but after b/332662551 - * feature is implemented, it will support different kinds of ongoing activities. + * If Flags.statusBarScreenSharingChips is disabled, this chip will only ever contain the + * ongoing call information, If that flag is enabled, it will support different kinds of ongoing + * activities. See b/332662551. */ private void showOngoingActivityChip(boolean animate) { animateShow(mOngoingActivityChip, animate); @@ -746,7 +783,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue */ private void animateHiddenState(final View v, int state, boolean animate) { v.animate().cancel(); - if (!animate) { + if (!animate || !mAnimationsEnabled) { v.setAlpha(0f); v.setVisibility(state); return; @@ -773,7 +810,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private void animateShow(View v, boolean animate) { v.animate().cancel(); v.setVisibility(View.VISIBLE); - if (!animate) { + if (!animate || !mAnimationsEnabled) { v.setAlpha(1f); return; } @@ -872,6 +909,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue @Override public void dump(PrintWriter printWriter, String[] args) { IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, /* singleIndent= */" "); + pw.println("mHasOngoingActivity=" + mHasOngoingActivity); + pw.println("mAnimationsEnabled=" + mAnimationsEnabled); StatusBarFragmentComponent component = mStatusBarFragmentComponent; if (component == null) { pw.println("StatusBarFragmentComponent is null"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt index 7cdb9c0a7aa8..0a19023d9e8c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt @@ -59,13 +59,13 @@ class CollapsedStatusBarFragmentLogger @Inject constructor( { bool1 = model.showClock bool2 = model.showNotificationIcons - bool3 = model.showOngoingCallChip + bool3 = model.showOngoingActivityChip bool4 = model.showSystemInfo }, { "New visibilities calculated internally. " + "showClock=$bool1 " + "showNotificationIcons=$bool2 " + - "showOngoingCallChip=$bool3 " + + "showOngoingActivityChip=$bool3 " + "showSystemInfo=$bool4" } ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt index cf54cb7aa954..fe24faece1d3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModel.kt @@ -26,7 +26,7 @@ import android.app.StatusBarManager.DISABLE_SYSTEM_INFO data class StatusBarVisibilityModel( val showClock: Boolean, val showNotificationIcons: Boolean, - val showOngoingCallChip: Boolean, + val showOngoingActivityChip: Boolean, val showSystemInfo: Boolean, ) { companion object { @@ -48,7 +48,7 @@ data class StatusBarVisibilityModel( showNotificationIcons = (disabled1 and DISABLE_NOTIFICATION_ICONS) == 0, // TODO(b/279899176): [CollapsedStatusBarFragment] always overwrites this with the // value of [OngoingCallController]. Do we need to process the flag here? - showOngoingCallChip = (disabled1 and DISABLE_ONGOING_CALL_CHIP) == 0, + showOngoingActivityChip = (disabled1 and DISABLE_ONGOING_CALL_CHIP) == 0, showSystemInfo = (disabled1 and DISABLE_SYSTEM_INFO) == 0 && (disabled2 and DISABLE2_SYSTEM_ICONS) == 0 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt index 7d7f49bb8d17..a2ec1f21a35c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt @@ -19,11 +19,17 @@ package com.android.systemui.statusbar.pipeline.shared.ui.binder import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.view.View +import android.widget.ImageView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.Flags +import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.binder.ChipChronometerBinder +import com.android.systemui.statusbar.chips.ui.view.ChipChronometer import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel import javax.inject.Inject @@ -75,6 +81,35 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa } } } + + if (Flags.statusBarScreenSharingChips()) { + val chipView: View = view.requireViewById(R.id.ongoing_activity_chip) + val chipIconView: ImageView = + chipView.requireViewById(R.id.ongoing_activity_chip_icon) + val chipTimeView: ChipChronometer = + chipView.requireViewById(R.id.ongoing_activity_chip_time) + launch { + viewModel.ongoingActivityChip.collect { chipModel -> + when (chipModel) { + is OngoingActivityChipModel.Shown -> { + IconViewBinder.bind(chipModel.icon, chipIconView) + ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView) + // TODO(b/332662551): Attach click listener to chip + + listener.onOngoingActivityStatusChanged( + hasOngoingActivity = true + ) + } + is OngoingActivityChipModel.Hidden -> { + chipTimeView.stop() + listener.onOngoingActivityStatusChanged( + hasOngoingActivity = false + ) + } + } + } + } + } } } } @@ -120,4 +155,7 @@ interface StatusBarVisibilityChangeListener { /** Called when a transition from lockscreen to dream has started. */ fun onTransitionFromLockscreenToDreamStarted() + + /** Called when the status of the ongoing activity chip (active or not active) has changed. */ + fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt index 0a6e95eee127..bb3a67ed49bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt @@ -24,6 +24,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteractor @@ -59,6 +61,9 @@ interface CollapsedStatusBarViewModel { /** Emits whenever a transition from lockscreen to dream has started. */ val transitionFromLockscreenToDreamStartedEvent: Flow<Unit> + /** The ongoing activity chip that should be shown on the left-hand side of the status bar. */ + val ongoingActivityChip: StateFlow<OngoingActivityChipModel> + /** * Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where * status bar and navigation icons dim. In this mode, a notification dot appears where the @@ -78,6 +83,7 @@ constructor( private val lightsOutInteractor: LightsOutInteractor, private val notificationsInteractor: ActiveNotificationsInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, + ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, @Application coroutineScope: CoroutineScope, ) : CollapsedStatusBarViewModel { override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> = @@ -91,6 +97,8 @@ constructor( .filter { it.transitionState == TransitionState.STARTED } .map {} + override val ongoingActivityChip = ongoingActivityChipsViewModel.chip + override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) { emptyFlow() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt index eb09e6ef39cb..a97298527e11 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.policy import android.util.Log import androidx.annotation.VisibleForTesting +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager @@ -35,6 +37,7 @@ class AvalancheController @Inject constructor( dumpManager: DumpManager, + private val uiEventLogger: UiEventLogger ) : Dumpable { private val tag = "AvalancheController" @@ -65,6 +68,21 @@ constructor( // For debugging only @VisibleForTesting var debugDropSet: MutableSet<HeadsUpEntry> = HashSet() + enum class ThrottleEvent(private val id: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "HUN was shown.") + SHOWN(1812), + + @UiEvent(doc = "HUN was dropped to show higher priority HUNs.") + DROPPED(1813), + + @UiEvent(doc = "HUN was removed while waiting to show.") + REMOVED(1814); + + override fun getId(): Int { + return id + } + } + init { dumpManager.registerNormalDumpable(tag, /* module */ this) } @@ -145,6 +163,7 @@ constructor( log { "$fn => remove from next" } if (entry in nextMap) nextMap.remove(entry) if (entry in nextList) nextList.remove(entry) + uiEventLogger.log(ThrottleEvent.REMOVED) } else if (entry in debugDropSet) { log { "$fn => remove from dropset" } debugDropSet.remove(entry) @@ -268,6 +287,7 @@ constructor( private fun showNow(entry: HeadsUpEntry, runnableList: MutableList<Runnable>) { log { "SHOW: " + getKey(entry) } + uiEventLogger.log(ThrottleEvent.SHOWN) headsUpEntryShowing = entry runnableList.forEach { @@ -295,6 +315,12 @@ constructor( // Remove runnable labels for dropped huns val listToDrop = nextList.subList(1, nextList.size) + + // Log dropped HUNs + for (e in listToDrop) { + uiEventLogger.log(ThrottleEvent.DROPPED) + } + if (debug) { // Clear runnable labels for (e in listToDrop) { diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java deleted file mode 100644 index 2cad8442e3ba..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.util.concurrency; - -import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME; - -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Process; - -import com.android.systemui.Flags; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.BroadcastRunning; -import com.android.systemui.dagger.qualifiers.LongRunning; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dagger.qualifiers.NotifInflation; - -import dagger.Module; -import dagger.Provides; - -import java.util.concurrent.Executor; - -import javax.inject.Named; - -/** - * Dagger Module for classes found within the concurrent package. - */ -@Module -public abstract class SysUIConcurrencyModule { - - // Slow BG executor can potentially affect UI if UI is waiting for an updated state from this - // thread - private static final Long BG_SLOW_DISPATCH_THRESHOLD = 1000L; - private static final Long BG_SLOW_DELIVERY_THRESHOLD = 1000L; - private static final Long LONG_SLOW_DISPATCH_THRESHOLD = 2500L; - private static final Long LONG_SLOW_DELIVERY_THRESHOLD = 2500L; - private static final Long BROADCAST_SLOW_DISPATCH_THRESHOLD = 1000L; - private static final Long BROADCAST_SLOW_DELIVERY_THRESHOLD = 1000L; - private static final Long NOTIFICATION_INFLATION_SLOW_DISPATCH_THRESHOLD = 1000L; - private static final Long NOTIFICATION_INFLATION_SLOW_DELIVERY_THRESHOLD = 1000L; - - /** Background Looper */ - @Provides - @SysUISingleton - @Background - public static Looper provideBgLooper() { - HandlerThread thread = new HandlerThread("SysUiBg", - Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - thread.getLooper().setSlowLogThresholdMs(BG_SLOW_DISPATCH_THRESHOLD, - BG_SLOW_DELIVERY_THRESHOLD); - return thread.getLooper(); - } - - /** BroadcastRunning Looper (for sending and receiving broadcasts) */ - @Provides - @SysUISingleton - @BroadcastRunning - public static Looper provideBroadcastRunningLooper() { - HandlerThread thread = new HandlerThread("BroadcastRunning", - Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - thread.getLooper().setSlowLogThresholdMs(BROADCAST_SLOW_DISPATCH_THRESHOLD, - BROADCAST_SLOW_DELIVERY_THRESHOLD); - return thread.getLooper(); - } - - /** Long running tasks Looper */ - @Provides - @SysUISingleton - @LongRunning - public static Looper provideLongRunningLooper() { - HandlerThread thread = new HandlerThread("SysUiLng", - Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - thread.getLooper().setSlowLogThresholdMs(LONG_SLOW_DISPATCH_THRESHOLD, - LONG_SLOW_DELIVERY_THRESHOLD); - return thread.getLooper(); - } - - /** Notification inflation Looper */ - @Provides - @SysUISingleton - @NotifInflation - public static Looper provideNotifInflationLooper(@Background Looper bgLooper) { - if (!Flags.dedicatedNotifInflationThread()) { - return bgLooper; - } - - final HandlerThread thread = new HandlerThread("NotifInflation", - Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - final Looper looper = thread.getLooper(); - looper.setSlowLogThresholdMs(NOTIFICATION_INFLATION_SLOW_DISPATCH_THRESHOLD, - NOTIFICATION_INFLATION_SLOW_DELIVERY_THRESHOLD); - return looper; - } - - /** - * Background Handler. - * - * Prefer the Background Executor when possible. - */ - @Provides - @Background - public static Handler provideBgHandler(@Background Looper bgLooper) { - return new Handler(bgLooper); - } - - /** - * Provide a BroadcastRunning Executor (for sending and receiving broadcasts). - */ - @Provides - @SysUISingleton - @BroadcastRunning - public static Executor provideBroadcastRunningExecutor(@BroadcastRunning Looper looper) { - return new ExecutorImpl(looper); - } - - /** - * Provide a Long running Executor. - */ - @Provides - @SysUISingleton - @LongRunning - public static Executor provideLongRunningExecutor(@LongRunning Looper looper) { - return new ExecutorImpl(looper); - } - - /** - * Provide a Long running Executor. - */ - @Provides - @SysUISingleton - @LongRunning - public static DelayableExecutor provideLongRunningDelayableExecutor( - @LongRunning Looper looper) { - return new ExecutorImpl(looper); - } - - /** - * Provide a Background-Thread Executor. - */ - @Provides - @SysUISingleton - @Background - public static Executor provideBackgroundExecutor(@Background Looper looper) { - return new ExecutorImpl(looper); - } - - /** - * Provide a Background-Thread Executor. - */ - @Provides - @SysUISingleton - @Background - public static DelayableExecutor provideBackgroundDelayableExecutor(@Background Looper looper) { - return new ExecutorImpl(looper); - } - - /** - * Provide a Background-Thread Executor. - */ - @Provides - @SysUISingleton - @Background - public static RepeatableExecutor provideBackgroundRepeatableExecutor( - @Background DelayableExecutor exec) { - return new RepeatableExecutorImpl(exec); - } - - /** - * Provide a Main-Thread Executor. - */ - @Provides - @SysUISingleton - @Main - public static RepeatableExecutor provideMainRepeatableExecutor(@Main DelayableExecutor exec) { - return new RepeatableExecutorImpl(exec); - } - - /** */ - @Provides - @Main - public static MessageRouter providesMainMessageRouter( - @Main DelayableExecutor executor) { - return new MessageRouterImpl(executor); - } - - /** */ - @Provides - @Background - public static MessageRouter providesBackgroundMessageRouter( - @Background DelayableExecutor executor) { - return new MessageRouterImpl(executor); - } - - /** */ - @Provides - @SysUISingleton - @Named(TIME_TICK_HANDLER_NAME) - public static Handler provideTimeTickHandler() { - HandlerThread thread = new HandlerThread("TimeTick"); - thread.start(); - return new Handler(thread.getLooper()); - } - - /** */ - @Provides - @SysUISingleton - @NotifInflation - public static Executor provideNotifInflationExecutor(@NotifInflation Looper looper) { - return new ExecutorImpl(looper); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt new file mode 100644 index 000000000000..a7abb6b5f1d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.util.concurrency + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.Process +import android.view.Choreographer +import com.android.systemui.Dependency +import com.android.systemui.Flags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.BroadcastRunning +import com.android.systemui.dagger.qualifiers.LongRunning +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dagger.qualifiers.NotifInflation +import dagger.Module +import dagger.Provides +import java.util.concurrent.Executor +import javax.inject.Named +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class BackPanelUiThread + +/** Dagger Module for classes found within the concurrent package. */ +@Module +object SysUIConcurrencyModule { + // Slow BG executor can potentially affect UI if UI is waiting for an updated state from this + // thread + private const val BG_SLOW_DISPATCH_THRESHOLD = 1000L + private const val BG_SLOW_DELIVERY_THRESHOLD = 1000L + private const val LONG_SLOW_DISPATCH_THRESHOLD = 2500L + private const val LONG_SLOW_DELIVERY_THRESHOLD = 2500L + private const val BROADCAST_SLOW_DISPATCH_THRESHOLD = 1000L + private const val BROADCAST_SLOW_DELIVERY_THRESHOLD = 1000L + private const val NOTIFICATION_INFLATION_SLOW_DISPATCH_THRESHOLD = 1000L + private const val NOTIFICATION_INFLATION_SLOW_DELIVERY_THRESHOLD = 1000L + + /** Background Looper */ + @Provides + @SysUISingleton + @Background + fun provideBgLooper(): Looper { + val thread = HandlerThread("SysUiBg", Process.THREAD_PRIORITY_BACKGROUND) + thread.start() + thread + .getLooper() + .setSlowLogThresholdMs(BG_SLOW_DISPATCH_THRESHOLD, BG_SLOW_DELIVERY_THRESHOLD) + return thread.getLooper() + } + + /** BroadcastRunning Looper (for sending and receiving broadcasts) */ + @Provides + @SysUISingleton + @BroadcastRunning + fun provideBroadcastRunningLooper(): Looper { + val thread = HandlerThread("BroadcastRunning", Process.THREAD_PRIORITY_BACKGROUND) + thread.start() + thread + .getLooper() + .setSlowLogThresholdMs( + BROADCAST_SLOW_DISPATCH_THRESHOLD, + BROADCAST_SLOW_DELIVERY_THRESHOLD + ) + return thread.getLooper() + } + + /** Long running tasks Looper */ + @Provides + @SysUISingleton + @LongRunning + fun provideLongRunningLooper(): Looper { + val thread = HandlerThread("SysUiLng", Process.THREAD_PRIORITY_BACKGROUND) + thread.start() + thread + .getLooper() + .setSlowLogThresholdMs(LONG_SLOW_DISPATCH_THRESHOLD, LONG_SLOW_DELIVERY_THRESHOLD) + return thread.getLooper() + } + + /** Notification inflation Looper */ + @Provides + @SysUISingleton + @NotifInflation + fun provideNotifInflationLooper(@Background bgLooper: Looper): Looper { + if (!Flags.dedicatedNotifInflationThread()) { + return bgLooper + } + val thread = HandlerThread("NotifInflation", Process.THREAD_PRIORITY_BACKGROUND) + thread.start() + val looper = thread.getLooper() + looper.setSlowLogThresholdMs( + NOTIFICATION_INFLATION_SLOW_DISPATCH_THRESHOLD, + NOTIFICATION_INFLATION_SLOW_DELIVERY_THRESHOLD + ) + return looper + } + + @Provides + @SysUISingleton + @BackPanelUiThread + fun provideBackPanelUiThreadContext( + @Main mainLooper: Looper, + @Main mainHandler: Handler, + @Main mainExecutor: Executor + ): UiThreadContext { + return if (Flags.edgeBackGestureHandlerThread()) { + val thread = + HandlerThread("BackPanelUiThread", Process.THREAD_PRIORITY_DISPLAY).apply { + start() + looper.setSlowLogThresholdMs( + LONG_SLOW_DISPATCH_THRESHOLD, + LONG_SLOW_DELIVERY_THRESHOLD + ) + } + UiThreadContext( + thread.looper, + thread.threadHandler, + thread.threadExecutor, + thread.threadHandler.runWithScissors { Choreographer.getInstance() } + ) + } else { + UiThreadContext( + mainLooper, + mainHandler, + mainExecutor, + mainHandler.runWithScissors { Choreographer.getInstance() } + ) + } + } + + /** + * Background Handler. + * + * Prefer the Background Executor when possible. + */ + @Provides + @Background + fun provideBgHandler(@Background bgLooper: Looper): Handler = Handler(bgLooper) + + /** Provide a BroadcastRunning Executor (for sending and receiving broadcasts). */ + @Provides + @SysUISingleton + @BroadcastRunning + fun provideBroadcastRunningExecutor(@BroadcastRunning looper: Looper): Executor = + ExecutorImpl(looper) + + /** Provide a Long running Executor. */ + @Provides + @SysUISingleton + @LongRunning + fun provideLongRunningExecutor(@LongRunning looper: Looper): Executor = ExecutorImpl(looper) + + /** Provide a Long running Executor. */ + @Provides + @SysUISingleton + @LongRunning + fun provideLongRunningDelayableExecutor(@LongRunning looper: Looper): DelayableExecutor = + ExecutorImpl(looper) + + /** Provide a Background-Thread Executor. */ + @Provides + @SysUISingleton + @Background + fun provideBackgroundExecutor(@Background looper: Looper): Executor = ExecutorImpl(looper) + + /** Provide a Background-Thread Executor. */ + @Provides + @SysUISingleton + @Background + fun provideBackgroundDelayableExecutor(@Background looper: Looper): DelayableExecutor = + ExecutorImpl(looper) + + /** Provide a Background-Thread Executor. */ + @Provides + @SysUISingleton + @Background + fun provideBackgroundRepeatableExecutor( + @Background exec: DelayableExecutor + ): RepeatableExecutor = RepeatableExecutorImpl(exec) + + /** Provide a Main-Thread Executor. */ + @Provides + @SysUISingleton + @Main + fun provideMainRepeatableExecutor(@Main exec: DelayableExecutor): RepeatableExecutor = + RepeatableExecutorImpl(exec) + + /** */ + @Provides + @Main + fun providesMainMessageRouter(@Main executor: DelayableExecutor): MessageRouter = + MessageRouterImpl(executor) + + /** */ + @Provides + @Background + fun providesBackgroundMessageRouter(@Background executor: DelayableExecutor): MessageRouter = + MessageRouterImpl(executor) + + /** */ + @Provides + @SysUISingleton + @Named(Dependency.TIME_TICK_HANDLER_NAME) + fun provideTimeTickHandler(): Handler { + val thread = HandlerThread("TimeTick") + thread.start() + return Handler(thread.getLooper()) + } + + /** */ + @Provides + @SysUISingleton + @NotifInflation + fun provideNotifInflationExecutor(@NotifInflation looper: Looper): Executor = + ExecutorImpl(looper) +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt new file mode 100644 index 000000000000..8c8c686dddcc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency + +import android.os.Handler +import android.os.Looper +import android.view.Choreographer +import com.android.systemui.util.Assert +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicReference + +private const val DEFAULT_TIMEOUT = 150L + +class UiThreadContext( + val looper: Looper, + val handler: Handler, + val executor: Executor, + val choreographer: Choreographer +) { + fun isCurrentThread() { + Assert.isCurrentThread(looper) + } + + fun <T> runWithScissors(block: () -> T): T { + return handler.runWithScissors(block) + } + + fun runWithScissors(block: Runnable) { + handler.runWithScissors(block, DEFAULT_TIMEOUT) + } +} + +fun <T> Handler.runWithScissors(block: () -> T): T { + val returnedValue = AtomicReference<T>() + runWithScissors({ returnedValue.set(block()) }, DEFAULT_TIMEOUT) + return returnedValue.get()!! +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt index 1ec86a4d1dfc..8c46f9af04e5 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -88,8 +88,8 @@ fun <T> collectFlow( flow: Flow<T>, consumer: Consumer<T>, state: Lifecycle.State = Lifecycle.State.CREATED, -) { - lifecycle.coroutineScope.launch { +): Job { + return lifecycle.coroutineScope.launch { lifecycle.repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt index b6246da4a7dd..e0f64b4f7dc2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt @@ -18,8 +18,6 @@ package com.android.systemui.volume.domain.interactor import android.bluetooth.BluetoothAdapter import android.media.AudioDeviceInfo -import android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES -import android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.media.BluetoothMediaDevice @@ -30,7 +28,6 @@ import com.android.settingslib.volume.data.repository.AudioSharingRepository import com.android.settingslib.volume.domain.interactor.AudioModeInteractor import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.volume.domain.model.AudioOutputDevice -import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import javax.inject.Inject @@ -58,8 +55,7 @@ constructor( private val bluetoothAdapter: BluetoothAdapter?, private val deviceIconInteractor: DeviceIconInteractor, private val mediaOutputInteractor: MediaOutputInteractor, - private val localMediaRepositoryFactory: LocalMediaRepositoryFactory, - private val audioSharingRepository: AudioSharingRepository, + audioSharingRepository: AudioSharingRepository, ) { val currentAudioDevice: Flow<AudioOutputDevice> = @@ -83,30 +79,32 @@ constructor( val isInAudioSharing: Flow<Boolean> = audioSharingRepository.inAudioSharing private fun AudioDeviceInfo.toAudioOutputDevice(): AudioOutputDevice { - if (type == TYPE_WIRED_HEADPHONES || type == TYPE_WIRED_HEADSET) { - return AudioOutputDevice.Wired( - name = productName.toString(), - icon = deviceIconInteractor.loadIcon(type), - ) - } - val cachedBluetoothDevice: CachedBluetoothDevice? = - if (address.isEmpty() || localBluetoothManager == null || bluetoothAdapter == null) { - null - } else { - val remoteDevice = bluetoothAdapter.getRemoteDevice(address) - localBluetoothManager.cachedDeviceManager.findDevice(remoteDevice) + if ( + BluetoothAdapter.checkBluetoothAddress(address) && + localBluetoothManager != null && + bluetoothAdapter != null + ) { + val remoteDevice = bluetoothAdapter.getRemoteDevice(address) + localBluetoothManager.cachedDeviceManager.findDevice(remoteDevice)?.let { + device: CachedBluetoothDevice -> + return AudioOutputDevice.Bluetooth( + name = device.name, + icon = deviceIconInteractor.loadIcon(device), + cachedBluetoothDevice = device, + ) } - return cachedBluetoothDevice?.let { - AudioOutputDevice.Bluetooth( - name = it.name, - icon = deviceIconInteractor.loadIcon(it), - cachedBluetoothDevice = it, - ) } - ?: AudioOutputDevice.BuiltIn( + // Built-in device has an empty address + if (address.isNotEmpty()) { + return AudioOutputDevice.Wired( name = productName.toString(), icon = deviceIconInteractor.loadIcon(type), ) + } + return AudioOutputDevice.BuiltIn( + name = productName.toString(), + icon = deviceIconInteractor.loadIcon(type), + ) } private fun MediaDevice.toAudioOutputDevice(): AudioOutputDevice { @@ -117,7 +115,8 @@ constructor( icon = icon, cachedBluetoothDevice = cachedDevice, ) - deviceType == MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE -> + deviceType == MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE || + deviceType == MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE -> AudioOutputDevice.Wired( name = name, icon = icon, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt index 0dc264781070..79a4ae74b217 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt @@ -19,14 +19,13 @@ import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.media.controls.util.LocalMediaManagerFactory import javax.inject.Inject import kotlinx.coroutines.CoroutineScope interface LocalMediaRepositoryFactory { - fun create(packageName: String?): LocalMediaRepository + fun create(packageName: String?, coroutineScope: CoroutineScope): LocalMediaRepository } @SysUISingleton @@ -35,10 +34,12 @@ class LocalMediaRepositoryFactoryImpl constructor( private val eventsReceiver: AudioManagerEventsReceiver, private val localMediaManagerFactory: LocalMediaManagerFactory, - @Application private val coroutineScope: CoroutineScope, ) : LocalMediaRepositoryFactory { - override fun create(packageName: String?): LocalMediaRepository = + override fun create( + packageName: String?, + coroutineScope: CoroutineScope + ): LocalMediaRepository = LocalMediaRepositoryImpl( eventsReceiver, localMediaManagerFactory.create(packageName), diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt index 9fbd79accf80..31a89775e916 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -35,6 +35,7 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -46,6 +47,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.withContext /** Provides observable models about the current media session state. */ @@ -105,12 +107,9 @@ constructor( .filterData() .map { it?.packageName } .distinctUntilChanged() - .map { localMediaRepositoryFactory.create(it) } - .stateIn( - coroutineScope, - SharingStarted.Eagerly, - localMediaRepositoryFactory.create(null) - ) + .transformLatest { + coroutineScope { emit(localMediaRepositoryFactory.create(it, this)) } + } /** Currently connected [MediaDevice]. */ val currentConnectedDevice: Flow<MediaDevice?> = diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index fd01b4864772..850162e65aa6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -146,14 +146,18 @@ constructor( isEnabled = isEnabled, a11yStep = volumeRange.step, a11yClickDescription = - context.getString( - if (isMuted) { - R.string.volume_panel_hint_unmute - } else { - R.string.volume_panel_hint_mute - }, - label, - ), + if (isAffectedByMute) { + context.getString( + if (isMuted) { + R.string.volume_panel_hint_unmute + } else { + R.string.volume_panel_hint_mute + }, + label, + ) + } else { + null + }, a11yStateDescription = if (volume == volumeRange.first) { context.getString( diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt index 99f956489bc3..3da725b9a51f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt @@ -98,12 +98,12 @@ constructor( private fun showNewVolumePanel() { activityStarter.dismissKeyguardThenExecute( - { + /* action = */ { volumePanelGlobalStateInteractor.setVisible(true) false }, - {}, - true + /* cancel = */ {}, + /* afterKeyguardGone = */ true, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java index d01d57e18223..358e8cbd4a3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java @@ -22,6 +22,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -45,6 +47,7 @@ import android.view.WindowMetrics; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; import androidx.test.filters.SmallTest; @@ -67,6 +70,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -119,6 +123,8 @@ public class TouchMonitorTest extends SysuiTestCase { private final KosmosJavaAdapter mKosmos; + private ArrayList<LifecycleObserver> mLifecycleObservers = new ArrayList<>(); + Environment(Set<TouchHandler> handlers, KosmosJavaAdapter kosmos) { mLifecycleOwner = new SimpleLifecycleOwner(); @@ -147,8 +153,14 @@ public class TouchMonitorTest extends SysuiTestCase { mMonitor = new TouchMonitor(mExecutor, mBackgroundExecutor, mLifecycleRegistry, mInputFactory, mDisplayHelper, mKosmos.getConfigurationInteractor(), handlers, mIWindowManager, 0); + clearInvocations(mLifecycleRegistry); mMonitor.init(); + ArgumentCaptor<LifecycleObserver> observerCaptor = + ArgumentCaptor.forClass(LifecycleObserver.class); + verify(mLifecycleRegistry, atLeast(1)).addObserver(observerCaptor.capture()); + mLifecycleObservers.addAll(observerCaptor.getAllValues()); + updateLifecycle(Lifecycle.State.RESUMED); // Capture creation request. @@ -187,6 +199,16 @@ public class TouchMonitorTest extends SysuiTestCase { verify(mInputSession).dispose(); Mockito.clearInvocations(mInputSession); } + + void destroyMonitor() { + mMonitor.destroy(); + } + + void verifyLifecycleObserversUnregistered() { + for (LifecycleObserver observer : mLifecycleObservers) { + verify(mLifecycleRegistry).removeObserver(observer); + } + } } @Test @@ -642,6 +664,16 @@ public class TouchMonitorTest extends SysuiTestCase { verify(callback).onRemoved(); } + @Test + public void testDestroy_cleansUpLifecycleObserver() { + final TouchHandler touchHandler = createTouchHandler(); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new)), mKosmos); + environment.destroyMonitor(); + environment.verifyLifecycleObserversUnregistered(); + } + private GestureDetector.OnGestureListener registerGestureListener(TouchHandler handler) { final GestureDetector.OnGestureListener gestureListener = Mockito.mock( GestureDetector.OnGestureListener.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index e81369d9631c..9a99ed7bb512 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.biometrics import android.app.ActivityTaskManager import android.app.admin.DevicePolicyManager import android.content.pm.PackageManager +import android.content.res.Configuration import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager @@ -127,6 +128,12 @@ open class AuthContainerViewTest : SysuiTestCase() { private lateinit var packageManager: PackageManager @Mock private lateinit var activityTaskManager: ActivityTaskManager + private lateinit var displayRepository: FakeDisplayRepository + private lateinit var displayStateInteractor: DisplayStateInteractor + private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor + private lateinit var biometricStatusInteractor: BiometricStatusInteractor + private lateinit var iconProvider: IconProvider + private val testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val biometricPromptRepository = FakePromptRepository() @@ -142,17 +149,12 @@ open class AuthContainerViewTest : SysuiTestCase() { private val promptSelectorInteractor by lazy { PromptSelectorInteractorImpl( fingerprintRepository, + displayStateInteractor, biometricPromptRepository, lockPatternUtils, ) } - private lateinit var displayRepository: FakeDisplayRepository - private lateinit var displayStateInteractor: DisplayStateInteractor - private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor - private lateinit var biometricStatusInteractor: BiometricStatusInteractor - private lateinit var iconProvider: IconProvider - private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android) @@ -392,6 +394,33 @@ open class AuthContainerViewTest : SysuiTestCase() { } @Test + fun testAnimateToCredentialUI_rotateCredentialUI() { + val container = initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + container.animateToCredentialUI(false) + waitForIdleSync() + + assertThat(container.hasCredentialView()).isTrue() + assertThat(container.hasBiometricPrompt()).isFalse() + + // Check credential view persists after new attachment + container.onAttachedToWindow() + + assertThat(container.hasCredentialView()).isTrue() + assertThat(container.hasBiometricPrompt()).isFalse() + + val configuration = Configuration(context.resources.configuration) + configuration.orientation = Configuration.ORIENTATION_LANDSCAPE + container.dispatchConfigurationChanged(configuration) + waitForIdleSync() + + assertThat(container.hasCredentialView()).isTrue() + assertThat(container.hasBiometricPrompt()).isFalse() + } + + @Test fun testShowBiometricUI() { mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) val container = initializeFingerprintContainer() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index 3102a84b852a..6e78e334891b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -25,13 +25,16 @@ import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.FakePromptRepository import com.android.systemui.biometrics.faceSensorPropertiesInternal import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal import com.android.systemui.biometrics.shared.model.BiometricModalities +import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever @@ -75,12 +78,30 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { private val promptRepository = FakePromptRepository() private val fakeExecutor = FakeExecutor(FakeSystemClock()) + private lateinit var displayStateRepository: FakeDisplayStateRepository + private lateinit var displayRepository: FakeDisplayRepository + private lateinit var displayStateInteractor: DisplayStateInteractor private lateinit var interactor: PromptSelectorInteractor @Before fun setup() { + displayStateRepository = FakeDisplayStateRepository() + displayRepository = FakeDisplayRepository() + displayStateInteractor = + DisplayStateInteractorImpl( + testScope.backgroundScope, + mContext, + fakeExecutor, + displayStateRepository, + displayRepository, + ) interactor = - PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils) + PromptSelectorInteractorImpl( + fingerprintRepository, + displayStateInteractor, + promptRepository, + lockPatternUtils + ) } private fun basicPromptInfo() = @@ -155,7 +176,8 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { modalities, CHALLENGE, OP_PACKAGE_NAME, - false /*onSwitchToCredential*/ + onSwitchToCredential = false, + isLandscape = false, ) assertThat(currentPrompt).isNotNull() @@ -200,22 +222,49 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { fun promptKind_isBiometric_whenBiometricAllowed() = testScope.runTest { setUserCredentialType(isPassword = true) - val info = basicPromptInfo() val promptKind by collectLastValue(interactor.promptKind) assertThat(promptKind).isEqualTo(PromptKind.None) - interactor.setPrompt( - info, - USER_ID, - REQUEST_ID, - modalities, - CHALLENGE, - OP_PACKAGE_NAME, - false /*onSwitchToCredential*/ - ) + setPrompt() + + assertThat(promptKind?.isOnePanePortraitBiometric()).isTrue() + + interactor.resetPrompt(REQUEST_ID) + verifyUnset() + } + + @Test + fun promptKind_isBiometricTwoPane_whenBiometricAllowed_landscape() = + testScope.runTest { + setUserCredentialType(isPassword = true) + displayStateRepository.setIsLargeScreen(false) + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) + + val promptKind by collectLastValue(interactor.promptKind) + assertThat(promptKind).isEqualTo(PromptKind.None) + + setPrompt() + + assertThat(promptKind?.isTwoPaneLandscapeBiometric()).isTrue() + + interactor.resetPrompt(REQUEST_ID) + verifyUnset() + } + + @Test + fun promptKind_isBiometricOnePane_whenBiometricAllowed_largeScreenLandscape() = + testScope.runTest { + setUserCredentialType(isPassword = true) + displayStateRepository.setIsLargeScreen(true) + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) + + val promptKind by collectLastValue(interactor.promptKind) + assertThat(promptKind).isEqualTo(PromptKind.None) - assertThat(promptKind?.isBiometric()).isTrue() + setPrompt() + + assertThat(promptKind?.isOnePaneLargeScreenLandscapeBiometric()).isTrue() interactor.resetPrompt(REQUEST_ID) verifyUnset() @@ -225,20 +274,11 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { fun promptKind_isCredential_onSwitchToCredential() = testScope.runTest { setUserCredentialType(isPassword = true) - val info = basicPromptInfo() val promptKind by collectLastValue(interactor.promptKind) assertThat(promptKind).isEqualTo(PromptKind.None) - interactor.setPrompt( - info, - USER_ID, - REQUEST_ID, - modalities, - CHALLENGE, - OP_PACKAGE_NAME, - true /*onSwitchToCredential*/ - ) + setPrompt(onSwitchToCredential = true) assertThat(promptKind).isEqualTo(PromptKind.Password) @@ -259,15 +299,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { val promptKind by collectLastValue(interactor.promptKind) assertThat(promptKind).isEqualTo(PromptKind.None) - interactor.setPrompt( - info, - USER_ID, - REQUEST_ID, - modalities, - CHALLENGE, - OP_PACKAGE_NAME, - false /*onSwitchToCredential*/ - ) + setPrompt(info) assertThat(promptKind).isEqualTo(PromptKind.Password) @@ -292,15 +324,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { val promptKind by collectLastValue(interactor.promptKind) assertThat(promptKind).isEqualTo(PromptKind.None) - interactor.setPrompt( - info, - USER_ID, - REQUEST_ID, - modalities, - CHALLENGE, - OP_PACKAGE_NAME, - false /*onSwitchToCredential*/ - ) + setPrompt(info) assertThat(promptKind).isEqualTo(PromptKind.Password) @@ -312,6 +336,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { fun promptKind_isBiometric_whenBiometricIsNotAllowed_withVerticalList() = testScope.runTest { setUserCredentialType(isPassword = true) + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) val info = basicPromptInfo().apply { isDeviceCredentialAllowed = true @@ -322,22 +347,32 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { val promptKind by collectLastValue(interactor.promptKind) assertThat(promptKind).isEqualTo(PromptKind.None) - interactor.setPrompt( - info, - USER_ID, - REQUEST_ID, - modalities, - CHALLENGE, - OP_PACKAGE_NAME, - false /*onSwitchToCredential*/ - ) + setPrompt(info) - assertThat(promptKind?.isBiometric()).isTrue() + assertThat(promptKind?.isOnePaneNoSensorLandscapeBiometric()).isTrue() interactor.resetPrompt(REQUEST_ID) verifyUnset() } + private fun setPrompt( + info: PromptInfo = basicPromptInfo(), + onSwitchToCredential: Boolean = false + ) { + interactor.setPrompt( + info, + USER_ID, + REQUEST_ID, + modalities, + CHALLENGE, + OP_PACKAGE_NAME, + onSwitchToCredential = onSwitchToCredential, + isLandscape = + displayStateRepository.currentRotation.value == DisplayRotation.ROTATION_90 || + displayStateRepository.currentRotation.value == DisplayRotation.ROTATION_270, + ) + } + private fun TestScope.useCredentialAndReset(kind: PromptKind) { setUserCredentialType( isPin = kind == PromptKind.Pin, @@ -366,7 +401,8 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { BiometricModalities(), CHALLENGE, OP_PACKAGE_NAME, - false /*onSwitchToCredential*/ + onSwitchToCredential = false, + isLandscape = false, ) // not using biometrics, should be null with no fallback option diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index c177511a2df4..f46cfdc280fe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -383,11 +383,25 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } if (testCase.isFaceOnly) { - val expectedIconAsset = R.raw.face_dialog_authenticating + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark) + + val expectedIconAsset = + if (shouldPulseAnimation!!) { + if (lastPulseLightToDark!!) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + } else { + R.drawable.face_dialog_pulse_dark_to_light + } assertThat(iconAsset).isEqualTo(expectedIconAsset) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(true) } if (testCase.isCoex) { @@ -409,11 +423,26 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } } else { // implicit flow - val expectedIconAsset = R.raw.face_dialog_authenticating + val shouldRepeatAnimation by + collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark) + + val expectedIconAsset = + if (shouldPulseAnimation!!) { + if (lastPulseLightToDark!!) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + } else { + R.drawable.face_dialog_pulse_dark_to_light + } assertThat(iconAsset).isEqualTo(expectedIconAsset) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(true) } } } @@ -503,9 +532,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_error) assertThat(iconContentDescriptionId).isEqualTo(R.string.keyguard_face_failed) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) // Clear error, go to idle errorJob.join() @@ -514,6 +548,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_idle) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) } if (testCase.isCoex) { @@ -596,10 +631,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa // If co-ex, using implicit flow (explicit flow always requires confirmation) if (testCase.isFaceOnly || testCase.isCoex) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) } } } @@ -621,10 +661,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa ) if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_wink_from_dark) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) } // explicit flow because confirmation requested @@ -666,10 +711,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel.confirmAuthenticated() if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark) assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) } // explicit flow because confirmation requested @@ -1407,7 +1457,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa runningTaskInfo.topActivity = topActivity whenever(activityTaskManager.getTasks(1)).thenReturn(listOf(runningTaskInfo)) selector = - PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils) + PromptSelectorInteractorImpl( + fingerprintRepository, + displayStateInteractor, + promptRepository, + lockPatternUtils + ) selector.resetPrompt(REQUEST_ID) viewModel = @@ -1643,7 +1698,8 @@ private fun PromptSelectorInteractor.initializePrompt( BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), CHALLENGE, packageName, - false /*onUseDeviceCredential*/ + onSwitchToCredential = false, + isLandscape = false, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt index a18b0330d48f..ec2a1d305ab3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt @@ -17,9 +17,11 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.os.fakeExecutorHandler import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.testKosmos import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -33,12 +35,16 @@ import org.mockito.MockitoAnnotations class KeyguardBlueprintViewModelTest : SysuiTestCase() { @Mock private lateinit var keyguardBlueprintInteractor: KeyguardBlueprintInteractor private lateinit var undertest: KeyguardBlueprintViewModel + private val kosmos = testKosmos() @Before fun setup() { MockitoAnnotations.initMocks(this) undertest = - KeyguardBlueprintViewModel(keyguardBlueprintInteractor = keyguardBlueprintInteractor) + KeyguardBlueprintViewModel( + handler = kosmos.fakeExecutorHandler, + keyguardBlueprintInteractor = keyguardBlueprintInteractor, + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index 8a12a90efc4c..c1e3e84f2bf4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -49,6 +49,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter @@ -289,6 +290,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { underTest = KeyguardQuickAffordancesCombinedViewModel( + applicationScope = kosmos.applicationCoroutineScope, quickAffordanceInteractor = KeyguardQuickAffordanceInteractor( keyguardInteractor = keyguardInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 3bf4173cd7c7..3906c40593f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -191,7 +191,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Before fun setup() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) staticMockSession = ExtendedMockito.mockitoSession() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt index 5f7c3869fee7..20fb7014b146 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt @@ -29,7 +29,9 @@ import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.domain.pipeline.MediaDataManager @@ -49,7 +51,6 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.testKosmos import com.android.systemui.util.animation.UniqueObjectHostView -import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.settings.FakeSettings @@ -75,6 +76,8 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -112,12 +115,14 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private val testScope = kosmos.testScope private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var isQsBypassingShade: MutableStateFlow<Boolean> + private lateinit var shadeExpansion: MutableStateFlow<Float> private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() private val settings = FakeSettings() private lateinit var testableLooper: TestableLooper private lateinit var fakeHandler: FakeHandler private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val keyguardRepository = kosmos.fakeKeyguardRepository @Before fun setup() { @@ -129,7 +134,9 @@ class MediaHierarchyManagerTest : SysuiTestCase() { fakeHandler = FakeHandler(testableLooper.looper) whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame) isQsBypassingShade = MutableStateFlow(false) + shadeExpansion = MutableStateFlow(0f) whenever(shadeInteractor.isQsBypassingShade).thenReturn(isQsBypassingShade) + whenever(shadeInteractor.shadeExpansion).thenReturn(shadeExpansion) whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(false) mediaHierarchyManager = MediaHierarchyManager( @@ -141,6 +148,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { mediaDataManager, keyguardViewController, dreamOverlayStateController, + kosmos.keyguardInteractor, kosmos.communalTransitionViewModel, configurationController, wakefulnessLifecycle, @@ -191,7 +199,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), + any<MediaHostState>(), anyBoolean(), anyLong(), anyLong() @@ -204,7 +212,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController, times(0)) .onDesiredLocationChanged( ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), + any<MediaHostState>(), anyBoolean(), anyLong(), anyLong() @@ -218,7 +226,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), + any<MediaHostState>(), anyBoolean(), anyLong(), anyLong() @@ -231,7 +239,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController, times(0)) .onDesiredLocationChanged( ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), + any<MediaHostState>(), anyBoolean(), anyLong(), anyLong() @@ -245,7 +253,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), + any<MediaHostState>(), anyBoolean(), anyLong(), anyLong() @@ -255,7 +263,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), + any<MediaHostState>(), anyBoolean(), anyLong(), anyLong() @@ -269,7 +277,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), + any<MediaHostState>(), anyBoolean(), anyLong(), anyLong() @@ -281,7 +289,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), + any<MediaHostState>(), anyBoolean(), anyLong(), anyLong() @@ -297,7 +305,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( eq(MediaHierarchyManager.LOCATION_QQS), - any(MediaHostState::class.java), + any<MediaHostState>(), eq(false), anyLong(), anyLong() @@ -309,7 +317,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( eq(MediaHierarchyManager.LOCATION_LOCKSCREEN), - any(MediaHostState::class.java), + any<MediaHostState>(), eq(false), anyLong(), anyLong() @@ -482,6 +490,26 @@ class MediaHierarchyManagerTest : SysuiTestCase() { } @Test + fun isCurrentlyInGuidedTransformation_desiredLocationIsHub_returnsFalse() = + testScope.runTest { + goToLockscreen() + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + testScope = testScope, + ) + mediaHierarchyManager.qsExpansion = 0f + mediaHierarchyManager.setTransitionToFullShadeAmount(123f) + + whenever(lockHost.visible).thenReturn(true) + whenever(qsHost.visible).thenReturn(true) + whenever(qqsHost.visible).thenReturn(true) + whenever(hubModeHost.visible).thenReturn(true) + + assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse() + } + + @Test fun testDream() { goToDream() setMediaDreamComplicationEnabled(true) @@ -499,7 +527,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( eq(MediaHierarchyManager.LOCATION_QQS), - any(MediaHostState::class.java), + any<MediaHostState>(), eq(false), anyLong(), anyLong() @@ -532,7 +560,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( eq(MediaHierarchyManager.LOCATION_QQS), - any(MediaHostState::class.java), + any<MediaHostState>(), eq(false), anyLong(), anyLong() @@ -590,7 +618,50 @@ class MediaHierarchyManagerTest : SysuiTestCase() { verify(mediaCarouselController) .onDesiredLocationChanged( eq(MediaHierarchyManager.LOCATION_QS), - any(MediaHostState::class.java), + any<MediaHostState>(), + eq(false), + anyLong(), + anyLong() + ) + } + + @Test + fun testCommunalLocation_whenDreamingAndShadeExpanding() = + testScope.runTest { + keyguardRepository.setDreaming(true) + runCurrent() + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.DREAMING, + to = KeyguardState.GLANCEABLE_HUB, + testScope = testScope, + ) + // Mock the behavior for dreaming that pulling down shade will immediately set QS as + // expanded + expandQS() + // Starts opening the shade + shadeExpansion.value = 0.1f + runCurrent() + + // UMO shows on hub + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB), + anyOrNull(), + eq(false), + anyLong(), + anyLong() + ) + clearInvocations(mediaCarouselController) + + // The shade is opened enough to make QS elements visible + shadeExpansion.value = 0.5f + runCurrent() + + // UMO shows on QS + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_QS), + any<MediaHostState>(), eq(false), anyLong(), anyLong() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt index e5d3082bb245..80ebe56453ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt @@ -376,7 +376,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun attachPlayer_seekBarDisabled_seekBarVisibilityIsSetToInvisible() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) getEnabledChangeListener().onEnabledChanged(enabled = true) @@ -388,7 +388,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun attachPlayer_seekBarEnabled_seekBarVisible() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) getEnabledChangeListener().onEnabledChanged(enabled = true) @@ -399,7 +399,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun attachPlayer_seekBarStatusUpdate_seekBarVisibilityChanges() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) getEnabledChangeListener().onEnabledChanged(enabled = true) @@ -415,7 +415,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun attachPlayer_notScrubbing_scrubbingViewsGone() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.canShowScrubbingTime = true @@ -435,7 +435,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_noSemanticActions_scrubbingViewsGone() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.canShowScrubbingTime = false @@ -454,7 +454,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(true) @@ -476,7 +476,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(false) @@ -498,7 +498,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(true) @@ -524,7 +524,7 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun setIsScrubbing_trueThenFalse_reservePrevAndNextButtons() { - whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(true, ConstraintSet.INVISIBLE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt index ef73e2e493b1..e4877808f133 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt @@ -28,7 +28,6 @@ import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager -import com.android.systemui.mediaprojection.taskswitcher.mediaProjectionManagerRepository import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -45,7 +44,7 @@ class MediaProjectionManagerRepositoryTest : SysuiTestCase() { private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager - private val repo = kosmos.mediaProjectionManagerRepository + private val repo = kosmos.realMediaProjectionRepository @Test fun switchProjectedTask_stateIsUpdatedWithNewTask() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt index f1c97dd45f09..23cf7fb9c73d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt @@ -55,6 +55,7 @@ class BackPanelControllerTest : SysuiTestCase() { companion object { private const val START_X: Float = 0f } + private val kosmos = testKosmos() private lateinit var mBackPanelController: BackPanelController private lateinit var systemClock: FakeSystemClock diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java index 6956beab418e..09d6a1a18ccd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java @@ -39,7 +39,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; -import android.annotation.Nullable; import android.content.Context; import android.graphics.Rect; import android.os.Bundle; @@ -51,6 +50,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import androidx.compose.ui.platform.ComposeView; import androidx.lifecycle.Lifecycle; import androidx.test.filters.SmallTest; @@ -58,12 +58,10 @@ import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.EnableSceneContainer; -import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.media.controls.ui.view.MediaHost; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSComponent; import com.android.systemui.qs.external.TileServiceRequestController; -import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.res.R; @@ -112,9 +110,7 @@ public class QSImplTest extends SysuiTestCase { @Mock private QSSquishinessController mSquishinessController; @Mock private FooterActionsViewModel mFooterActionsViewModel; @Mock private FooterActionsViewModel.Factory mFooterActionsViewModelFactory; - @Mock private FooterActionsViewBinder mFooterActionsViewBinder; @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; - @Mock private FeatureFlagsClassic mFeatureFlags; private ViewGroup mQsView; private final CommandQueue mCommandQueue = @@ -259,6 +255,39 @@ public class QSImplTest extends SysuiTestCase { } @Test + public void setQsExpansion_whenShouldUpdateSquishinessTrue_setsSquishinessBasedOnFraction() { + enableSplitShade(); + when(mStatusBarStateController.getState()).thenReturn(KEYGUARD); + float expansion = 0.456f; + float panelExpansionFraction = 0.678f; + float proposedTranslation = 567f; + float squishinessFraction = 0.789f; + + mUnderTest.setShouldUpdateSquishinessOnMedia(true); + mUnderTest.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation, + squishinessFraction); + + verify(mQSMediaHost).setSquishFraction(squishinessFraction); + } + + @Test + public void setQsExpansion_whenOnKeyguardAndShouldUpdateSquishinessFalse_setsSquishiness() { + // Random test values without any meaning. They just have to be different from each other. + float expansion = 0.123f; + float panelExpansionFraction = 0.321f; + float proposedTranslation = 456f; + float squishinessFraction = 0.567f; + + enableSplitShade(); + setStatusBarCurrentAndUpcomingState(KEYGUARD); + mUnderTest.setShouldUpdateSquishinessOnMedia(false); + mUnderTest.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation, + squishinessFraction); + + verify(mQSMediaHost).setSquishFraction(1.0f); + } + + @Test public void setQsExpansion_inSplitShade_setsFooterActionsExpansion_basedOnPanelExpFraction() { // Random test values without any meaning. They just have to be different from each other. float expansion = 0.123f; @@ -496,18 +525,13 @@ public class QSImplTest extends SysuiTestCase { @Test @EnableSceneContainer public void testSceneContainerFlagsEnabled_FooterActionsRemoved_controllerNotStarted() { - clearInvocations( - mFooterActionsViewBinder, mFooterActionsViewModel, mFooterActionsViewModelFactory); + clearInvocations(mFooterActionsViewModel, mFooterActionsViewModelFactory); QSImpl other = instantiate(); other.onComponentCreated(mQsComponent, null); assertThat((View) other.getView().findViewById(R.id.qs_footer_actions)).isNull(); - verifyZeroInteractions( - mFooterActionsViewModel, - mFooterActionsViewBinder, - mFooterActionsViewModelFactory - ); + verifyZeroInteractions(mFooterActionsViewModel, mFooterActionsViewModelFactory); } @Test @@ -553,9 +577,7 @@ public class QSImplTest extends SysuiTestCase { mock(QSLogger.class), mock(FooterActionsController.class), mFooterActionsViewModelFactory, - mFooterActionsViewBinder, - mLargeScreenShadeInterpolator, - mFeatureFlags + mLargeScreenShadeInterpolator ); } @@ -589,41 +611,20 @@ public class QSImplTest extends SysuiTestCase { customizer.setId(android.R.id.edit); mQsView.addView(customizer); - View footerActionsView = new FooterActionsViewBinder().create(mContext); + ComposeView footerActionsView = new ComposeView(mContext); footerActionsView.setId(R.id.qs_footer_actions); mQsView.addView(footerActionsView); } private void setUpInflater() { - LayoutInflater realInflater = LayoutInflater.from(mContext); - when(mLayoutInflater.cloneInContext(any(Context.class))).thenReturn(mLayoutInflater); when(mLayoutInflater.inflate(anyInt(), nullable(ViewGroup.class), anyBoolean())) - .thenAnswer((invocation) -> inflate(realInflater, (int) invocation.getArgument(0), - (ViewGroup) invocation.getArgument(1), - (boolean) invocation.getArgument(2))); + .thenAnswer((invocation) -> mQsView); when(mLayoutInflater.inflate(anyInt(), nullable(ViewGroup.class))) - .thenAnswer((invocation) -> inflate(realInflater, (int) invocation.getArgument(0), - (ViewGroup) invocation.getArgument(1))); + .thenAnswer((invocation) -> mQsView); mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater); } - private View inflate(LayoutInflater realInflater, int layoutRes, @Nullable ViewGroup root) { - return inflate(realInflater, layoutRes, root, root != null); - } - - private View inflate(LayoutInflater realInflater, int layoutRes, @Nullable ViewGroup root, - boolean attachToRoot) { - if (layoutRes == R.layout.footer_actions - || layoutRes == R.layout.footer_actions_text_button - || layoutRes == R.layout.footer_actions_number_button - || layoutRes == R.layout.footer_actions_icon_button) { - return realInflater.inflate(layoutRes, root, attachToRoot); - } - - return mQsView; - } - private void setupQsComponent() { when(mQsComponent.getQSPanelController()).thenReturn(mQSPanelController); when(mQsComponent.getQuickQSPanelController()).thenReturn(mQuickQSPanelController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt index 2da4b7296c35..87031d93db15 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt @@ -31,9 +31,6 @@ import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -46,22 +43,21 @@ import org.junit.runner.RunWith class GridConsistencyInteractorTest : SysuiTestCase() { private val iconOnlyTiles = - MutableStateFlow( - setOf( - TileSpec.create("smallA"), - TileSpec.create("smallB"), - TileSpec.create("smallC"), - TileSpec.create("smallD"), - TileSpec.create("smallE"), - ) + setOf( + TileSpec.create("smallA"), + TileSpec.create("smallB"), + TileSpec.create("smallC"), + TileSpec.create("smallD"), + TileSpec.create("smallE"), ) private val kosmos = testKosmos().apply { iconTilesRepository = object : IconTilesRepository { - override val iconTilesSpecs: StateFlow<Set<TileSpec>> - get() = iconOnlyTiles.asStateFlow() + override fun isIconTile(spec: TileSpec): Boolean { + return iconOnlyTiles.contains(spec) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt index bda48adbfcc3..1eb6d63c5a39 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt @@ -25,9 +25,6 @@ import com.android.systemui.qs.panels.data.repository.iconTilesRepository import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -37,21 +34,20 @@ import org.junit.runner.RunWith class InfiniteGridConsistencyInteractorTest : SysuiTestCase() { private val iconOnlyTiles = - MutableStateFlow( - setOf( - TileSpec.create("smallA"), - TileSpec.create("smallB"), - TileSpec.create("smallC"), - TileSpec.create("smallD"), - TileSpec.create("smallE"), - ) + setOf( + TileSpec.create("smallA"), + TileSpec.create("smallB"), + TileSpec.create("smallC"), + TileSpec.create("smallD"), + TileSpec.create("smallE"), ) private val kosmos = testKosmos().apply { iconTilesRepository = object : IconTilesRepository { - override val iconTilesSpecs: StateFlow<Set<TileSpec>> - get() = iconOnlyTiles.asStateFlow() + override fun isIconTile(spec: TileSpec): Boolean { + return iconOnlyTiles.contains(spec) + } } } private val underTest = with(kosmos) { infiniteGridConsistencyInteractor } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index bde1445acfa8..b8267a0e83d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.shade import android.graphics.Rect import android.os.PowerManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.ViewUtils @@ -30,6 +32,7 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE import com.android.systemui.SysuiTestCase import com.android.systemui.ambient.touch.TouchHandler import com.android.systemui.ambient.touch.TouchMonitor @@ -51,6 +54,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat @@ -64,9 +68,11 @@ import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @ExperimentalCoroutinesApi @@ -124,6 +130,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ambientTouchComponentFactory, communalContent, kosmos.sceneDataSourceDelegator, + kosmos.notificationStackScrollLayoutController ) } testableLooper = TestableLooper.get(this) @@ -166,6 +173,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ambientTouchComponentFactory, communalContent, kosmos.sceneDataSourceDelegator, + kosmos.notificationStackScrollLayoutController ) // First call succeeds. @@ -176,6 +184,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalClosed_doesNotIntercept() = with(kosmos) { @@ -187,6 +196,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_openGesture_interceptsTouches() = with(kosmos) { @@ -204,6 +214,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalTransitioning_interceptsTouches() = with(kosmos) { @@ -230,6 +241,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalOpen_interceptsTouches() = with(kosmos) { @@ -244,6 +256,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() = with(kosmos) { @@ -262,6 +275,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() = with(kosmos) { @@ -278,6 +292,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_containerViewDisposed_doesNotIntercept() = with(kosmos) { @@ -310,6 +325,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ambientTouchComponentFactory, communalContent, kosmos.sceneDataSourceDelegator, + kosmos.notificationStackScrollLayoutController, ) assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) @@ -329,6 +345,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ambientTouchComponentFactory, communalContent, kosmos.sceneDataSourceDelegator, + kosmos.notificationStackScrollLayoutController, ) // Only initView without attaching a view as we don't want the flows to start collecting @@ -499,13 +516,30 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) + fun fullScreenSwipeGesture_doNotProcessTouchesInNotificationStack() = + with(kosmos) { + testScope.runTest { + // Communal is closed. + goToScene(CommunalScenes.Blank) + `when`( + notificationStackScrollLayoutController.isBelowLastNotification( + anyFloat(), + anyFloat() + ) + ) + .thenReturn(false) + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + } + } + private fun initAndAttachContainerView() { containerView = View(context) parentView = FrameLayout(context) - parentView.addView(containerView) - underTest.initView(containerView) + parentView.addView(underTest.initView(containerView)) // Attach the view so that flows start collecting. ViewUtils.attachView(parentView, CONTAINER_WIDTH, CONTAINER_HEIGHT) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 041adea8decc..c3cedf84a864 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -837,6 +837,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mJavaAdapter, mCastController, new ResourcesSplitShadeStateController(), + () -> mKosmos.getCommunalTransitionViewModel(), () -> mLargeScreenHeaderHelper ); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index 845744a54791..85541aa8abda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -308,6 +308,7 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { new JavaAdapter(mTestScope.getBackgroundScope()), mCastController, splitShadeStateController, + () -> mKosmos.getCommunalTransitionViewModel(), () -> mLargeScreenHeaderHelper ); mQsController.init(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt new file mode 100644 index 000000000000..0f33b9dfc077 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.viewmodel.mediaProjectionChipInteractor +import com.android.systemui.util.time.fakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.test.runTest + +@SmallTest +class MediaProjectionChipInteractorTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val testScope = kosmos.testScope + private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository + private val systemClock = kosmos.fakeSystemClock + + private val underTest = kosmos.mediaProjectionChipInteractor + + @Test + fun chip_notProjectingState_isHidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + } + + @Test + fun chip_singleTaskState_isShownWithIcon() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.SingleTask(createTask(taskId = 1)) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + val icon = (latest as OngoingActivityChipModel.Shown).icon + assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected) + } + + @Test + fun chip_entireScreenState_isShownWithIcon() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.EntireScreen + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + val icon = (latest as OngoingActivityChipModel.Shown).icon + assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected) + } + + @Test + fun chip_timeResetsOnEachNewShare() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + systemClock.setElapsedRealtime(1234) + mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.EntireScreen + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234) + + mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + + systemClock.setElapsedRealtime(5678) + mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.EntireScreen + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index 1260f07d0ba1..121229c321b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -23,6 +23,9 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask import com.android.systemui.res.R import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository @@ -35,13 +38,20 @@ import org.junit.Test class OngoingActivityChipsViewModelTest : SysuiTestCase() { private val kosmos = Kosmos() + private val testScope = kosmos.testScope + + private val screenRecordState = kosmos.screenRecordRepository.screenRecordState + private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState + private val callState = kosmos.callChipInteractor.chip + private val underTest = kosmos.ongoingActivityChipsViewModel @Test fun chip_allHidden_hidden() = - kosmos.testScope.runTest { - kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing - kosmos.callChipInteractor.chip.value = OngoingActivityChipModel.Hidden + testScope.runTest { + screenRecordState.value = ScreenRecordModel.DoingNothing + mediaProjectionState.value = MediaProjectionState.NotProjecting + callState.value = OngoingActivityChipModel.Hidden val latest by collectLastValue(underTest.chip) @@ -50,9 +60,10 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { @Test fun chip_screenRecordShow_restHidden_screenRecordShown() = - kosmos.testScope.runTest { - kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording - kosmos.callChipInteractor.chip.value = OngoingActivityChipModel.Hidden + testScope.runTest { + screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionState.value = MediaProjectionState.NotProjecting + callState.value = OngoingActivityChipModel.Hidden val latest by collectLastValue(underTest.chip) @@ -61,15 +72,15 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { @Test fun chip_screenRecordShowAndCallShow_screenRecordShown() = - kosmos.testScope.runTest { - kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + testScope.runTest { + screenRecordState.value = ScreenRecordModel.Recording val callChip = OngoingActivityChipModel.Shown( Icon.Resource(R.drawable.ic_call, ContentDescription.Loaded("icon")), startTimeMs = 600L, ) {} - kosmos.callChipInteractor.chip.value = callChip + callState.value = callChip val latest by collectLastValue(underTest.chip) @@ -77,16 +88,46 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { } @Test - fun chip_screenRecordHideAndCallShown_callShown() = - kosmos.testScope.runTest { - kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing + fun chip_screenRecordShowAndMediaProjectionShow_screenRecordShown() = + testScope.runTest { + screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionState.value = MediaProjectionState.EntireScreen + callState.value = OngoingActivityChipModel.Hidden + + val latest by collectLastValue(underTest.chip) + + assertIsScreenRecordChip(latest) + } + @Test + fun chip_mediaProjectionShowAndCallShow_mediaProjectionShown() = + testScope.runTest { + screenRecordState.value = ScreenRecordModel.DoingNothing + mediaProjectionState.value = MediaProjectionState.EntireScreen val callChip = OngoingActivityChipModel.Shown( Icon.Resource(R.drawable.ic_call, ContentDescription.Loaded("icon")), startTimeMs = 600L, ) {} - kosmos.callChipInteractor.chip.value = callChip + callState.value = callChip + + val latest by collectLastValue(underTest.chip) + + assertIsMediaProjectionChip(latest) + } + + @Test + fun chip_screenRecordAndMediaProjectionHideAndCallShown_callShown() = + testScope.runTest { + screenRecordState.value = ScreenRecordModel.DoingNothing + mediaProjectionState.value = MediaProjectionState.NotProjecting + + val callChip = + OngoingActivityChipModel.Shown( + Icon.Resource(R.drawable.ic_call, ContentDescription.Loaded("icon")), + startTimeMs = 600L, + ) {} + callState.value = callChip val latest by collectLastValue(underTest.chip) @@ -95,22 +136,29 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { @Test fun chip_higherPriorityChipAdded_lowerPriorityChipReplaced() = - kosmos.testScope.runTest { + testScope.runTest { // Start with just the lower priority call chip val callChip = OngoingActivityChipModel.Shown( Icon.Resource(R.drawable.ic_call, ContentDescription.Loaded("icon")), startTimeMs = 600L, ) {} - kosmos.callChipInteractor.chip.value = callChip - kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing + callState.value = callChip + mediaProjectionState.value = MediaProjectionState.NotProjecting + screenRecordState.value = ScreenRecordModel.DoingNothing val latest by collectLastValue(underTest.chip) assertThat(latest).isEqualTo(callChip) + // WHEN the higher priority media projection chip is added + mediaProjectionState.value = MediaProjectionState.SingleTask(createTask(taskId = 1)) + + // THEN the higher priority media projection chip is used + assertIsMediaProjectionChip(latest) + // WHEN the higher priority screen record chip is added - kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + screenRecordState.value = ScreenRecordModel.Recording // THEN the higher priority screen record chip is used assertIsScreenRecordChip(latest) @@ -118,31 +166,47 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { @Test fun chip_highestPriorityChipRemoved_showsNextPriorityChip() = - kosmos.testScope.runTest { - // Start with both the higher priority screen record chip and lower priority call chip - kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + testScope.runTest { + // WHEN all chips are active + screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionState.value = MediaProjectionState.EntireScreen val callChip = OngoingActivityChipModel.Shown( Icon.Resource(R.drawable.ic_call, ContentDescription.Loaded("icon")), startTimeMs = 600L, ) {} - kosmos.callChipInteractor.chip.value = callChip + callState.value = callChip val latest by collectLastValue(underTest.chip) + // THEN the highest priority screen record is used assertIsScreenRecordChip(latest) // WHEN the higher priority screen record is removed - kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing + screenRecordState.value = ScreenRecordModel.DoingNothing + + // THEN the lower priority media projection is used + assertIsMediaProjectionChip(latest) + + // WHEN the higher priority media projection is removed + mediaProjectionState.value = MediaProjectionState.NotProjecting // THEN the lower priority call is used assertThat(latest).isEqualTo(callChip) } - private fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) { - assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) - val icon = (latest as OngoingActivityChipModel.Shown).icon - assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.stat_sys_screen_record) + companion object { + fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) { + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + val icon = (latest as OngoingActivityChipModel.Shown).icon + assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.stat_sys_screen_record) + } + + fun assertIsMediaProjectionChip(latest: OngoingActivityChipModel?) { + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + val icon = (latest as OngoingActivityChipModel.Shown).icon + assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt index 7903a731c1d0..e984200c305e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt @@ -91,7 +91,8 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) { ensurePeekState() assertShouldHeadsUp( @@ -110,7 +111,8 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) { ensurePeekState() assertShouldNotHeadsUp( @@ -129,7 +131,8 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) { ensurePeekState() assertShouldHeadsUp( @@ -146,7 +149,8 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) { ensurePeekState() assertShouldHeadsUp( @@ -163,7 +167,8 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) { ensurePeekState() assertShouldHeadsUp( @@ -180,7 +185,8 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) { ensurePeekState() assertShouldHeadsUp( @@ -197,7 +203,8 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) { assertFsiNotSuppressed() } @@ -208,7 +215,8 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) { ensurePeekState() assertShouldHeadsUp( @@ -232,7 +240,8 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro ).thenReturn(PERMISSION_GRANTED) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager) + AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager, + uiEventLogger) ) { ensurePeekState() assertShouldHeadsUp( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt index 87d813c6d19f..e0f1e1a46d3b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt @@ -63,7 +63,7 @@ class CollapsedStatusBarFragmentLoggerTest : SysuiTestCase() { StatusBarVisibilityModel( showClock = false, showNotificationIcons = true, - showOngoingCallChip = false, + showOngoingActivityChip = false, showSystemInfo = true, ) ) @@ -74,7 +74,7 @@ class CollapsedStatusBarFragmentLoggerTest : SysuiTestCase() { assertThat(actualString).contains("showClock=false") assertThat(actualString).contains("showNotificationIcons=true") - assertThat(actualString).contains("showOngoingCallChip=false") + assertThat(actualString).contains("showOngoingActivityChip=false") assertThat(actualString).contains("showSystemInfo=true") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index ff182ad4a5ea..ee27cea48565 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone.fragment; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN; @@ -33,6 +34,8 @@ import android.app.StatusBarManager; import android.content.Context; import android.os.Bundle; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; @@ -46,7 +49,6 @@ import com.android.systemui.SysuiBaseFragmentTest; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.LogcatEchoTracker; import com.android.systemui.plugins.DarkIconDispatcher; @@ -56,7 +58,6 @@ import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.OperatorNameViewController; -import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder; @@ -92,11 +93,9 @@ import java.util.List; @RunWithLooper(setAsMainLooper = true) @SmallTest public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { - private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(); private NotificationIconAreaController mMockNotificationAreaController; private ShadeExpansionStateManager mShadeExpansionStateManager; private OngoingCallController mOngoingCallController; - private OngoingActivityChipsViewModel mOngoingActivityChipsViewModel; private SystemStatusAnimationScheduler mAnimationScheduler; private StatusBarLocationPublisher mLocationPublisher; // Set in instantiate() @@ -421,6 +420,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test + @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) public void disable_noOngoingCall_chipHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -433,6 +433,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test + @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) public void disable_hasOngoingCall_chipDisplayedAndNotificationIconsHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -446,6 +447,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test + @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) public void disable_hasOngoingCallButNotificationIconsDisabled_chipHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -459,6 +461,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test + @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) public void disable_hasOngoingCallButAlsoHun_chipHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -472,6 +475,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test + @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) public void disable_ongoingCallEnded_chipHidden() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -498,8 +502,11 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test + @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) public void disable_hasOngoingCall_hidesNotifsWithoutAnimation() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + // Enable animations for testing so that we can verify we still aren't animating + fragment.enableAnimationsForTesting(); fragment.disable(DEFAULT_DISPLAY, 0, 0, false); // Ongoing call started @@ -512,6 +519,161 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test + @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) + public void screenSharingChipsDisabled_ignoresNewCallback() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + + // WHEN there *is* an ongoing call via old callback + when(mOngoingCallController.hasOngoingCall()).thenReturn(true); + fragment.disable(DEFAULT_DISPLAY, 0, 0, true); + + // WHEN there's *no* ongoing activity via new callback + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ false); + + // THEN the old callback value is used, so the view is shown + assertEquals(View.VISIBLE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + + // WHEN there's *no* ongoing call via old callback + when(mOngoingCallController.hasOngoingCall()).thenReturn(false); + fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + + // WHEN there *is* an ongoing activity via new callback + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ true); + + // THEN the old callback value is used, so the view is hidden + assertEquals(View.GONE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) + public void noOngoingActivity_chipHidden() { + resumeAndGetFragment(); + + // TODO(b/332662551): We *should* be able to just set a value on + // mCollapsedStatusBarViewModel.getOngoingActivityChip() instead of manually invoking the + // listener, but I'm unable to get the fragment to get attached so that the binder starts + // listening to flows. + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ false); + + assertEquals(View.GONE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) + public void hasOngoingActivity_chipDisplayedAndNotificationIconsHidden() { + resumeAndGetFragment(); + + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ true); + + assertEquals(View.VISIBLE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility()); + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) + public void hasOngoingActivityButNotificationIconsDisabled_chipHidden() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ true); + + fragment.disable(DEFAULT_DISPLAY, + StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false); + + assertEquals(View.GONE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) + public void hasOngoingActivityButAlsoHun_chipHidden() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ true); + when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true); + + fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + + assertEquals(View.GONE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) + public void ongoingActivityEnded_chipHidden() { + resumeAndGetFragment(); + + // Ongoing activity started + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ true); + + assertEquals(View.VISIBLE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + + // Ongoing activity ended + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ false); + + assertEquals(View.GONE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) + public void hasOngoingActivity_hidesNotifsWithoutAnimation() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + // Enable animations for testing so that we can verify we still aren't animating + fragment.enableAnimationsForTesting(); + + // Ongoing call started + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ true); + + // Notification area is hidden without delay + assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01); + assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility()); + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) + public void screenSharingChipsEnabled_ignoresOngoingCallController() { + CollapsedStatusBarFragment fragment = resumeAndGetFragment(); + + // WHEN there *is* an ongoing call via old callback + when(mOngoingCallController.hasOngoingCall()).thenReturn(true); + fragment.disable(DEFAULT_DISPLAY, 0, 0, true); + + // WHEN there's *no* ongoing activity via new callback + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ false); + + // THEN the new callback value is used, so the view is hidden + assertEquals(View.GONE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + + // WHEN there's *no* ongoing call via old callback + when(mOngoingCallController.hasOngoingCall()).thenReturn(false); + fragment.disable(DEFAULT_DISPLAY, 0, 0, false); + + // WHEN there *is* an ongoing activity via new callback + mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( + /* hasOngoingActivity= */ true); + + // THEN the new callback value is used, so the view is shown + assertEquals(View.VISIBLE, + mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); + } + + @Test public void disable_isDozing_clockAndSystemInfoVisible() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); when(mStatusBarStateController.isDozing()).thenReturn(true); @@ -670,7 +832,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { MockitoAnnotations.initMocks(this); setUpDaggerComponent(); mOngoingCallController = mock(OngoingCallController.class); - mOngoingActivityChipsViewModel = mKosmos.getOngoingActivityChipsViewModel(); mAnimationScheduler = mock(SystemStatusAnimationScheduler.class); mLocationPublisher = mock(StatusBarLocationPublisher.class); mStatusBarIconController = mock(StatusBarIconController.class); @@ -691,7 +852,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { return new CollapsedStatusBarFragment( mStatusBarFragmentComponentFactory, mOngoingCallController, - mOngoingActivityChipsViewModel, mAnimationScheduler, mLocationPublisher, mMockNotificationAreaController, @@ -773,7 +933,9 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { private CollapsedStatusBarFragment resumeAndGetFragment() { mFragments.dispatchResume(); processAllMessages(); - return (CollapsedStatusBarFragment) mFragment; + CollapsedStatusBarFragment fragment = (CollapsedStatusBarFragment) mFragment; + fragment.disableAnimationsForTesting(); + return fragment; } private View getUserChipView() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt index 8e789cb2cae6..022b5d295256 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/StatusBarVisibilityModelTest.kt @@ -36,7 +36,7 @@ class StatusBarVisibilityModelTest : SysuiTestCase() { StatusBarVisibilityModel( showClock = true, showNotificationIcons = true, - showOngoingCallChip = true, + showOngoingActivityChip = true, showSystemInfo = true, ) @@ -72,17 +72,17 @@ class StatusBarVisibilityModelTest : SysuiTestCase() { } @Test - fun createModelFromFlags_ongoingCallChipNotDisabled_showOngoingCallChipTrue() { + fun createModelFromFlags_ongoingCallChipNotDisabled_showOngoingActivityChipTrue() { val result = createModelFromFlags(disabled1 = 0, disabled2 = 0) - assertThat(result.showOngoingCallChip).isTrue() + assertThat(result.showOngoingActivityChip).isTrue() } @Test - fun createModelFromFlags_ongoingCallChipDisabled_showOngoingCallChipFalse() { + fun createModelFromFlags_ongoingCallChipDisabled_showOngoingActivityChipFalse() { val result = createModelFromFlags(disabled1 = DISABLE_ONGOING_CALL_CHIP, disabled2 = 0) - assertThat(result.showOngoingCallChip).isFalse() + assertThat(result.showOngoingActivityChip).isFalse() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt index 606feab86d58..c9fe44918757 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt @@ -32,6 +32,14 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.assertLogsWtf +import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository +import com.android.systemui.screenrecord.data.model.ScreenRecordModel +import com.android.systemui.screenrecord.data.repository.screenRecordRepository +import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsMediaProjectionChip +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip +import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.data.model.StatusBarMode import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository @@ -65,6 +73,7 @@ class CollapsedStatusBarViewModelImplTest : SysuiTestCase() { kosmos.lightsOutInteractor, kosmos.activeNotificationsInteractor, kosmos.keyguardTransitionInteractor, + kosmos.ongoingActivityChipsViewModel, kosmos.applicationCoroutineScope, ) @@ -382,6 +391,25 @@ class CollapsedStatusBarViewModelImplTest : SysuiTestCase() { } } + @Test + fun ongoingActivityChip_matchesViewModel() = + testScope.runTest { + val latest by collectLastValue(underTest.ongoingActivityChip) + + kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + + assertIsScreenRecordChip(latest) + + kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing + + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden) + + kosmos.fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.EntireScreen + + assertIsMediaProjectionChip(latest) + } + private fun activeNotificationsStore(notifications: List<ActiveNotificationModel>) = ActiveNotificationsStore.Builder() .apply { notifications.forEach(::addIndividualNotif) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt index bc50f7967403..c3c9907bc891 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel +import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -27,6 +28,9 @@ class FakeCollapsedStatusBarViewModel : CollapsedStatusBarViewModel { override val transitionFromLockscreenToDreamStartedEvent = MutableSharedFlow<Unit>() + override val ongoingActivityChip: MutableStateFlow<OngoingActivityChipModel> = + MutableStateFlow(OngoingActivityChipModel.Hidden) + override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = areNotificationLightsOut fun setNotificationLightsOut(lightsOut: Boolean) { diff --git a/packages/SystemUI/tests/utils/src/android/graphics/drawable/TestStubDrawable.kt b/packages/SystemUI/tests/utils/src/android/graphics/drawable/TestStubDrawable.kt index 1a9f4b40c179..430fb5985848 100644 --- a/packages/SystemUI/tests/utils/src/android/graphics/drawable/TestStubDrawable.kt +++ b/packages/SystemUI/tests/utils/src/android/graphics/drawable/TestStubDrawable.kt @@ -27,8 +27,11 @@ import android.graphics.PixelFormat class TestStubDrawable(private val name: String? = null) : Drawable() { override fun draw(canvas: Canvas) = Unit + override fun setAlpha(alpha: Int) = Unit + override fun setColorFilter(colorFilter: ColorFilter?) = Unit + override fun getOpacity(): Int = PixelFormat.UNKNOWN override fun toString(): String { @@ -38,6 +41,10 @@ class TestStubDrawable(private val name: String? = null) : Drawable() { override fun getConstantState(): ConstantState = TestStubConstantState(this, changingConfigurations) + override fun equals(other: Any?): Boolean { + return (other as? TestStubDrawable ?: return false).name == name + } + private class TestStubConstantState( private val drawable: Drawable, private val changingConfigurations: Int, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java index e470406499b6..a5819931549d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java @@ -19,6 +19,7 @@ import static org.mockito.Mockito.when; import android.app.Fragment; import android.app.Instrumentation; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.BaseFragmentTest; import android.testing.DexmakerShareClassLoaderRule; @@ -31,6 +32,7 @@ import com.android.systemui.utils.leaks.LeakCheckedTest.SysuiLeakCheck; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Rule; import org.mockito.Mockito; @@ -43,6 +45,12 @@ public abstract class SysuiBaseFragmentTest extends BaseFragmentTest { @Rule public final SysuiLeakCheck mLeakCheck = new SysuiLeakCheck(); + @ClassRule + public static final SetFlagsRule.ClassRule mSetFlagsClassRule = + new SetFlagsRule.ClassRule( + com.android.systemui.Flags.class); + @Rule public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule(); + @Rule public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = new DexmakerShareClassLoaderRule(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt index 9765d531472c..53285eb715ba 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeDisplayStateRepository.kt @@ -23,7 +23,6 @@ import com.android.systemui.dagger.SysUISingleton import dagger.Binds import dagger.Module import javax.inject.Inject -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -40,7 +39,7 @@ class FakeDisplayStateRepository @Inject constructor() : DisplayStateRepository override val currentDisplaySize: StateFlow<Size> = _currentDisplaySize.asStateFlow() private val _isLargeScreen = MutableStateFlow<Boolean>(false) - override val isLargeScreen: Flow<Boolean> = _isLargeScreen.asStateFlow() + override val isLargeScreen: StateFlow<Boolean> = _isLargeScreen.asStateFlow() override val isReverseDefaultRotation = false @@ -55,6 +54,10 @@ class FakeDisplayStateRepository @Inject constructor() : DisplayStateRepository fun setCurrentDisplaySize(size: Size) { _currentDisplaySize.value = size } + + fun setIsLargeScreen(isLargeScreen: Boolean) { + _isLargeScreen.value = isLargeScreen + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt index 7f9a71cd149e..56297f0d7f43 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.kosmos.Kosmos.Fixture val Kosmos.promptSelectorInteractor by Fixture { PromptSelectorInteractorImpl( fingerprintPropertyRepository = fingerprintPropertyRepository, + displayStateInteractor = displayStateInteractor, promptRepository = promptRepository, lockPatternUtils = lockPatternUtils ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt index a05b5e65ce9d..ad5242e2e036 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt @@ -19,7 +19,7 @@ package com.android.systemui.brightness.data.repository import android.hardware.display.BrightnessInfo import android.hardware.display.BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE import android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF -import com.android.systemui.brightness.data.model.LinearBrightness +import com.android.systemui.brightness.shared.model.LinearBrightness import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt index 22784e47d277..0e8427310895 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt @@ -18,6 +18,15 @@ package com.android.systemui.brightness.domain.interactor import com.android.systemui.brightness.data.repository.screenBrightnessRepository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.util.mockito.mock val Kosmos.screenBrightnessInteractor by - Kosmos.Fixture { ScreenBrightnessInteractor(screenBrightnessRepository) } + Kosmos.Fixture { + ScreenBrightnessInteractor( + screenBrightnessRepository, + applicationCoroutineScope, + mock<TableLogBuffer>(), + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt index 63b87c075378..0c538ff1d6fe 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt @@ -16,8 +16,14 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.os.fakeExecutorHandler import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor import com.android.systemui.kosmos.Kosmos val Kosmos.keyguardBlueprintViewModel by - Kosmos.Fixture { KeyguardBlueprintViewModel(keyguardBlueprintInteractor) } + Kosmos.Fixture { + KeyguardBlueprintViewModel( + fakeExecutorHandler, + keyguardBlueprintInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index f856d2700270..2567ffee9be8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -32,7 +32,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.keyguardRootViewModel by Fixture { KeyguardRootViewModel( - scope = applicationCoroutineScope, + applicationScope = applicationCoroutineScope, deviceEntryInteractor = deviceEntryInteractor, dozeParameters = dozeParameters, keyguardInteractor = keyguardInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index b8620781bc1e..0b28e3f0a083 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -29,6 +29,7 @@ import com.android.systemui.common.ui.data.repository.fakeConfigurationRepositor import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor @@ -87,6 +88,7 @@ class KosmosJavaAdapter() { val configurationInteractor by lazy { kosmos.configurationInteractor } val bouncerRepository by lazy { kosmos.bouncerRepository } val communalRepository by lazy { kosmos.fakeCommunalSceneRepository } + val communalTransitionViewModel by lazy { kosmos.communalTransitionViewModel } val headsUpNotificationInteractor by lazy { kosmos.headsUpNotificationInteractor } val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } val keyguardBouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository } diff --git a/packages/SystemUI/src/com/android/systemui/brightness/data/model/LinearBrightness.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt index 608f301da85d..c4365c9093d2 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/data/model/LinearBrightness.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt @@ -14,17 +14,15 @@ * limitations under the License. */ -package com.android.systemui.brightness.data.model +package com.android.systemui.mediaprojection.data.repository -@JvmInline -value class LinearBrightness(val floatValue: Float) { - fun clamp(min: LinearBrightness, max: LinearBrightness): LinearBrightness { - return if (floatValue < min.floatValue) { - min - } else if (floatValue > max.floatValue) { - max - } else { - this - } - } +import android.app.ActivityManager +import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMediaProjectionRepository : MediaProjectionRepository { + override suspend fun switchProjectedTask(task: ActivityManager.RunningTaskInfo) {} + + override val mediaProjectionState: MutableStateFlow<MediaProjectionState> = + MutableStateFlow(MediaProjectionState.NotProjecting) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt new file mode 100644 index 000000000000..f253e949375e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.mediaprojection.data.repository + +import android.os.Handler +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.mediaprojection.taskswitcher.activityTaskManagerTasksRepository +import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager + +val Kosmos.fakeMediaProjectionRepository: FakeMediaProjectionRepository by + Kosmos.Fixture { FakeMediaProjectionRepository() } + +val Kosmos.realMediaProjectionRepository by + Kosmos.Fixture { + MediaProjectionManagerRepository( + mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, + handler = Handler.getMain(), + applicationScope = applicationCoroutineScope, + tasksRepository = activityTaskManagerTasksRepository, + backgroundDispatcher = testDispatcher, + mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt index 5b1f95a654cb..5acadd7f192a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt @@ -16,11 +16,10 @@ package com.android.systemui.mediaprojection.taskswitcher -import android.os.Handler import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.mediaprojection.data.repository.MediaProjectionManagerRepository +import com.android.systemui.mediaprojection.data.repository.realMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel @@ -40,21 +39,9 @@ val Kosmos.activityTaskManagerTasksRepository by ) } -val Kosmos.mediaProjectionManagerRepository by - Kosmos.Fixture { - MediaProjectionManagerRepository( - mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager, - handler = Handler.getMain(), - applicationScope = applicationCoroutineScope, - tasksRepository = activityTaskManagerTasksRepository, - backgroundDispatcher = testDispatcher, - mediaProjectionServiceHelper = fakeMediaProjectionManager.helper, - ) - } - val Kosmos.taskSwitcherInteractor by Kosmos.Fixture { - TaskSwitchInteractor(mediaProjectionManagerRepository, activityTaskManagerTasksRepository) + TaskSwitchInteractor(realMediaProjectionRepository, activityTaskManagerTasksRepository) } val Kosmos.taskSwitcherViewModel by diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt index 604c16fd9e74..5ff44e5d33c5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt @@ -17,6 +17,8 @@ package com.android.systemui.qs.pipeline.data.repository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.retail.data.repository.FakeRetailModeRepository +import com.android.systemui.retail.data.repository.RetailModeRepository /** This fake uses 0 as the minimum number of tiles. That means that no tiles is a valid state. */ var Kosmos.fakeMinimumTilesRepository by Kosmos.Fixture { MinimumTilesFixedRepository(0) } @@ -46,3 +48,6 @@ var Kosmos.installedTilesRepository: InstalledTilesComponentRepository by val Kosmos.fakeCustomTileAddedRepository by Kosmos.Fixture { FakeCustomTileAddedRepository() } var Kosmos.customTileAddedRepository: CustomTileAddedRepository by Kosmos.Fixture { fakeCustomTileAddedRepository } + +val Kosmos.fakeRetailModeRepository by Kosmos.Fixture { FakeRetailModeRepository() } +var Kosmos.retailModeRepository: RetailModeRepository by Kosmos.Fixture { fakeRetailModeRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt index b870039982f1..d97a5b2bede2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.qs.external.tileLifecycleManagerFactory import com.android.systemui.qs.pipeline.data.repository.customTileAddedRepository import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository import com.android.systemui.qs.pipeline.data.repository.minimumTilesRepository +import com.android.systemui.qs.pipeline.data.repository.retailModeRepository import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository import com.android.systemui.qs.pipeline.shared.logging.qsLogger import com.android.systemui.qs.pipeline.shared.pipelineFlagsRepository @@ -39,6 +40,7 @@ val Kosmos.currentTilesInteractor: CurrentTilesInteractor by installedTilesRepository, userRepository, minimumTilesRepository, + retailModeRepository, customTileStatePersister, { newQSTileFactory }, qsTileFactory, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt index 9e02df9c3f41..88bde2ed5d8f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt @@ -19,7 +19,9 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope +import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.screenrecord.data.repository.screenRecordRepository +import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor import com.android.systemui.util.time.fakeSystemClock @@ -32,6 +34,15 @@ val Kosmos.screenRecordChipInteractor: ScreenRecordChipInteractor by ) } +val Kosmos.mediaProjectionChipInteractor: MediaProjectionChipInteractor by + Kosmos.Fixture { + MediaProjectionChipInteractor( + scope = applicationCoroutineScope, + mediaProjectionRepository = fakeMediaProjectionRepository, + systemClock = fakeSystemClock, + ) + } + val Kosmos.callChipInteractor: FakeCallChipInteractor by Kosmos.Fixture { FakeCallChipInteractor() } val Kosmos.ongoingActivityChipsViewModel: OngoingActivityChipsViewModel by @@ -39,6 +50,7 @@ val Kosmos.ongoingActivityChipsViewModel: OngoingActivityChipsViewModel by OngoingActivityChipsViewModel( testScope.backgroundScope, screenRecordChipInteractor = screenRecordChipInteractor, + mediaProjectionChipInteractor = mediaProjectionChipInteractor, callChipInteractor = callChipInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerKosmos.kt index 24d2c2f5dae1..569429f180df 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerKosmos.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.android.systemui.keyguard.ui.binder +package com.android.systemui.statusbar.notification.stack -import android.os.fakeExecutorHandler import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock -val Kosmos.keyguardBlueprintViewBinder by - Kosmos.Fixture { KeyguardBlueprintViewBinder(fakeExecutorHandler) } +val Kosmos.notificationStackScrollLayoutController by + Kosmos.Fixture { mock<NotificationStackScrollLayoutController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt new file mode 100644 index 000000000000..15ef26d58ece --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.data.repository + +import android.annotation.SuppressLint +import android.media.AudioDeviceInfo +import android.media.AudioDevicePort + +@SuppressLint("VisibleForTests") +object TestAudioDevicesFactory { + + fun builtInDevice(deviceName: String = "built_in"): AudioDeviceInfo { + return AudioDeviceInfo( + AudioDevicePort.createForTesting( + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + deviceName, + "", + ) + ) + } + + fun wiredDevice( + deviceName: String = "wired", + deviceAddress: String = "card=1;device=0", + ): AudioDeviceInfo { + return AudioDeviceInfo( + AudioDevicePort.createForTesting( + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, + deviceName, + deviceAddress, + ) + ) + } + + fun bluetoothDevice( + deviceName: String = "bt", + deviceAddress: String = "00:43:A8:23:10:F0", + ): AudioDeviceInfo { + return AudioDeviceInfo( + AudioDevicePort.createForTesting( + AudioDeviceInfo.TYPE_BLE_HEADSET, + deviceName, + deviceAddress, + ) + ) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt index 63c3ee55ef40..3f51a790aade 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt @@ -22,7 +22,6 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.volume.data.repository.audioRepository import com.android.systemui.volume.data.repository.audioSharingRepository -import com.android.systemui.volume.localMediaRepositoryFactory import com.android.systemui.volume.mediaOutputInteractor val Kosmos.audioOutputInteractor by @@ -36,7 +35,6 @@ val Kosmos.audioOutputInteractor by bluetoothAdapter, deviceIconInteractor, mediaOutputInteractor, - localMediaRepositoryFactory, audioSharingRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/captioning/CaptioningModuleKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/captioning/CaptioningModuleKosmos.kt new file mode 100644 index 000000000000..e7162d27a031 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/captioning/CaptioningModuleKosmos.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.captioning + +import com.android.internal.logging.uiEventLogger +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.view.accessibility.data.repository.captioningInteractor +import com.android.systemui.volume.panel.component.button.ui.composable.ToggleButtonComponent +import com.android.systemui.volume.panel.component.captioning.domain.CaptioningAvailabilityCriteria +import com.android.systemui.volume.panel.component.captioning.ui.viewmodel.captioningViewModel + +val Kosmos.captioningComponent by + Kosmos.Fixture { + ToggleButtonComponent( + captioningViewModel.buttonViewModel, + captioningViewModel::setIsSystemAudioCaptioningEnabled, + ) + } +val Kosmos.captioningAvailabilityCriteria by + Kosmos.Fixture { + CaptioningAvailabilityCriteria( + captioningInteractor, + testScope.backgroundScope, + uiEventLogger, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModelKosmos.kt new file mode 100644 index 000000000000..0edd9e026912 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModelKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.captioning.ui.viewmodel + +import android.content.applicationContext +import com.android.internal.logging.uiEventLogger +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.view.accessibility.data.repository.captioningInteractor + +val Kosmos.captioningViewModel by + Kosmos.Fixture { + CaptioningViewModel( + applicationContext, + captioningInteractor, + testScope.backgroundScope, + uiEventLogger, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt index 9c902cf57fde..680535dfa909 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.component.mediaoutput.data.repository import com.android.settingslib.volume.data.repository.LocalMediaRepository +import kotlinx.coroutines.CoroutineScope class FakeLocalMediaRepositoryFactory(private val defaultProvider: () -> LocalMediaRepository) : LocalMediaRepositoryFactory { @@ -27,6 +28,8 @@ class FakeLocalMediaRepositoryFactory(private val defaultProvider: () -> LocalMe repositories[packageName] = localMediaRepository } - override fun create(packageName: String?): LocalMediaRepository = - repositories[packageName] ?: defaultProvider() + override fun create( + packageName: String?, + coroutineScope: CoroutineScope + ): LocalMediaRepository = repositories[packageName] ?: defaultProvider() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt index 40296099bfe0..141f2426f365 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interacto import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice +import android.graphics.drawable.Drawable import android.graphics.drawable.TestStubDrawable import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.media.BluetoothMediaDevice @@ -29,29 +30,44 @@ import com.android.systemui.util.mockito.whenever @SuppressLint("StaticFieldLeak") // These are mocks object TestMediaDevicesFactory { - fun builtInMediaDevice(): MediaDevice = mock { - whenever(name).thenReturn("built_in_media") - whenever(icon).thenReturn(TestStubDrawable()) + fun builtInMediaDevice( + deviceName: String = "built_in_media", + deviceIcon: Drawable? = TestStubDrawable(), + ): MediaDevice = mock { + whenever(name).thenReturn(deviceName) + whenever(icon).thenReturn(deviceIcon) } - fun wiredMediaDevice(): MediaDevice = + fun wiredMediaDevice( + deviceName: String = "wired_media", + deviceIcon: Drawable? = TestStubDrawable(), + ): MediaDevice = mock<PhoneMediaDevice> { whenever(deviceType) .thenReturn(MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE) - whenever(name).thenReturn("wired_media") - whenever(icon).thenReturn(TestStubDrawable()) + whenever(name).thenReturn(deviceName) + whenever(icon).thenReturn(deviceIcon) } - fun bluetoothMediaDevice(): MediaDevice { - val bluetoothDevice = mock<BluetoothDevice>() + fun bluetoothMediaDevice( + deviceName: String = "bt_media", + deviceIcon: Drawable? = TestStubDrawable(), + deviceAddress: String = "bt_media_device", + ): BluetoothMediaDevice { + val bluetoothDevice = + mock<BluetoothDevice> { + whenever(name).thenReturn(deviceName) + whenever(address).thenReturn(deviceAddress) + } val cachedBluetoothDevice: CachedBluetoothDevice = mock { whenever(isHearingAidDevice).thenReturn(true) - whenever(address).thenReturn("bt_media_device") + whenever(address).thenReturn(deviceAddress) whenever(device).thenReturn(bluetoothDevice) + whenever(name).thenReturn(deviceName) } return mock<BluetoothMediaDevice> { - whenever(name).thenReturn("bt_media") - whenever(icon).thenReturn(TestStubDrawable()) + whenever(name).thenReturn(deviceName) + whenever(icon).thenReturn(deviceIcon) whenever(cachedDevice).thenReturn(cachedBluetoothDevice) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/spatial/SpatialAudioModuleKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/spatial/SpatialAudioModuleKosmos.kt new file mode 100644 index 000000000000..ea5d70d35030 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/spatial/SpatialAudioModuleKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.spatial + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.panel.component.button.ui.composable.ButtonComponent +import com.android.systemui.volume.panel.component.spatial.domain.SpatialAudioAvailabilityCriteria +import com.android.systemui.volume.panel.component.spatial.domain.interactor.spatialAudioComponentInteractor +import com.android.systemui.volume.panel.component.spatial.ui.viewmodel.spatialAudioViewModel + +val Kosmos.spatialAudioComponent by + Kosmos.Fixture { ButtonComponent(spatialAudioViewModel.spatialAudioButton) { _, _ -> } } +val Kosmos.spatialAudioAvailabilityCriteria by + Kosmos.Fixture { SpatialAudioAvailabilityCriteria(spatialAudioComponentInteractor) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorKosmos.kt new file mode 100644 index 000000000000..95a7b9bb185b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.spatial.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.spatializerInteractor +import com.android.systemui.volume.domain.interactor.audioOutputInteractor + +val Kosmos.spatialAudioComponentInteractor by + Kosmos.Fixture { + SpatialAudioComponentInteractor( + audioOutputInteractor, + spatializerInteractor, + testScope.backgroundScope, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModelKosmos.kt new file mode 100644 index 000000000000..1b8a3fcfd311 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModelKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.spatial.ui.viewmodel + +import android.content.applicationContext +import com.android.internal.logging.uiEventLogger +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.volume.panel.component.spatial.domain.interactor.spatialAudioComponentInteractor +import com.android.systemui.volume.panel.component.spatial.spatialAudioAvailabilityCriteria + +val Kosmos.spatialAudioViewModel by + Kosmos.Fixture { + SpatialAudioViewModel( + applicationContext, + testScope.backgroundScope, + spatialAudioAvailabilityCriteria, + spatialAudioComponentInteractor, + uiEventLogger, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt index 8862942aa083..a18f498e5441 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt @@ -19,8 +19,10 @@ package com.android.systemui.volume.panel.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.volume.panel.component.bottombar.ui.bottomBarAvailabilityCriteria +import com.android.systemui.volume.panel.component.captioning.captioningAvailabilityCriteria import com.android.systemui.volume.panel.component.mediaoutput.mediaOutputAvailabilityCriteria import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents +import com.android.systemui.volume.panel.component.spatial.spatialAudioAvailabilityCriteria import com.android.systemui.volume.panel.component.volume.volumeSlidersAvailabilityCriteria import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.domain.defaultCriteria @@ -36,6 +38,8 @@ var Kosmos.prodCriteriaByKey: mapOf( VolumePanelComponents.MEDIA_OUTPUT to Provider { mediaOutputAvailabilityCriteria }, VolumePanelComponents.VOLUME_SLIDERS to Provider { volumeSlidersAvailabilityCriteria }, + VolumePanelComponents.CAPTIONING to Provider { captioningAvailabilityCriteria }, + VolumePanelComponents.SPATIAL_AUDIO to Provider { spatialAudioAvailabilityCriteria }, VolumePanelComponents.BOTTOM_BAR to Provider { bottomBarAvailabilityCriteria }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactoryKosmos.kt index bacf22c0fef6..6bea416fa6a0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactoryKosmos.kt @@ -18,8 +18,10 @@ package com.android.systemui.volume.panel.ui.composable import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.panel.component.bottombar.ui.bottomBarComponent +import com.android.systemui.volume.panel.component.captioning.captioningComponent import com.android.systemui.volume.panel.component.mediaoutput.mediaOutputComponent import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents +import com.android.systemui.volume.panel.component.spatial.spatialAudioComponent import com.android.systemui.volume.panel.component.volume.volumeSlidersComponent import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent @@ -30,9 +32,11 @@ var Kosmos.componentByKey: Map<VolumePanelComponentKey, Provider<VolumePanelUiCo var Kosmos.prodComponentByKey: Map<VolumePanelComponentKey, Provider<VolumePanelUiComponent>> by Kosmos.Fixture { mapOf( - VolumePanelComponents.BOTTOM_BAR to Provider { bottomBarComponent }, VolumePanelComponents.MEDIA_OUTPUT to Provider { mediaOutputComponent }, VolumePanelComponents.VOLUME_SLIDERS to Provider { volumeSlidersComponent }, + VolumePanelComponents.CAPTIONING to Provider { captioningComponent }, + VolumePanelComponents.SPATIAL_AUDIO to Provider { spatialAudioComponent }, + VolumePanelComponents.BOTTOM_BAR to Provider { bottomBarComponent }, ) } var Kosmos.enabledComponents: Collection<VolumePanelComponentKey> by diff --git a/packages/services/VirtualCamera/OWNERS b/packages/services/VirtualCamera/OWNERS deleted file mode 100644 index c66443fb8a14..000000000000 --- a/packages/services/VirtualCamera/OWNERS +++ /dev/null @@ -1,3 +0,0 @@ -include /services/companion/java/com/android/server/companion/virtual/OWNERS -caen@google.com -jsebechlebsky@google.com
\ No newline at end of file diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 42f168bb4a6b..77decb6a52fa 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -1379,6 +1379,30 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } } + @RequiresNoPermission + @Override + public boolean isMagnificationSystemUIConnected() { + if (svcConnTracingEnabled()) { + logTraceSvcConn("isMagnificationSystemUIConnected", ""); + } + synchronized (mLock) { + if (!hasRightsToCurrentUserLocked()) { + return false; + } + if (!mSecurityPolicy.canControlMagnification(this)) { + return false; + } + final long identity = Binder.clearCallingIdentity(); + try { + MagnificationProcessor magnificationProcessor = + mSystemSupport.getMagnificationProcessor(); + return magnificationProcessor.isMagnificationSystemUIConnected(); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + public boolean isMagnificationCallbackEnabled(int displayId) { return mInvocationHandler.isMagnificationCallbackEnabled(displayId); } @@ -1925,6 +1949,11 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ InvocationHandler.MSG_CLEAR_ACCESSIBILITY_CACHE); } + public void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) { + mInvocationHandler + .notifyMagnificationSystemUIConnectionChangedLocked(connected); + } + public void notifyMagnificationChangedLocked(int displayId, @NonNull Region region, @NonNull MagnificationConfig config) { mInvocationHandler @@ -1976,6 +2005,21 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ return (mGenericMotionEventSources & eventSourceWithoutClass) != 0; } + /** + * Called by the invocation handler to notify the service that the + * magnification systemui connection has changed. + */ + private void notifyMagnificationSystemUIConnectionChangedInternal(boolean connected) { + final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); + if (listener != null) { + try { + listener.onMagnificationSystemUIConnectionChanged(connected); + } catch (RemoteException re) { + Slog.e(LOG_TAG, + "Error sending magnification sysui connection changes to " + mService, re); + } + } + } /** * Called by the invocation handler to notify the service that the @@ -2372,6 +2416,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ private static final int MSG_BIND_INPUT = 12; private static final int MSG_UNBIND_INPUT = 13; private static final int MSG_START_INPUT = 14; + private static final int MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED = 15; /** List of magnification callback states, mapping from displayId -> Boolean */ @GuardedBy("mlock") @@ -2398,6 +2443,13 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ notifyClearAccessibilityCacheInternal(); } break; + case MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED: { + final SomeArgs args = (SomeArgs) message.obj; + final boolean connected = args.argi1 == 1; + notifyMagnificationSystemUIConnectionChangedInternal(connected); + args.recycle(); + } break; + case MSG_ON_MAGNIFICATION_CHANGED: { final SomeArgs args = (SomeArgs) message.obj; final Region region = (Region) args.arg1; @@ -2455,6 +2507,15 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } } + public void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) { + final SomeArgs args = SomeArgs.obtain(); + args.argi1 = connected ? 1 : 0; + + final Message msg = + obtainMessage(MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED, args); + msg.sendToTarget(); + } + public void notifyMagnificationChangedLocked(int displayId, @NonNull Region region, @NonNull MagnificationConfig config) { synchronized (mLock) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 20b727cd6f09..fe083389fa77 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -1812,6 +1812,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** + * Called by the MagnificationController when the magnification systemui connection changes. + * + * @param connected Whether the connection is ready. + */ + public void notifyMagnificationSystemUIConnectionChanged(boolean connected) { + synchronized (mLock) { + notifyMagnificationSystemUIConnectionChangedLocked(connected); + } + } + + /** * Called by the MagnificationController when the state of display * magnification changes. * @@ -2243,6 +2254,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mProxyManager.clearCacheLocked(); } + private void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) { + final AccessibilityUserState state = getCurrentUserStateLocked(); + for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { + final AccessibilityServiceConnection service = state.mBoundServices.get(i); + service.notifyMagnificationSystemUIConnectionChangedLocked(connected); + } + } + private void notifyMagnificationChangedLocked(int displayId, @NonNull Region region, @NonNull MagnificationConfig config) { final AccessibilityUserState state = getCurrentUserStateLocked(); diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java index 4cb3d247edb0..420bac759ea6 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java @@ -489,6 +489,14 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon /** @throws UnsupportedOperationException since a proxy does not need magnification */ @RequiresNoPermission @Override + public boolean isMagnificationSystemUIConnected() throws UnsupportedOperationException { + throw new UnsupportedOperationException("isMagnificationSystemUIConnected is not" + + " supported"); + } + + /** @throws UnsupportedOperationException since a proxy does not need magnification */ + @RequiresNoPermission + @Override public boolean isMagnificationCallbackEnabled(int displayId) { throw new UnsupportedOperationException("isMagnificationCallbackEnabled is not supported"); } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java index 0719ebaba707..7f4c808b2251 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java @@ -126,6 +126,7 @@ public class MagnificationConnectionManager implements @ConnectionState private int mConnectionState = DISCONNECTED; + ConnectionStateChangedCallback mConnectionStateChangedCallback = null; private static final int WAIT_CONNECTION_TIMEOUT_MILLIS = 100; @@ -264,6 +265,9 @@ public class MagnificationConnectionManager implements } } } + if (mConnectionStateChangedCallback != null) { + mConnectionStateChangedCallback.onConnectionStateChanged(connection != null); + } } /** @@ -271,7 +275,7 @@ public class MagnificationConnectionManager implements */ public boolean isConnected() { synchronized (mLock) { - return mConnectionWrapper != null; + return mConnectionWrapper != null && mConnectionState == CONNECTED; } } @@ -1344,4 +1348,8 @@ public class MagnificationConnectionManager implements } } } + + interface ConnectionStateChangedCallback { + void onConnectionStateChanged(boolean connected); + } } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index 76367a2b11c3..9b7884711a6d 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -828,6 +828,8 @@ public class MagnificationController implements MagnificationConnectionManager.C mMagnificationConnectionManager = new MagnificationConnectionManager(mContext, mLock, this, mAms.getTraceManager(), mScaleProvider); + mMagnificationConnectionManager.mConnectionStateChangedCallback = + mAms::notifyMagnificationSystemUIConnectionChanged; } return mMagnificationConnectionManager; } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java index ed8f1ab3a1b2..603683906d06 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java @@ -147,6 +147,10 @@ public class MagnificationProcessor { return false; } + public boolean isMagnificationSystemUIConnected() { + return mController.getMagnificationConnectionManager().isConnected(); + } + private boolean setScaleAndCenterForFullScreenMagnification(int displayId, float scale, float centerX, float centerY, boolean animate, int id) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 215f6402fa76..4a9900763a94 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -29,6 +29,7 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOAR import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery; +import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix; import android.annotation.EnforcePermission; import android.annotation.NonNull; @@ -1478,7 +1479,13 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub synchronized (mVirtualDeviceLock) { boolean hasInterceptedIntent = false; for (Map.Entry<IBinder, IntentFilter> interceptor : mIntentInterceptors.entrySet()) { - if (interceptor.getValue().match( + IntentFilter intentFilter = interceptor.getValue(); + // Explicitly match the actions because the intent filter will match any intent + // without an explicit action. If the intent has no action, then require that there + // are no actions specified in the filter either. + boolean explicitActionMatch = !intentInterceptionActionMatchingFix() + || intent.getAction() != null || intentFilter.countActions() == 0; + if (explicitActionMatch && intentFilter.match( intent.getAction(), intent.getType(), intent.getScheme(), intent.getData(), intent.getCategories(), TAG) >= 0) { try { diff --git a/services/core/Android.bp b/services/core/Android.bp index 0fdf6d0fd507..f1339e91d3d4 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -232,7 +232,6 @@ java_library_static { "android.hardware.rebootescrow-V1-java", "android.hardware.power.stats-V2-java", "android.hidl.manager-V1.2-java", - "audio-permission-aidl-java", "cbor-java", "com.android.media.audio-aconfig-java", "icu4j_calendar_astronomer", diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 8647750d510f..ab34dd4477fd 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -2205,12 +2205,15 @@ public class OomAdjuster { != 0 ? PROCESS_CAPABILITY_FOREGROUND_LOCATION : 0; if (roForegroundAudioControl()) { // flag check - final int fgsAudioType = FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK - | FOREGROUND_SERVICE_TYPE_CAMERA - | FOREGROUND_SERVICE_TYPE_MICROPHONE - | FOREGROUND_SERVICE_TYPE_PHONE_CALL; - capabilityFromFGS |= (psr.getForegroundServiceTypes() & fgsAudioType) != 0 - ? PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL : 0; + // TODO revisit restriction of FOREGROUND_AUDIO_CONTROL when it can be + // limited to specific FGS types + //final int fgsAudioType = FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + // | FOREGROUND_SERVICE_TYPE_CAMERA + // | FOREGROUND_SERVICE_TYPE_MICROPHONE + // | FOREGROUND_SERVICE_TYPE_PHONE_CALL; + //capabilityFromFGS |= (psr.getForegroundServiceTypes() & fgsAudioType) != 0 + // ? PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL : 0; + capabilityFromFGS |= PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL; } final boolean enabled = state.getCachedCompatChange( diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 8d7a1c9f8228..8eef71e603b2 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -22,6 +22,8 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; +import static android.os.Process.ROOT_UID; +import static android.os.Process.SYSTEM_UID; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -422,6 +424,10 @@ public final class PendingIntentRecord extends IIntentSender.Stub { }) public static BackgroundStartPrivileges getDefaultBackgroundStartPrivileges( int callingUid, @Nullable String callingPackage) { + if (callingUid == ROOT_UID || callingUid == SYSTEM_UID) { + // root and system must always opt in explicitly + return BackgroundStartPrivileges.ALLOW_FGS; + } boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled( DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage, UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled( diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 1dc1846fbb96..1d21ccb62b8c 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -15,6 +15,8 @@ */ package com.android.server.audio; +import static android.media.audio.Flags.scoManagedByAudio; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.compat.CompatChanges; @@ -54,6 +56,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; +import android.sysprop.BluetoothProperties; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -74,7 +77,6 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; - /** * @hide * (non final for mocking/spying) @@ -167,6 +169,15 @@ public class AudioDeviceBroker { @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.S_V2) public static final long USE_SET_COMMUNICATION_DEVICE = 243827847L; + /** Indicates if headset profile connection and SCO audio control use the new implementation + * aligned with other BT profiles. True if both the feature flag Flags.scoManagedByAudio() and + * the system property audio.sco.managed.by.audio are true. + */ + private final boolean mScoManagedByAudio; + /*package*/ boolean isScoManagedByAudio() { + return mScoManagedByAudio; + } + //------------------------------------------------------------------- /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service, @NonNull AudioSystemAdapter audioSystem) { @@ -176,7 +187,8 @@ public class AudioDeviceBroker { mDeviceInventory = new AudioDeviceInventory(this); mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext); mAudioSystem = audioSystem; - + mScoManagedByAudio = scoManagedByAudio() + && BluetoothProperties.isScoManagedByAudioEnabled().orElse(false); init(); } @@ -192,7 +204,8 @@ public class AudioDeviceBroker { mDeviceInventory = mockDeviceInventory; mSystemServer = mockSystemServer; mAudioSystem = audioSystem; - + mScoManagedByAudio = scoManagedByAudio() + && BluetoothProperties.isScoManagedByAudioEnabled().orElse(false); init(); } @@ -400,24 +413,24 @@ public class AudioDeviceBroker { if (client == null) { return; } - - boolean isBtScoRequested = isBluetoothScoRequested(); - if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) { - if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) { - Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for uid: " - + uid); - // clean up or restore previous client selection - if (prevClientDevice != null) { - addCommunicationRouteClient(cb, uid, prevClientDevice, prevPrivileged); - } else { - removeCommunicationRouteClient(cb, true); + if (!mScoManagedByAudio) { + boolean isBtScoRequested = isBluetoothScoRequested(); + if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) { + if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) { + Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for uid: " + + uid); + // clean up or restore previous client selection + if (prevClientDevice != null) { + addCommunicationRouteClient(cb, uid, prevClientDevice, prevPrivileged); + } else { + removeCommunicationRouteClient(cb, true); + } + postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); } - postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); + } else if (!isBtScoRequested && wasBtScoRequested) { + mBtHelper.stopBluetoothSco(eventSource); } - } else if (!isBtScoRequested && wasBtScoRequested) { - mBtHelper.stopBluetoothSco(eventSource); } - // In BT classic for communication, the device changes from a2dp to sco device, but for // LE Audio it stays the same and we must trigger the proper stream volume alignment, if // LE Audio communication device is activated after the audio system has already switched to @@ -1685,6 +1698,8 @@ public class AudioDeviceBroker { pw.println("\n" + prefix + "mAudioModeOwner: " + mAudioModeOwner); + pw.println("\n" + prefix + "mScoManagedByAudio: " + mScoManagedByAudio); + mBtHelper.dump(pw, prefix); } @@ -1837,10 +1852,10 @@ public class AudioDeviceBroker { ? mAudioService.getBluetoothContextualVolumeStream() : AudioSystem.STREAM_DEFAULT); if (btInfo.mProfile == BluetoothProfile.LE_AUDIO - || btInfo.mProfile - == BluetoothProfile.HEARING_AID) { - onUpdateCommunicationRouteClient( - isBluetoothScoRequested(), + || btInfo.mProfile == BluetoothProfile.HEARING_AID + || (mScoManagedByAudio + && btInfo.mProfile == BluetoothProfile.HEADSET)) { + onUpdateCommunicationRouteClient(isBluetoothScoRequested(), "setBluetoothActiveDevice"); } } @@ -2511,7 +2526,7 @@ public class AudioDeviceBroker { setCommunicationRouteForClient(crc.getBinder(), crc.getUid(), crc.getDevice(), BtHelper.SCO_MODE_UNDEFINED, crc.isPrivileged(), eventSource); } else { - if (!isBluetoothScoRequested() && wasBtScoRequested) { + if (!mScoManagedByAudio && !isBluetoothScoRequested() && wasBtScoRequested) { mBtHelper.stopBluetoothSco(eventSource); } updateCommunicationRoute(eventSource); @@ -2815,4 +2830,5 @@ public class AudioDeviceBroker { void clearDeviceInventory() { mDeviceInventory.clearDeviceInventory(); } + } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index e0790da7cd09..287c92f86f0f 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -859,6 +859,15 @@ public class AudioDeviceInventory { btInfo, streamType, codec, "onSetBtActiveDevice"); } break; + case BluetoothProfile.HEADSET: + if (mDeviceBroker.isScoManagedByAudio()) { + if (switchToUnavailable) { + mDeviceBroker.onSetBtScoActiveDevice(null); + } else if (switchToAvailable) { + mDeviceBroker.onSetBtScoActiveDevice(btInfo.mDevice); + } + } + break; default: throw new IllegalArgumentException("Invalid profile " + BluetoothProfile.getProfileName(btInfo.mProfile)); } diff --git a/services/core/java/com/android/server/audio/AudioPolicyFacade.java b/services/core/java/com/android/server/audio/AudioPolicyFacade.java index 02e80d611f3f..f652b33b3fd3 100644 --- a/services/core/java/com/android/server/audio/AudioPolicyFacade.java +++ b/services/core/java/com/android/server/audio/AudioPolicyFacade.java @@ -16,12 +16,14 @@ package com.android.server.audio; +import com.android.media.permission.INativePermissionController; /** * Facade to IAudioPolicyService which fulfills AudioService dependencies. * See @link{IAudioPolicyService.aidl} */ public interface AudioPolicyFacade { - public boolean isHotwordStreamSupported(boolean lookbackAudio); + public INativePermissionController getPermissionController(); + public void registerOnStartTask(Runnable r); } diff --git a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java new file mode 100644 index 000000000000..5ea3c4bf538d --- /dev/null +++ b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.audio; + +import android.annotation.Nullable; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.ArraySet; + +import com.android.internal.annotations.GuardedBy; +import com.android.media.permission.INativePermissionController; +import com.android.media.permission.UidPackageState; +import com.android.server.pm.pkg.PackageState; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +/** Responsible for synchronizing system server permission state to the native audioserver. */ +public class AudioServerPermissionProvider { + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private INativePermissionController mDest; + + @GuardedBy("mLock") + private final Map<Integer, Set<String>> mPackageMap; + + /** + * @param appInfos - PackageState for all apps on the device, used to populate init state + */ + public AudioServerPermissionProvider(Collection<PackageState> appInfos) { + // Initialize the package state + mPackageMap = generatePackageMappings(appInfos); + } + + /** + * Called whenever audioserver starts (or started before us) + * + * @param pc - The permission controller interface from audioserver, which we push updates to + */ + public void onServiceStart(@Nullable INativePermissionController pc) { + if (pc == null) return; + synchronized (mLock) { + mDest = pc; + resetNativePackageState(); + } + } + + /** + * Called when a package is added or removed + * + * @param uid - uid of modified package (only app-id matters) + * @param packageName - the (new) packageName + * @param isRemove - true if the package is being removed, false if it is being added + */ + public void onModifyPackageState(int uid, String packageName, boolean isRemove) { + // No point in maintaining package mappings for uids of different users + uid = UserHandle.getAppId(uid); + synchronized (mLock) { + // Update state + Set<String> packages; + if (!isRemove) { + packages = mPackageMap.computeIfAbsent(uid, unused -> new ArraySet(1)); + packages.add(packageName); + } else { + packages = mPackageMap.get(uid); + if (packages != null) { + packages.remove(packageName); + if (packages.isEmpty()) mPackageMap.remove(uid); + } + } + // Push state to destination + if (mDest == null) { + return; + } + var state = new UidPackageState(); + state.uid = uid; + state.packageNames = packages != null ? List.copyOf(packages) : Collections.emptyList(); + try { + mDest.updatePackagesForUid(state); + } catch (RemoteException e) { + // We will re-init the state when the service comes back up + mDest = null; + } + } + } + + /** Called when full syncing package state to audioserver. */ + @GuardedBy("mLock") + private void resetNativePackageState() { + if (mDest == null) return; + List<UidPackageState> states = + mPackageMap.entrySet().stream() + .map( + entry -> { + UidPackageState state = new UidPackageState(); + state.uid = entry.getKey(); + state.packageNames = List.copyOf(entry.getValue()); + return state; + }) + .toList(); + try { + mDest.populatePackagesForUids(states); + } catch (RemoteException e) { + // We will re-init the state when the service comes back up + mDest = null; + } + } + + /** + * Aggregation operation on all package states list: groups by states by app-id and merges the + * packageName for each state into an ArraySet. + */ + private static Map<Integer, Set<String>> generatePackageMappings( + Collection<PackageState> appInfos) { + Collector<PackageState, Object, Set<String>> reducer = + Collectors.mapping( + (PackageState p) -> p.getPackageName(), + Collectors.toCollection(() -> new ArraySet(1))); + + return appInfos.stream() + .collect( + Collectors.groupingBy( + /* predicate */ (PackageState p) -> p.getAppId(), + /* factory */ HashMap::new, + /* downstream collector */ reducer)); + } +} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 2addf6f9ec96..ef65b2523024 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -31,6 +31,10 @@ import static android.Manifest.permission.MODIFY_PHONE_STATE; import static android.Manifest.permission.QUERY_AUDIO_STATE; import static android.Manifest.permission.WRITE_SETTINGS; import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT; +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static android.content.Intent.EXTRA_ARCHIVAL; +import static android.content.Intent.EXTRA_REPLACING; import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET; import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER; import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP; @@ -48,6 +52,7 @@ import static android.media.audio.Flags.automaticBtDeviceType; import static android.media.audio.Flags.featureSpatialAudioHeadtrackingLowLatency; import static android.media.audio.Flags.focusFreezeTestApi; import static android.media.audio.Flags.roForegroundAudioControl; +import static android.media.audio.Flags.scoManagedByAudio; import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration; import static android.os.Process.FIRST_APPLICATION_UID; import static android.os.Process.INVALID_UID; @@ -56,7 +61,9 @@ 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.absVolumeIndexFix; import static com.android.media.audio.Flags.alarmMinVolumeZero; +import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; import static com.android.media.audio.Flags.setStreamVolumeOrder; @@ -238,15 +245,18 @@ import com.android.internal.os.SomeArgs; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; +import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent; import com.android.server.audio.AudioServiceEvents.PhoneStateEvent; import com.android.server.audio.AudioServiceEvents.VolChangedBroadcastEvent; import com.android.server.audio.AudioServiceEvents.VolumeEvent; +import com.android.server.pm.PackageManagerLocal; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerInternal.UserRestrictionsListener; import com.android.server.pm.UserManagerService; +import com.android.server.pm.pkg.PackageState; import com.android.server.utils.EventLogger; import com.android.server.wm.ActivityTaskManagerInternal; @@ -271,6 +281,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; @@ -301,6 +312,8 @@ public class AudioService extends IAudioService.Stub private final SettingsAdapter mSettings; private final AudioPolicyFacade mAudioPolicy; + private final AudioServerPermissionProvider mPermissionProvider; + private final MusicFxHelper mMusicFxHelper; /** Debug audio mode */ @@ -631,6 +644,17 @@ public class AudioService extends IAudioService.Stub // If absolute volume is supported in AVRCP device private volatile boolean mAvrcpAbsVolSupported = false; + private final Object mCachedAbsVolDrivingStreamsLock = new Object(); + // Contains for all the device types which support absolute volume the current streams that + // are driving the volume changes + @GuardedBy("mCachedAbsVolDrivingStreamsLock") + private final HashMap<Integer, Integer> mCachedAbsVolDrivingStreams = new HashMap<>( + Map.of(AudioSystem.DEVICE_OUT_BLE_HEADSET, AudioSystem.STREAM_MUSIC, + AudioSystem.DEVICE_OUT_BLE_SPEAKER, AudioSystem.STREAM_MUSIC, + AudioSystem.DEVICE_OUT_BLE_BROADCAST, AudioSystem.STREAM_MUSIC, + AudioSystem.DEVICE_OUT_HEARING_AID, AudioSystem.STREAM_MUSIC + )); + /** * Default stream type used for volume control in the absence of playback * e.g. user on homescreen, no app playing anything, presses hardware volume buttons, this @@ -1008,14 +1032,22 @@ public class AudioService extends IAudioService.Stub public Lifecycle(Context context) { super(context); + var audioserverLifecycleExecutor = Executors.newSingleThreadExecutor(); + var audioPolicyFacade = new DefaultAudioPolicyFacade(audioserverLifecycleExecutor); mService = new AudioService(context, AudioSystemAdapter.getDefaultAdapter(), SystemServerAdapter.getDefaultAdapter(context), SettingsAdapter.getDefaultAdapter(), new AudioVolumeGroupHelper(), - new DefaultAudioPolicyFacade(), - null); - + audioPolicyFacade, + null, + context.getSystemService(AppOpsManager.class), + PermissionEnforcer.fromContext(context), + audioserverPermissions() ? + initializeAudioServerPermissionProvider( + context, audioPolicyFacade, audioserverLifecycleExecutor) : + null + ); } @Override @@ -1092,25 +1124,6 @@ public class AudioService extends IAudioService.Stub /** * @param context * @param audioSystem Adapter for {@link AudioSystem} - * @param systemServer Adapter for privileged functionality for system server components - * @param settings Adapter for {@link Settings} - * @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup} - * @param audioPolicy Interface of a facade to IAudioPolicyManager - * @param looper Looper to use for the service's message handler. If this is null, an - * {@link AudioSystemThread} is created as the messaging thread instead. - */ - public AudioService(Context context, AudioSystemAdapter audioSystem, - SystemServerAdapter systemServer, SettingsAdapter settings, - AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, - @Nullable Looper looper) { - this (context, audioSystem, systemServer, settings, audioVolumeGroupHelper, - audioPolicy, looper, context.getSystemService(AppOpsManager.class), - PermissionEnforcer.fromContext(context)); - } - - /** - * @param context - * @param audioSystem Adapter for {@link AudioSystem} * @param systemServer Adapter for privilieged functionality for system server components * @param settings Adapter for {@link Settings} * @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup} @@ -1124,13 +1137,16 @@ public class AudioService extends IAudioService.Stub public AudioService(Context context, AudioSystemAdapter audioSystem, SystemServerAdapter systemServer, SettingsAdapter settings, AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, - @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer) { + @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer, + /* @NonNull */ AudioServerPermissionProvider permissionProvider) { super(enforcer); sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()")); mContext = context; mContentResolver = context.getContentResolver(); mAppOps = appOps; + mPermissionProvider = permissionProvider; + mAudioSystem = audioSystem; mSystemServer = systemServer; mAudioVolumeGroupHelper = audioVolumeGroupHelper; @@ -1471,6 +1487,13 @@ public class AudioService extends IAudioService.Stub // check on volume initialization checkVolumeRangeInitialization("AudioService()"); + + synchronized (mCachedAbsVolDrivingStreamsLock) { + mCachedAbsVolDrivingStreams.forEach((dev, stream) -> { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true, + stream); + }); + } } private SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionChangedListener = @@ -1503,7 +1526,9 @@ public class AudioService extends IAudioService.Stub // Register for device connection intent broadcasts. IntentFilter intentFilter = new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); - intentFilter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED); + if (!mDeviceBroker.isScoManagedByAudio()) { + intentFilter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED); + } intentFilter.addAction(Intent.ACTION_DOCK_EVENT); if (mDisplayManager == null) { intentFilter.addAction(Intent.ACTION_SCREEN_ON); @@ -1908,6 +1933,14 @@ public class AudioService extends IAudioService.Stub } onIndicateSystemReady(); + + synchronized (mCachedAbsVolDrivingStreamsLock) { + mCachedAbsVolDrivingStreams.forEach((dev, stream) -> { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true, + stream); + }); + } + // indicate the end of reconfiguration phase to audio HAL AudioSystem.setParameters("restarting=false"); @@ -3734,8 +3767,10 @@ public class AudioService extends IAudioService.Stub int newIndex = mStreamStates[streamType].getIndex(device); + int streamToDriveAbsVol = absVolumeIndexFix() ? getBluetoothContextualVolumeStream() : + AudioSystem.STREAM_MUSIC; // Check if volume update should be send to AVRCP - if (streamTypeAlias == AudioSystem.STREAM_MUSIC + if (streamTypeAlias == streamToDriveAbsVol && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device) && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) { if (DEBUG_VOL) { @@ -4527,15 +4562,20 @@ public class AudioService extends IAudioService.Stub + featureSpatialAudioHeadtrackingLowLatency()); pw.println("\tandroid.media.audio.focusFreezeTestApi:" + focusFreezeTestApi()); + pw.println("\tcom.android.media.audio.audioserverPermissions:" + + audioserverPermissions()); pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:" + disablePrescaleAbsoluteVolume()); - pw.println("\tcom.android.media.audio.setStreamVolumeOrder:" + setStreamVolumeOrder()); pw.println("\tandroid.media.audio.roForegroundAudioControl:" + roForegroundAudioControl()); + pw.println("\tandroid.media.audio.scoManagedByAudio:" + + scoManagedByAudio()); pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:" + vgsVssSyncMuteOrder()); + pw.println("\tcom.android.media.audio.absVolumeIndexFix:" + + absVolumeIndexFix()); } private void dumpAudioMode(PrintWriter pw) { @@ -4731,7 +4771,9 @@ public class AudioService extends IAudioService.Stub } } - if (streamTypeAlias == AudioSystem.STREAM_MUSIC + int streamToDriveAbsVol = absVolumeIndexFix() ? getBluetoothContextualVolumeStream() : + AudioSystem.STREAM_MUSIC; + if (streamTypeAlias == streamToDriveAbsVol && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device) && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) { if (DEBUG_VOL) { @@ -6180,6 +6222,17 @@ public class AudioService extends IAudioService.Stub setLeAudioVolumeOnModeUpdate(mode, device, streamAlias, index, maxIndex); + synchronized (mCachedAbsVolDrivingStreamsLock) { + mCachedAbsVolDrivingStreams.replaceAll((absDev, stream) -> { + int streamToDriveAbs = getBluetoothContextualVolumeStream(); + if (stream != streamToDriveAbs) { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(absDev, /*address=*/ + "", /*enabled*/true, streamToDriveAbs); + } + return streamToDriveAbs; + }); + } + // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO // connections not started by the application changing the mode when pid changes mDeviceBroker.postSetModeOwner(mode, pid, uid); @@ -7859,7 +7912,8 @@ public class AudioService extends IAudioService.Stub if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK && profile != BluetoothProfile.LE_AUDIO && profile != BluetoothProfile.LE_AUDIO_BROADCAST - && profile != BluetoothProfile.HEARING_AID) { + && profile != BluetoothProfile.HEARING_AID + && !(mDeviceBroker.isScoManagedByAudio() && profile == BluetoothProfile.HEADSET)) { throw new IllegalArgumentException("Illegal BluetoothProfile profile for device " + previousDevice + " -> " + newDevice + ". Got: " + profile); } @@ -8100,6 +8154,10 @@ public class AudioService extends IAudioService.Stub return mAudioVolumeGroup.name(); } + public int getId() { + return mAudioVolumeGroup.getId(); + } + /** * Volume group with non null minimum index are considered as non mutable, thus * bijectivity is broken with potential associated stream type. @@ -8750,24 +8808,30 @@ public class AudioService extends IAudioService.Stub } private int getAbsoluteVolumeIndex(int index) { - /* Special handling for Bluetooth Absolute Volume scenario - * If we send full audio gain, some accessories are too loud even at its lowest - * volume. We are not able to enumerate all such accessories, so here is the - * workaround from phone side. - * Pre-scale volume at lowest volume steps 1 2 and 3. - * For volume step 0, set audio gain to 0 as some accessories won't mute on their end. - */ - if (index == 0) { - // 0% for volume 0 - index = 0; - } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) { - // Pre-scale for volume steps 1 2 and 3 - index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10; + if (absVolumeIndexFix()) { + // The attenuation is applied in the APM. No need to manipulate the index here + return index; } else { - // otherwise, full gain - index = (mIndexMax + 5) / 10; + /* Special handling for Bluetooth Absolute Volume scenario + * If we send full audio gain, some accessories are too loud even at its lowest + * volume. We are not able to enumerate all such accessories, so here is the + * workaround from phone side. + * Pre-scale volume at lowest volume steps 1 2 and 3. + * For volume step 0, set audio gain to 0 as some accessories won't mute on their + * end. + */ + if (index == 0) { + // 0% for volume 0 + index = 0; + } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) { + // Pre-scale for volume steps 1 2 and 3 + index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10; + } else { + // otherwise, full gain + index = (mIndexMax + 5) / 10; + } + return index; } - return index; } private void setStreamVolumeIndex(int index, int device) { @@ -8778,6 +8842,11 @@ public class AudioService extends IAudioService.Stub && !isFullyMuted()) { index = 1; } + + if (DEBUG_VOL) { + Log.d(TAG, "setStreamVolumeIndexAS(" + mStreamType + ", " + index + ", " + device + + ")"); + } mAudioSystem.setStreamVolumeIndexAS(mStreamType, index, device); } @@ -8789,14 +8858,24 @@ public class AudioService extends IAudioService.Stub } else if (isAbsoluteVolumeDevice(device) || isA2dpAbsoluteVolumeDevice(device) || AudioSystem.isLeAudioDeviceType(device)) { - index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10); + // do not change the volume logic for dynamic abs behavior devices like HDMI + if (absVolumeIndexFix() && isAbsoluteVolumeDevice(device)) { + index = getAbsoluteVolumeIndex((mIndexMax + 5) / 10); + } else { + index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10); + } } else if (isFullVolumeDevice(device)) { index = (mIndexMax + 5)/10; } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) { - index = (mIndexMax + 5)/10; + if (absVolumeIndexFix()) { + index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10); + } else { + index = (mIndexMax + 5) / 10; + } } else { index = (getIndex(device) + 5)/10; } + setStreamVolumeIndex(index, device); } @@ -8814,11 +8893,22 @@ public class AudioService extends IAudioService.Stub || isA2dpAbsoluteVolumeDevice(device) || AudioSystem.isLeAudioDeviceType(device)) { isAbsoluteVolume = true; - index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10); + // do not change the volume logic for dynamic abs behavior devices + // like HDMI + if (absVolumeIndexFix() && isAbsoluteVolumeDevice(device)) { + index = getAbsoluteVolumeIndex((mIndexMax + 5) / 10); + } else { + index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10); + } } else if (isFullVolumeDevice(device)) { index = (mIndexMax + 5)/10; } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) { - index = (mIndexMax + 5)/10; + if (absVolumeIndexFix()) { + isAbsoluteVolume = true; + index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10); + } else { + index = (mIndexMax + 5) / 10; + } } else { index = (mIndexMap.valueAt(i) + 5)/10; } @@ -9815,6 +9905,27 @@ public class AudioService extends IAudioService.Stub /*package*/ void setAvrcpAbsoluteVolumeSupported(boolean support) { mAvrcpAbsVolSupported = support; + if (absVolumeIndexFix()) { + int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP; + synchronized (mCachedAbsVolDrivingStreamsLock) { + mCachedAbsVolDrivingStreams.compute(a2dpDev, (dev, stream) -> { + if (stream != null && !mAvrcpAbsVolSupported) { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/ + "", /*enabled*/false, AudioSystem.DEVICE_NONE); + return null; + } + // For A2DP and AVRCP we need to set the driving stream based on the + // BT contextual stream. Hence, we need to make sure in adjustStreamVolume + // and setStreamVolume that the driving abs volume stream is consistent. + int streamToDriveAbs = getBluetoothContextualVolumeStream(); + if (stream == null || stream != streamToDriveAbs) { + mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/ + "", /*enabled*/true, streamToDriveAbs); + } + return streamToDriveAbs; + }); + } + } sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0, mStreamStates[AudioSystem.STREAM_MUSIC], 0); @@ -11831,6 +11942,45 @@ public class AudioService extends IAudioService.Stub private static final String mMetricsId = MediaMetrics.Name.AUDIO_SERVICE + MediaMetrics.SEPARATOR; + private static AudioServerPermissionProvider initializeAudioServerPermissionProvider( + Context context, AudioPolicyFacade audioPolicy, Executor audioserverExecutor) { + Collection<PackageState> packageStates = null; + try (PackageManagerLocal.UnfilteredSnapshot snapshot = + LocalManagerRegistry.getManager(PackageManagerLocal.class) + .withUnfilteredSnapshot()) { + packageStates = snapshot.getPackageStates().values(); + } + var provider = new AudioServerPermissionProvider(packageStates); + audioPolicy.registerOnStartTask(() -> { + provider.onServiceStart(audioPolicy.getPermissionController()); + }); + + // Set up event listeners + IntentFilter packageUpdateFilter = new IntentFilter(); + packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED); + packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED); + packageUpdateFilter.addDataScheme("package"); + + context.registerReceiverForAllUsers(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + String pkgName = intent.getData().getEncodedSchemeSpecificPart(); + int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); + if (intent.getBooleanExtra(EXTRA_REPLACING, false) || + intent.getBooleanExtra(EXTRA_ARCHIVAL, false)) return; + if (action.equals(ACTION_PACKAGE_ADDED)) { + audioserverExecutor.execute(() -> + provider.onModifyPackageState(uid, pkgName, false /* isRemoved */)); + } else if (action.equals(ACTION_PACKAGE_REMOVED)) { + audioserverExecutor.execute(() -> + provider.onModifyPackageState(uid, pkgName, true /* isRemoved */)); + } + } + }, packageUpdateFilter, null, null); // main thread is fine, since dispatch on executor + return provider; + } + // Inform AudioFlinger of our device's low RAM attribute private static void readAndSetLowRamDevice() { diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index 7202fa286453..7f4bc74bd59e 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -598,6 +598,21 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } /** + * Same as {@link AudioSystem#setDeviceAbsoluteVolumeEnabled(int, String, boolean, int)} + * @param nativeDeviceType the internal device type for which absolute volume is + * enabled/disabled + * @param address the address of the device for which absolute volume is enabled/disabled + * @param enabled whether the absolute volume is enabled/disabled + * @param streamToDriveAbs the stream that is controlling the absolute volume + * @return status of indicating the success of this operation + */ + public int setDeviceAbsoluteVolumeEnabled(int nativeDeviceType, @NonNull String address, + boolean enabled, int streamToDriveAbs) { + return AudioSystem.setDeviceAbsoluteVolumeEnabled(nativeDeviceType, address, enabled, + streamToDriveAbs); + } + + /** * Same as {@link AudioSystem#registerPolicyMixes(ArrayList, boolean)} * @param mixes * @param register diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 07daecdfc9f6..6bb3eb1c3078 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -94,14 +94,14 @@ public class BtHelper { private final Map<BluetoothDevice, AudioDeviceAttributes> mResolvedScoAudioDevices = new HashMap<>(); - private @Nullable BluetoothHearingAid mHearingAid; + private @Nullable BluetoothHearingAid mHearingAid = null; - private @Nullable BluetoothLeAudio mLeAudio; + private @Nullable BluetoothLeAudio mLeAudio = null; private @Nullable BluetoothLeAudioCodecConfig mLeAudioCodecConfig; // Reference to BluetoothA2dp to query for AbsoluteVolume. - private @Nullable BluetoothA2dp mA2dp; + private @Nullable BluetoothA2dp mA2dp = null; private @Nullable BluetoothCodecConfig mA2dpCodecConfig; @@ -401,50 +401,67 @@ public class BtHelper { private void onScoAudioStateChanged(int state) { boolean broadcast = false; int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR; - switch (state) { - case BluetoothHeadset.STATE_AUDIO_CONNECTED: - scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED; - if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL - && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) { - mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; - } else if (mDeviceBroker.isBluetoothScoRequested()) { - // broadcast intent if the connection was initated by AudioService + if (mDeviceBroker.isScoManagedByAudio()) { + switch (state) { + case BluetoothHeadset.STATE_AUDIO_CONNECTED: + mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged"); + scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED; broadcast = true; - } - mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged"); - break; - case BluetoothHeadset.STATE_AUDIO_DISCONNECTED: - mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged"); - scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED; - // There are two cases where we want to immediately reconnect audio: - // 1) If a new start request was received while disconnecting: this was - // notified by requestScoState() setting state to SCO_STATE_ACTIVATE_REQ. - // 2) If audio was connected then disconnected via Bluetooth APIs and - // we still have pending activation requests by apps: this is indicated by - // state SCO_STATE_ACTIVE_EXTERNAL and BT SCO is requested. - if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) { - if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null - && connectBluetoothScoAudioHelper(mBluetoothHeadset, - mBluetoothHeadsetDevice, mScoAudioMode)) { - mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; - scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTING; + break; + case BluetoothHeadset.STATE_AUDIO_DISCONNECTED: + mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged"); + scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED; + broadcast = true; + break; + default: + break; + } + } else { + switch (state) { + case BluetoothHeadset.STATE_AUDIO_CONNECTED: + scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED; + if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL + && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) { + mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; + } else if (mDeviceBroker.isBluetoothScoRequested()) { + // broadcast intent if the connection was initated by AudioService broadcast = true; - break; } - } - if (mScoAudioState != SCO_STATE_ACTIVE_EXTERNAL) { - broadcast = true; - } - mScoAudioState = SCO_STATE_INACTIVE; - break; - case BluetoothHeadset.STATE_AUDIO_CONNECTING: - if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL - && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) { - mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; - } - break; - default: - break; + mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged"); + break; + case BluetoothHeadset.STATE_AUDIO_DISCONNECTED: + mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged"); + scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED; + // There are two cases where we want to immediately reconnect audio: + // 1) If a new start request was received while disconnecting: this was + // notified by requestScoState() setting state to SCO_STATE_ACTIVATE_REQ. + // 2) If audio was connected then disconnected via Bluetooth APIs and + // we still have pending activation requests by apps: this is indicated by + // state SCO_STATE_ACTIVE_EXTERNAL and BT SCO is requested. + if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) { + if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null + && connectBluetoothScoAudioHelper(mBluetoothHeadset, + mBluetoothHeadsetDevice, mScoAudioMode)) { + mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; + scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTING; + broadcast = true; + break; + } + } + if (mScoAudioState != SCO_STATE_ACTIVE_EXTERNAL) { + broadcast = true; + } + mScoAudioState = SCO_STATE_INACTIVE; + break; + case BluetoothHeadset.STATE_AUDIO_CONNECTING: + if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL + && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) { + mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; + } + break; + default: + break; + } } if (broadcast) { broadcastScoConnectionState(scoAudioState); @@ -454,7 +471,6 @@ public class BtHelper { newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, scoAudioState); sendStickyBroadcastToAll(newIntent); } - } /** * @@ -577,7 +593,11 @@ public class BtHelper { mHearingAid = null; break; case BluetoothProfile.LE_AUDIO: + if (mLeAudio != null && mLeAudioCallback != null) { + mLeAudio.unregisterCallback(mLeAudioCallback); + } mLeAudio = null; + mLeAudioCallback = null; mLeAudioCodecConfig = null; break; case BluetoothProfile.LE_AUDIO_BROADCAST: @@ -596,8 +616,6 @@ public class BtHelper { // BluetoothLeAudio callback used to update the list of addresses in the same group as a // connected LE Audio device - MyLeAudioCallback mLeAudioCallback = null; - class MyLeAudioCallback implements BluetoothLeAudio.Callback { @Override public void onCodecConfigChanged(int groupId, @@ -620,6 +638,8 @@ public class BtHelper { } } + MyLeAudioCallback mLeAudioCallback = null; + // @GuardedBy("mDeviceBroker.mSetModeLock") @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock") /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) { @@ -635,18 +655,28 @@ public class BtHelper { onHeadsetProfileConnected((BluetoothHeadset) proxy); return; case BluetoothProfile.A2DP: + if (((BluetoothA2dp) proxy).equals(mA2dp)) { + return; + } mA2dp = (BluetoothA2dp) proxy; break; case BluetoothProfile.HEARING_AID: + if (((BluetoothHearingAid) proxy).equals(mHearingAid)) { + return; + } mHearingAid = (BluetoothHearingAid) proxy; break; case BluetoothProfile.LE_AUDIO: - if (mLeAudio == null) { - mLeAudioCallback = new MyLeAudioCallback(); - ((BluetoothLeAudio) proxy).registerCallback( - mContext.getMainExecutor(), mLeAudioCallback); + if (((BluetoothLeAudio) proxy).equals(mLeAudio)) { + return; + } + if (mLeAudio != null && mLeAudioCallback != null) { + mLeAudio.unregisterCallback(mLeAudioCallback); } mLeAudio = (BluetoothLeAudio) proxy; + mLeAudioCallback = new MyLeAudioCallback(); + mLeAudio.registerCallback( + mContext.getMainExecutor(), mLeAudioCallback); break; case BluetoothProfile.A2DP_SINK: case BluetoothProfile.LE_AUDIO_BROADCAST: diff --git a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java index 37b812685a3d..09701e49a8ac 100644 --- a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java +++ b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java @@ -16,100 +16,68 @@ package com.android.server.audio; -import android.annotation.NonNull; import android.annotation.Nullable; import android.media.IAudioPolicyService; -import android.media.permission.ClearCallingIdentityContext; -import android.media.permission.SafeCloseable; +import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; -import com.android.internal.annotations.GuardedBy; +import com.android.media.permission.INativePermissionController; + +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Function; /** - * Default implementation of a facade to IAudioPolicyManager which fulfills AudioService - * dependencies. This forwards calls as-is to IAudioPolicyManager. - * Public methods throw IllegalStateException if AudioPolicy is not initialized/available + * Default implementation of a facade to IAudioPolicyService which fulfills AudioService + * dependencies. This forwards calls as-is to IAudioPolicyService. */ -public class DefaultAudioPolicyFacade implements AudioPolicyFacade, IBinder.DeathRecipient { +public class DefaultAudioPolicyFacade implements AudioPolicyFacade { - private static final String TAG = "DefaultAudioPolicyFacade"; private static final String AUDIO_POLICY_SERVICE_NAME = "media.audio_policy"; - private final Object mServiceLock = new Object(); - @GuardedBy("mServiceLock") - private IAudioPolicyService mAudioPolicy; + private final ServiceHolder<IAudioPolicyService> mServiceHolder; - public DefaultAudioPolicyFacade() { - try { - getAudioPolicyOrInit(); - } catch (IllegalStateException e) { - // Log and suppress this exception, we may be able to connect later - Log.e(TAG, "Failed to initialize APM connection", e); - } + /** + * @param e - Executor for service start tasks + */ + public DefaultAudioPolicyFacade(Executor e) { + mServiceHolder = + new ServiceHolder( + AUDIO_POLICY_SERVICE_NAME, + (Function<IBinder, IAudioPolicyService>) + IAudioPolicyService.Stub::asInterface, + e); + mServiceHolder.registerOnStartTask(i -> Binder.allowBlocking(i.asBinder())); } @Override public boolean isHotwordStreamSupported(boolean lookbackAudio) { - IAudioPolicyService ap = getAudioPolicyOrInit(); - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + IAudioPolicyService ap = mServiceHolder.waitForService(); + try { return ap.isHotwordStreamSupported(lookbackAudio); } catch (RemoteException e) { - resetServiceConnection(ap.asBinder()); - throw new IllegalStateException(e); + mServiceHolder.attemptClear(ap.asBinder()); + throw new IllegalStateException(); } } @Override - public void binderDied() { - Log.wtf(TAG, "Unexpected binderDied without IBinder object"); - } - - @Override - public void binderDied(@NonNull IBinder who) { - resetServiceConnection(who); - } - - private void resetServiceConnection(@Nullable IBinder deadAudioPolicy) { - synchronized (mServiceLock) { - if (mAudioPolicy != null && mAudioPolicy.asBinder().equals(deadAudioPolicy)) { - mAudioPolicy.asBinder().unlinkToDeath(this, 0); - mAudioPolicy = null; - } - } - } - - private @Nullable IAudioPolicyService getAudioPolicy() { - synchronized (mServiceLock) { - return mAudioPolicy; + public @Nullable INativePermissionController getPermissionController() { + IAudioPolicyService ap = mServiceHolder.checkService(); + if (ap == null) return null; + try { + var res = Objects.requireNonNull(ap.getPermissionController()); + Binder.allowBlocking(res.asBinder()); + return res; + } catch (RemoteException e) { + mServiceHolder.attemptClear(ap.asBinder()); + return null; } } - /* - * Does not block. - * @throws IllegalStateException for any failed connection - */ - private @NonNull IAudioPolicyService getAudioPolicyOrInit() { - synchronized (mServiceLock) { - if (mAudioPolicy != null) { - return mAudioPolicy; - } - // Do not block while attempting to connect to APM. Defer to caller. - IAudioPolicyService ap = IAudioPolicyService.Stub.asInterface( - ServiceManager.checkService(AUDIO_POLICY_SERVICE_NAME)); - if (ap == null) { - throw new IllegalStateException(TAG + ": Unable to connect to AudioPolicy"); - } - try { - ap.asBinder().linkToDeath(this, 0); - } catch (RemoteException e) { - throw new IllegalStateException( - TAG + ": Unable to link deathListener to AudioPolicy", e); - } - mAudioPolicy = ap; - return mAudioPolicy; - } + @Override + public void registerOnStartTask(Runnable task) { + mServiceHolder.registerOnStartTask(unused -> task.run()); } } diff --git a/services/core/java/com/android/server/audio/ServiceHolder.java b/services/core/java/com/android/server/audio/ServiceHolder.java new file mode 100644 index 000000000000..e2588fb4fdb1 --- /dev/null +++ b/services/core/java/com/android/server/audio/ServiceHolder.java @@ -0,0 +1,219 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.audio; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.os.IInterface; +import android.os.IServiceCallback; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Manages a remote service which can start and stop. Allows clients to add tasks to run when the + * remote service starts or dies. + * + * <p>Example usage should look something like: + * + * <pre> + * var service = mServiceHolder.checkService(); + * if (service == null) handleFailure(); + * try { + * service.foo(); + * } catch (RemoteException e) { + * mServiceHolder.attemptClear(service.asBinder()); + * handleFailure(); + * } + * </pre> + */ +public class ServiceHolder<I extends IInterface> implements IBinder.DeathRecipient { + + private final String mTag; + private final String mServiceName; + private final Function<? super IBinder, ? extends I> mCastFunction; + private final Executor mExecutor; + private final ServiceProviderFacade mServiceProvider; + + private final AtomicReference<I> mService = new AtomicReference(); + private final Set<Consumer<I>> mOnStartTasks = ConcurrentHashMap.newKeySet(); + private final Set<Consumer<I>> mOnDeathTasks = ConcurrentHashMap.newKeySet(); + + private final IServiceCallback mServiceListener = + new IServiceCallback.Stub() { + @Override + public void onRegistration(String name, IBinder binder) { + onServiceInited(binder); + } + }; + + // For test purposes + public static interface ServiceProviderFacade { + public void registerForNotifications(String name, IServiceCallback listener); + + public IBinder checkService(String name); + + public IBinder waitForService(String name); + } + + public ServiceHolder( + @NonNull String serviceName, + @NonNull Function<? super IBinder, ? extends I> castFunction, + @NonNull Executor executor) { + this( + serviceName, + castFunction, + executor, + new ServiceProviderFacade() { + @Override + public void registerForNotifications(String name, IServiceCallback listener) { + try { + ServiceManager.registerForNotifications(name, listener); + } catch (RemoteException e) { + throw new IllegalStateException("ServiceManager died!!", e); + } + } + + @Override + public IBinder checkService(String name) { + return ServiceManager.checkService(name); + } + + @Override + public IBinder waitForService(String name) { + return ServiceManager.waitForService(name); + } + }); + } + + public ServiceHolder( + @NonNull String serviceName, + @NonNull Function<? super IBinder, ? extends I> castFunction, + @NonNull Executor executor, + @NonNull ServiceProviderFacade provider) { + mServiceName = Objects.requireNonNull(serviceName); + mCastFunction = Objects.requireNonNull(castFunction); + mExecutor = Objects.requireNonNull(executor); + mServiceProvider = Objects.requireNonNull(provider); + mTag = "ServiceHolder: " + serviceName; + mServiceProvider.registerForNotifications(mServiceName, mServiceListener); + } + + /** + * Add tasks to run when service becomes available. Ran on the executor provided at + * construction. Note, for convenience, if the service is already connected, the task is + * immediately run. + */ + public void registerOnStartTask(Consumer<I> task) { + mOnStartTasks.add(task); + I i; + if ((i = mService.get()) != null) { + mExecutor.execute(() -> task.accept(i)); + } + } + + public void unregisterOnStartTask(Consumer<I> task) { + mOnStartTasks.remove(task); + } + + /** + * Add tasks to run when service goes down. Ran on the executor provided at construction. Should + * be called before getService to avoid dropping a death notification. + */ + public void registerOnDeathTask(Consumer<I> task) { + mOnDeathTasks.add(task); + } + + public void unregisterOnDeathTask(Consumer<I> task) { + mOnDeathTasks.remove(task); + } + + @Override + public void binderDied(@NonNull IBinder who) { + attemptClear(who); + } + + @Override + public void binderDied() { + throw new AssertionError("Wrong binderDied called, this should never happen"); + } + + /** + * Notify the holder that the service has gone done, usually in response to a RemoteException. + * Equivalent to receiving a binder death notification. + */ + public void attemptClear(IBinder who) { + // Possibly prone to weird races, resulting in spurious dead/revive, + // but that should be fine. + var current = mService.get(); + if (current != null + && Objects.equals(current.asBinder(), who) + && mService.compareAndSet(current, null)) { + who.unlinkToDeath(this, 0); + for (var r : mOnDeathTasks) { + mExecutor.execute(() -> r.accept(current)); + } + } + } + + /** Get the service, without blocking. Can trigger start tasks, on the provided executor. */ + public @Nullable I checkService() { + var s = mService.get(); + if (s != null) return s; + IBinder registered = mServiceProvider.checkService(mServiceName); + if (registered == null) return null; + return onServiceInited(registered); + } + + /** Get the service, but block. Can trigger start tasks, on the provided executor. */ + public @NonNull I waitForService() { + var s = mService.get(); + return (s != null) ? s : onServiceInited(mServiceProvider.waitForService(mServiceName)); + } + + /* + * Called when the native service is initialized. + */ + private @NonNull I onServiceInited(@NonNull IBinder who) { + var service = mCastFunction.apply(who); + Objects.requireNonNull(service); + if (!mService.compareAndSet(null, service)) { + return service; + } + // Even if the service has immediately died, we should perform these tasks for consistency + for (var r : mOnStartTasks) { + mExecutor.execute(() -> r.accept(service)); + } + try { + who.linkToDeath(this, 0); + } catch (RemoteException e) { + Log.e(mTag, "Immediate service death. Service crash-looping"); + attemptClear(who); + } + // This interface is non-null, but could represent a dead object + return service; + } +} diff --git a/services/core/java/com/android/server/biometrics/biometrics.aconfig b/services/core/java/com/android/server/biometrics/biometrics.aconfig index 712dcee55b7b..92fd9cbcf14e 100644 --- a/services/core/java/com/android/server/biometrics/biometrics.aconfig +++ b/services/core/java/com/android/server/biometrics/biometrics.aconfig @@ -14,10 +14,3 @@ flag { description: "This flag controls whether virtual HAL is used for testing instead of TestHal " bug: "294254230" } - -flag { - name: "mandatory_biometrics" - namespace: "biometrics_framework" - description: "This flag controls whether LSKF fallback is removed from biometric prompt when the phone is outside trusted locations" - bug: "322081563" -} diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java index f77a360addd0..0fef55da5749 100644 --- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java +++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java @@ -21,13 +21,21 @@ import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIG import android.annotation.Nullable; import android.hardware.display.DisplayManagerInternal; import android.os.Trace; +import android.util.Slog; import android.view.Display; +import com.android.server.display.utils.DebugUtils; + /** * An implementation of the offload session that keeps track of whether the session is active. * An offload session is used to control the display's brightness using the offload chip. */ public class DisplayOffloadSessionImpl implements DisplayManagerInternal.DisplayOffloadSession { + private static final String TAG = "DisplayOffloadSessionImpl"; + + // To enable these logs, run: + // 'adb shell setprop persist.log.tag.DisplayOffloadSessionImpl DEBUG && adb reboot' + private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); @Nullable private final DisplayManagerInternal.DisplayOffloader mDisplayOffloader; @@ -99,9 +107,14 @@ public class DisplayOffloadSessionImpl implements DisplayManagerInternal.Display if (mDisplayOffloader == null || mIsActive) { return false; } + Trace.traceBegin(Trace.TRACE_TAG_POWER, "DisplayOffloader#startOffload"); try { - return mIsActive = mDisplayOffloader.startOffload(); + mIsActive = mDisplayOffloader.startOffload(); + if (DEBUG) { + Slog.d(TAG, "startOffload = " + mIsActive); + } + return mIsActive; } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } @@ -118,6 +131,9 @@ public class DisplayOffloadSessionImpl implements DisplayManagerInternal.Display try { mDisplayOffloader.stopOffload(); mIsActive = false; + if (DEBUG) { + Slog.i(TAG, "stopOffload"); + } } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 7d482f74d5b3..b97053b21b8e 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -791,10 +791,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call @Override public void overrideDozeScreenState(int displayState, @Display.StateReason int reason) { + Slog.i(TAG, "New offload doze override: " + Display.stateToString(displayState)); mHandler.postAtTime(() -> { if (mDisplayOffloadSession == null || !(DisplayOffloadSession.isSupportedOffloadState(displayState) - || displayState == Display.STATE_UNKNOWN)) { + || displayState == Display.STATE_UNKNOWN)) { return; } mDisplayStateController.overrideDozeScreenState(displayState, reason); @@ -1279,7 +1280,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private void updatePowerStateInternal() { // Update the power state request. - final boolean mustNotify; + boolean mustNotify = false; final int previousPolicy; boolean mustInitialize = false; mBrightnessReasonTemp.set(null); @@ -1327,6 +1328,30 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call initialize(readyToUpdateDisplayState() ? state : Display.STATE_UNKNOWN); } + if (mFlags.isOffloadDozeOverrideHoldsWakelockEnabled()) { + // Sometimes, a display-state change can come without an associated PowerRequest, + // as with DisplayOffload. For those cases, we have to make sure to also mark the + // display as "not ready" so that we can inform power-manager when the state-change is + // complete. + if (mPowerState.getScreenState() != state) { + final boolean wasReady; + synchronized (mLock) { + wasReady = mDisplayReadyLocked; + mDisplayReadyLocked = false; + mustNotify = true; + } + + if (wasReady) { + // If we went from ready to not-ready from the state-change (instead of a + // PowerRequest) there's a good chance that nothing is keeping PowerManager + // from suspending. Grab the unfinished business suspend blocker to keep the + // device awake until the display-state change goes into effect. + mWakelockController.acquireWakelock( + WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS); + } + } + } + // Animate the screen state change unless already animating. // The transition may be deferred, so after this point we will use the // actual state instead of the desired one. diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 182b05a68028..44846f310348 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -168,6 +168,12 @@ final class LocalDisplayAdapter extends DisplayAdapter { } SurfaceControl.DesiredDisplayModeSpecs modeSpecs = mSurfaceControlProxy.getDesiredDisplayModeSpecs(displayToken); + if (modeSpecs == null) { + // If mode specs is null, it most probably means that display got + // unplugged very rapidly. + Slog.w(TAG, "Desired display mode specs from SurfaceFlinger are null"); + return; + } LocalDisplayDevice device = mDevices.get(physicalDisplayId); if (device == null) { // Display was added. diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index 8f775a54a8cd..f923cbc978ff 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -174,6 +174,11 @@ public class DisplayManagerFlags { Flags::enableSynthetic60hzModes ); + private final FlagState mOffloadDozeOverrideHoldsWakelock = new FlagState( + Flags.FLAG_OFFLOAD_DOZE_OVERRIDE_HOLDS_WAKELOCK, + Flags::offloadDozeOverrideHoldsWakelock + ); + /** * @return {@code true} if 'port' is allowed in display layout configuration file. */ @@ -343,6 +348,10 @@ public class DisplayManagerFlags { return mPeakRefreshRatePhysicalLimit.isEnabled(); } + public boolean isOffloadDozeOverrideHoldsWakelockEnabled() { + return mOffloadDozeOverrideHoldsWakelock.isEnabled(); + } + /** * @return Whether to ignore preferredRefreshRate app request or not */ @@ -389,6 +398,7 @@ public class DisplayManagerFlags { pw.println(" " + mPeakRefreshRatePhysicalLimit); pw.println(" " + mIgnoreAppPreferredRefreshRate); pw.println(" " + mSynthetic60hzModes); + pw.println(" " + mOffloadDozeOverrideHoldsWakelock); } private static class FlagState { diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 697218dc0f41..95d0ca381f77 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -289,3 +289,13 @@ flag { } } +flag { + name: "offload_doze_override_holds_wakelock" + namespace: "display_manager" + description: "DisplayPowerController holds a suspend-blocker while changing the display state on behalf of offload doze override." + bug: "338403827" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index dbd1e65e8b0f..6e027c6d44c4 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -1029,6 +1029,10 @@ public class HdmiControlService extends SystemService { /** Helper method for sending feature discovery command */ private void reportFeatures(boolean isTvDeviceSetting) { + // <Report Features> should only be sent for HDMI 2.0 + if (getCecVersion() < HdmiControlManager.HDMI_CEC_VERSION_2_0) { + return; + } // check if tv device is enabled for tv device specific RC profile setting if (isTvDeviceSetting) { if (isTvDeviceEnabled()) { diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java index dd6433d98553..82ecb4acb197 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java @@ -16,12 +16,16 @@ package com.android.server.inputmethod; +import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.annotation.WorkerThread; import android.content.Context; import android.content.pm.UserInfo; import android.os.Handler; +import android.os.Process; +import android.util.IntArray; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -29,6 +33,10 @@ import com.android.internal.inputmethod.DirectBootAwareness; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; +import java.util.ArrayList; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + /** * Provides accesses to per-user additional {@link android.view.inputmethod.InputMethodSubtype} * persistent storages. @@ -38,6 +46,152 @@ final class AdditionalSubtypeMapRepository { @NonNull private static final SparseArray<AdditionalSubtypeMap> sPerUserMap = new SparseArray<>(); + record WriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap, + @NonNull InputMethodMap inputMethodMap) { + } + + static final class SingleThreadedBackgroundWriter { + /** + * A {@link ReentrantLock} used to guard {@link #mPendingTasks} and {@link #mRemovedUsers}. + */ + @NonNull + private final ReentrantLock mLock = new ReentrantLock(); + /** + * A {@link Condition} associated with {@link #mLock} for producer to unblock consumer. + */ + @NonNull + private final Condition mLockNotifier = mLock.newCondition(); + + @GuardedBy("mLock") + @NonNull + private final SparseArray<WriteTask> mPendingTasks = new SparseArray<>(); + + @GuardedBy("mLock") + private final IntArray mRemovedUsers = new IntArray(); + + @NonNull + private final Thread mWriterThread = new Thread("android.ime.as") { + + /** + * Waits until the next data has come then return the result after filtering out any + * already removed users. + * + * @return A list of {@link WriteTask} to be written into persistent storage + */ + @WorkerThread + private ArrayList<WriteTask> fetchNextTasks() { + final SparseArray<WriteTask> tasks; + final IntArray removedUsers; + mLock.lock(); + try { + while (true) { + if (mPendingTasks.size() != 0) { + tasks = mPendingTasks.clone(); + mPendingTasks.clear(); + if (mRemovedUsers.size() == 0) { + removedUsers = null; + } else { + removedUsers = mRemovedUsers.clone(); + } + break; + } + mLockNotifier.awaitUninterruptibly(); + } + } finally { + mLock.unlock(); + } + final int size = tasks.size(); + final ArrayList<WriteTask> result = new ArrayList<>(size); + for (int i = 0; i < size; ++i) { + final int userId = tasks.keyAt(i); + if (removedUsers != null && removedUsers.contains(userId)) { + continue; + } + result.add(tasks.valueAt(i)); + } + return result; + } + + @WorkerThread + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + while (true) { + final ArrayList<WriteTask> tasks = fetchNextTasks(); + tasks.forEach(task -> AdditionalSubtypeUtils.save( + task.subtypeMap, task.inputMethodMap, task.userId)); + } + } + }; + + /** + * Schedules a write operation + * + * @param userId the target user ID of this operation + * @param subtypeMap {@link AdditionalSubtypeMap} to be saved + * @param inputMethodMap {@link InputMethodMap} to be used to filter our {@code subtypeMap} + */ + @AnyThread + void scheduleWriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap, + @NonNull InputMethodMap inputMethodMap) { + final var task = new WriteTask(userId, subtypeMap, inputMethodMap); + mLock.lock(); + try { + if (mRemovedUsers.contains(userId)) { + return; + } + mPendingTasks.put(userId, task); + mLockNotifier.signalAll(); + } finally { + mLock.unlock(); + } + } + + /** + * Called back when a user is being created. + * + * @param userId The user ID to be created + */ + @AnyThread + void onUserCreated(@UserIdInt int userId) { + mLock.lock(); + try { + for (int i = mRemovedUsers.size() - 1; i >= 0; --i) { + if (mRemovedUsers.get(i) == userId) { + mRemovedUsers.remove(i); + } + } + } finally { + mLock.unlock(); + } + } + + /** + * Called back when a user is being removed. Any pending task will be effectively canceled + * if the user is removed before the task is fulfilled. + * + * @param userId The user ID to be removed + */ + @AnyThread + void onUserRemoved(@UserIdInt int userId) { + mLock.lock(); + try { + mRemovedUsers.add(userId); + mPendingTasks.remove(userId); + } finally { + mLock.unlock(); + } + } + + void startThread() { + mWriterThread.start(); + } + } + + private static final SingleThreadedBackgroundWriter sWriter = + new SingleThreadedBackgroundWriter(); + /** * Not intended to be instantiated. */ @@ -64,9 +218,11 @@ final class AdditionalSubtypeMapRepository { return; } sPerUserMap.put(userId, map); - // TODO: Offload this to a background thread. - // TODO: Skip if the previous data is exactly the same as new one. - AdditionalSubtypeUtils.save(map, inputMethodMap, userId); + sWriter.scheduleWriteTask(userId, map, inputMethodMap); + } + + static void startWriterThread() { + sWriter.startThread(); } static void initialize(@NonNull Handler handler, @NonNull Context context) { @@ -78,6 +234,7 @@ final class AdditionalSubtypeMapRepository { @Override public void onUserCreated(UserInfo user, @Nullable Object token) { final int userId = user.id; + sWriter.onUserCreated(userId); handler.post(() -> { synchronized (ImfLock.class) { if (!sPerUserMap.contains(userId)) { @@ -99,6 +256,7 @@ final class AdditionalSubtypeMapRepository { @Override public void onUserRemoved(UserInfo user) { final int userId = user.id; + sWriter.onUserRemoved(userId); handler.post(() -> { synchronized (ImfLock.class) { sPerUserMap.remove(userId); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 5843d72f346a..7513c40a1f90 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1547,6 +1547,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, currentUserId), newSettings.getEnabledInputMethodList()); + + final var unused = SystemServerInitThreadPool.submit( + AdditionalSubtypeMapRepository::startWriterThread, + "Start AdditionalSubtypeMapRepository's writer thread"); } } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index 1c958a929546..23f947cc8452 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -367,9 +367,9 @@ final class InputMethodSubtypeSwitchingController { } protected void dump(final Printer pw, final String prefix) { - for (int i = 0; i < mUsageHistoryOfSubtypeListItemIndex.length; ++i) { - final int rank = mUsageHistoryOfSubtypeListItemIndex[i]; - final ImeSubtypeListItem item = mImeSubtypeList.get(i); + for (int rank = 0; rank < mUsageHistoryOfSubtypeListItemIndex.length; ++rank) { + final int index = mUsageHistoryOfSubtypeListItemIndex[rank]; + final ImeSubtypeListItem item = mImeSubtypeList.get(index); pw.println(prefix + "rank=" + rank + " item=" + item); } } diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java index 563f93e96331..b9e09605477a 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java +++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java @@ -84,9 +84,16 @@ class LocaleManagerBackupHelper { * from the delegate selector. */ private static final String LOCALES_FROM_DELEGATE_PREFS = "LocalesFromDelegatePrefs.xml"; + private static final String LOCALES_STAGED_DATA_PREFS = "LocalesStagedDataPrefs.xml"; + private static final String ARCHIVED_PACKAGES_PREFS = "ArchivedPackagesPrefs.xml"; // Stage data would be deleted on reboot since it's stored in memory. So it's retained until // retention period OR next reboot, whichever happens earlier. private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3); + // Store the locales staged data for the specified package in the SharedPreferences. The format + // is locales s:setFromDelegate + // For example: en-US s:true + private static final String STRING_SPLIT = " s:"; + private static final String KEY_STAGED_DATA_TIME = "staged_data_time"; private final LocaleManagerService mLocaleManagerService; private final PackageManager mPackageManager; @@ -94,39 +101,34 @@ class LocaleManagerBackupHelper { private final Context mContext; private final Object mStagedDataLock = new Object(); - // Staged data map keyed by user-id to handle multi-user scenario / work profiles. We are using - // SparseArray because it is more memory-efficient than a HashMap. - private final SparseArray<StagedData> mStagedData; - // SharedPreferences to store packages whose app-locale was set by a delegate, as opposed to // the application setting the app-locale itself. private final SharedPreferences mDelegateAppLocalePackages; + // For unit tests + private final SparseArray<File> mStagedDataFiles; + private final File mArchivedPackagesFile; + private final BroadcastReceiver mUserMonitor; - // To determine whether an app is pre-archived, check for Intent.EXTRA_ARCHIVAL upon receiving - // the initial PACKAGE_ADDED broadcast. If it is indeed pre-archived, perform the data - // restoration during the second PACKAGE_ADDED broadcast, which is sent subsequently when the - // app is installed. - private final Set<String> mPkgsToRestore; LocaleManagerBackupHelper(LocaleManagerService localeManagerService, PackageManager packageManager, HandlerThread broadcastHandlerThread) { this(localeManagerService.mContext, localeManagerService, packageManager, Clock.systemUTC(), - new SparseArray<>(), broadcastHandlerThread, null); + broadcastHandlerThread, null, null, null); } - @VisibleForTesting LocaleManagerBackupHelper(Context context, - LocaleManagerService localeManagerService, - PackageManager packageManager, Clock clock, SparseArray<StagedData> stagedData, - HandlerThread broadcastHandlerThread, SharedPreferences delegateAppLocalePackages) { + @VisibleForTesting + LocaleManagerBackupHelper(Context context, LocaleManagerService localeManagerService, + PackageManager packageManager, Clock clock, HandlerThread broadcastHandlerThread, + SparseArray<File> stagedDataFiles, File archivedPackagesFile, + SharedPreferences delegateAppLocalePackages) { mContext = context; mLocaleManagerService = localeManagerService; mPackageManager = packageManager; mClock = clock; - mStagedData = stagedData; mDelegateAppLocalePackages = delegateAppLocalePackages != null ? delegateAppLocalePackages - : createPersistedInfo(); - mPkgsToRestore = new ArraySet<>(); - + : createPersistedInfo(); + mArchivedPackagesFile = archivedPackagesFile; + mStagedDataFiles = stagedDataFiles; mUserMonitor = new UserMonitor(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_REMOVED); @@ -148,7 +150,7 @@ class LocaleManagerBackupHelper { } synchronized (mStagedDataLock) { - cleanStagedDataForOldEntriesLocked(); + cleanStagedDataForOldEntriesLocked(userId); } HashMap<String, LocalesInfo> pkgStates = new HashMap<>(); @@ -207,14 +209,11 @@ class LocaleManagerBackupHelper { return out.toByteArray(); } - private void cleanStagedDataForOldEntriesLocked() { - for (int i = 0; i < mStagedData.size(); i++) { - int userId = mStagedData.keyAt(i); - StagedData stagedData = mStagedData.get(userId); - if (stagedData.mCreationTimeMillis - < mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) { - deleteStagedDataLocked(userId); - } + private void cleanStagedDataForOldEntriesLocked(@UserIdInt int userId) { + Long created_time = getStagedDataSp(userId).getLong(KEY_STAGED_DATA_TIME, -1); + if (created_time != -1 + && created_time < mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) { + deleteStagedDataLocked(userId); } } @@ -252,20 +251,16 @@ class LocaleManagerBackupHelper { // performed simultaneously. synchronized (mStagedDataLock) { // Backups for apps which are yet to be installed. - StagedData stagedData = new StagedData(mClock.millis(), new HashMap<>()); - for (String pkgName : pkgStates.keySet()) { LocalesInfo localesInfo = pkgStates.get(pkgName); // Check if the application is already installed for the concerned user. if (isPackageInstalledForUser(pkgName, userId)) { - if (mPkgsToRestore != null) { - mPkgsToRestore.remove(pkgName); - } + removeFromArchivedPackagesInfo(userId, pkgName); // Don't apply the restore if the locales have already been set for the app. checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId); } else { // Stage the data if the app isn't installed. - stagedData.mPackageStates.put(pkgName, localesInfo); + storeStagedDataInfo(userId, pkgName, localesInfo); if (DEBUG) { Slog.d(TAG, "Add locales=" + localesInfo.mLocales + " fromDelegate=" + localesInfo.mSetFromDelegate @@ -274,8 +269,9 @@ class LocaleManagerBackupHelper { } } - if (!stagedData.mPackageStates.isEmpty()) { - mStagedData.put(userId, stagedData); + // Create the time if the data is being staged. + if (!getStagedDataSp(userId).getAll().isEmpty()) { + storeStagedDataCreatedTime(userId); } } } @@ -293,14 +289,23 @@ class LocaleManagerBackupHelper { * added on device. */ void onPackageAddedWithExtras(String packageName, int uid, Bundle extras) { - boolean archived = false; + int userId = UserHandle.getUserId(uid); if (extras != null) { - archived = extras.getBoolean(Intent.EXTRA_ARCHIVAL, false); - if (archived && mPkgsToRestore != null) { - mPkgsToRestore.add(packageName); + // To determine whether an app is pre-archived, check for Intent.EXTRA_ARCHIVAL upon + // receiving the initial PACKAGE_ADDED broadcast. If it is indeed pre-archived, perform + // the data restoration during the second PACKAGE_ADDED broadcast, which is sent + // subsequently when the app is installed. + boolean archived = extras.getBoolean(Intent.EXTRA_ARCHIVAL, false); + if (DEBUG) { + Slog.d(TAG, + "onPackageAddedWithExtras packageName: " + packageName + ", userId: " + + userId + ", archived: " + archived); + } + if (archived) { + addInArchivedPackagesInfo(userId, packageName); } } - checkStageDataAndApplyRestore(packageName, uid); + checkStageDataAndApplyRestore(packageName, userId); } /** @@ -310,9 +315,32 @@ class LocaleManagerBackupHelper { */ void onPackageUpdateFinished(String packageName, int uid) { int userId = UserHandle.getUserId(uid); - if (mPkgsToRestore != null && mPkgsToRestore.contains(packageName)) { - mPkgsToRestore.remove(packageName); - checkStageDataAndApplyRestore(packageName, uid); + if (DEBUG) { + Slog.d(TAG, + "onPackageUpdateFinished userId: " + userId + ", packageName: " + packageName); + } + String user = Integer.toString(userId); + File file = getArchivedPackagesFile(); + if (file.exists()) { + SharedPreferences sp = getArchivedPackagesSp(file); + Set<String> packageNames = new ArraySet<>(sp.getStringSet(user, new ArraySet<>())); + if (packageNames.remove(packageName)) { + SharedPreferences.Editor editor = sp.edit(); + if (packageNames.isEmpty()) { + if (!editor.remove(user).commit()) { + Slog.e(TAG, "Failed to remove the user"); + } + if (sp.getAll().isEmpty()) { + file.delete(); + } + } else { + // commit and log the result. + if (!editor.putStringSet(user, packageNames).commit()) { + Slog.e(TAG, "failed to remove the package"); + } + } + checkStageDataAndApplyRestore(packageName, userId); + } } cleanApplicationLocalesIfNeeded(packageName, userId); } @@ -347,16 +375,16 @@ class LocaleManagerBackupHelper { } } - private void checkStageDataAndApplyRestore(String packageName, int uid) { + private void checkStageDataAndApplyRestore(String packageName, int userId) { try { synchronized (mStagedDataLock) { - cleanStagedDataForOldEntriesLocked(); - - int userId = UserHandle.getUserId(uid); - if (mStagedData.contains(userId)) { - if (mPkgsToRestore != null) { - mPkgsToRestore.remove(packageName); + cleanStagedDataForOldEntriesLocked(userId); + if (!getStagedDataSp(userId).getString(packageName, "").isEmpty()) { + if (DEBUG) { + Slog.d(TAG, + "checkStageDataAndApplyRestore, remove package and restore data"); } + removeFromArchivedPackagesInfo(userId, packageName); // Perform lazy restore only if the staged data exists. doLazyRestoreLocked(packageName, userId); } @@ -417,8 +445,17 @@ class LocaleManagerBackupHelper { } } - private void deleteStagedDataLocked(@UserIdInt int userId) { - mStagedData.remove(userId); + void deleteStagedDataLocked(@UserIdInt int userId) { + File stagedFile = getStagedDataFile(userId); + SharedPreferences sp = getStagedDataSp(stagedFile); + // commit and log the result. + if (!sp.edit().clear().commit()) { + Slog.e(TAG, "Failed to commit data!"); + } + + if (stagedFile.exists()) { + stagedFile.delete(); + } } /** @@ -473,16 +510,6 @@ class LocaleManagerBackupHelper { out.endDocument(); } - static class StagedData { - final long mCreationTimeMillis; - final HashMap<String, LocalesInfo> mPackageStates; - - StagedData(long creationTimeMillis, HashMap<String, LocalesInfo> pkgStates) { - mCreationTimeMillis = creationTimeMillis; - mPackageStates = pkgStates; - } - } - static class LocalesInfo { final String mLocales; final boolean mSetFromDelegate; @@ -508,6 +535,7 @@ class LocaleManagerBackupHelper { synchronized (mStagedDataLock) { deleteStagedDataLocked(userId); removeProfileFromPersistedInfo(userId); + removeArchivedPackagesForUser(userId); } } } catch (Exception e) { @@ -533,29 +561,162 @@ class LocaleManagerBackupHelper { return; } - StagedData stagedData = mStagedData.get(userId); - for (String pkgName : stagedData.mPackageStates.keySet()) { - LocalesInfo localesInfo = stagedData.mPackageStates.get(pkgName); + SharedPreferences sp = getStagedDataSp(userId); + String value = sp.getString(packageName, ""); + if (!value.isEmpty()) { + String[] info = value.split(STRING_SPLIT); + if (info == null || info.length != 2) { + Slog.e(TAG, "Failed to restore data"); + return; + } + LocalesInfo localesInfo = new LocalesInfo(info[0], Boolean.parseBoolean(info[1])); + checkExistingLocalesAndApplyRestore(packageName, localesInfo, userId); - if (pkgName.equals(packageName)) { + // Remove the restored entry from the staged data list. + if (!sp.edit().remove(packageName).commit()) { + Slog.e(TAG, "Failed to commit data!"); + } + } - checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId); + // Remove the stage data entry for user if there are no more packages to restore. + if (sp.getAll().size() == 1 && sp.getLong(KEY_STAGED_DATA_TIME, -1) != -1) { + deleteStagedDataLocked(userId); + } + } - // Remove the restored entry from the staged data list. - stagedData.mPackageStates.remove(pkgName); + private File getStagedDataFile(@UserIdInt int userId) { + return mStagedDataFiles == null ? new File(Environment.getDataSystemDeDirectory(userId), + LOCALES_STAGED_DATA_PREFS) : mStagedDataFiles.get(userId); + } - // Remove the stage data entry for user if there are no more packages to restore. - if (stagedData.mPackageStates.isEmpty()) { - mStagedData.remove(userId); - } + private SharedPreferences getStagedDataSp(File file) { + return mStagedDataFiles == null ? mContext.createDeviceProtectedStorageContext() + .getSharedPreferences(file, Context.MODE_PRIVATE) + : mContext.getSharedPreferences(file, Context.MODE_PRIVATE); + } + + private SharedPreferences getStagedDataSp(@UserIdInt int userId) { + return mStagedDataFiles == null ? mContext.createDeviceProtectedStorageContext() + .getSharedPreferences(getStagedDataFile(userId), Context.MODE_PRIVATE) + : mContext.getSharedPreferences(mStagedDataFiles.get(userId), Context.MODE_PRIVATE); + } - // No need to loop further after restoring locales because the staged data will - // contain at most one entry for the newly added package. - break; + /** + * Store the staged locales info. + */ + private void storeStagedDataInfo(@UserIdInt int userId, @NonNull String packageName, + @NonNull LocalesInfo localesInfo) { + if (DEBUG) { + Slog.d(TAG, "storeStagedDataInfo, userId: " + userId + ", packageName: " + packageName + + ", localesInfo.mLocales: " + localesInfo.mLocales + + ", localesInfo.mSetFromDelegate: " + localesInfo.mSetFromDelegate); + } + String info = + localesInfo.mLocales + STRING_SPLIT + String.valueOf(localesInfo.mSetFromDelegate); + SharedPreferences sp = getStagedDataSp(userId); + // commit and log the result. + if (!sp.edit().putString(packageName, info).commit()) { + Slog.e(TAG, "Failed to commit data!"); + } + } + + /** + * Store the time of creation for staged locales info. + */ + private void storeStagedDataCreatedTime(@UserIdInt int userId) { + SharedPreferences sp = getStagedDataSp(userId); + // commit and log the result. + if (!sp.edit().putLong(KEY_STAGED_DATA_TIME, mClock.millis()).commit()) { + Slog.e(TAG, "Failed to commit data!"); + } + } + + private File getArchivedPackagesFile() { + return mArchivedPackagesFile == null ? new File( + Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM), + ARCHIVED_PACKAGES_PREFS) : mArchivedPackagesFile; + } + + private SharedPreferences getArchivedPackagesSp(File file) { + return mArchivedPackagesFile == null ? mContext.createDeviceProtectedStorageContext() + .getSharedPreferences(file, Context.MODE_PRIVATE) + : mContext.getSharedPreferences(file, Context.MODE_PRIVATE); + } + + /** + * Add the package into the archived packages list. + */ + private void addInArchivedPackagesInfo(@UserIdInt int userId, @NonNull String packageName) { + String user = Integer.toString(userId); + SharedPreferences sp = getArchivedPackagesSp(getArchivedPackagesFile()); + Set<String> packageNames = new ArraySet<>(sp.getStringSet(user, new ArraySet<>())); + if (DEBUG) { + Slog.d(TAG, "addInArchivedPackagesInfo before packageNames: " + packageNames + + ", packageName: " + packageName); + } + if (packageNames.add(packageName)) { + // commit and log the result. + if (!sp.edit().putStringSet(user, packageNames).commit()) { + Slog.e(TAG, "failed to add the package"); + } + } + } + + /** + * Remove the package from the archived packages list. + */ + private void removeFromArchivedPackagesInfo(@UserIdInt int userId, + @NonNull String packageName) { + File file = getArchivedPackagesFile(); + if (file.exists()) { + String user = Integer.toString(userId); + SharedPreferences sp = getArchivedPackagesSp(getArchivedPackagesFile()); + Set<String> packageNames = new ArraySet<>(sp.getStringSet(user, new ArraySet<>())); + if (DEBUG) { + Slog.d(TAG, "removeFromArchivedPackagesInfo before packageNames: " + packageNames + + ", packageName: " + packageName); + } + if (packageNames.remove(packageName)) { + SharedPreferences.Editor editor = sp.edit(); + if (packageNames.isEmpty()) { + if (!editor.remove(user).commit()) { + Slog.e(TAG, "Failed to remove user"); + } + if (sp.getAll().isEmpty()) { + file.delete(); + } + } else { + // commit and log the result. + if (!editor.putStringSet(user, packageNames).commit()) { + Slog.e(TAG, "failed to remove the package"); + } + } } } } + /** + * Remove the user from the archived packages list. + */ + private void removeArchivedPackagesForUser(@UserIdInt int userId) { + String user = Integer.toString(userId); + File file = getArchivedPackagesFile(); + SharedPreferences sp = getArchivedPackagesSp(file); + + if (sp == null || !sp.contains(user)) { + Slog.w(TAG, "The profile is not existed in the archived package info"); + return; + } + + if (!sp.edit().remove(user).commit()) { + Slog.e(TAG, "Failed to remove user"); + } + + if (sp.getAll().isEmpty() && file.exists()) { + file.delete(); + } + } + SharedPreferences createPersistedInfo() { final File prefsFile = new File( Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM), diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index b14702dc6647..b3ab229927fe 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -1243,23 +1243,24 @@ public class LockSettingsService extends ILockSettings.Stub { } } - private void enforceFrpResolved() { + private void enforceFrpNotActive() { final int mainUserId = mInjector.getUserManagerInternal().getMainUserId(); if (mainUserId < 0) { - Slog.d(TAG, "No Main user on device; skipping enforceFrpResolved"); + Slog.d(TAG, "No Main user on device; skipping enforceFrpNotActive"); return; } - final ContentResolver cr = mContext.getContentResolver(); + final ContentResolver cr = mContext.getContentResolver(); final boolean inSetupWizard = Settings.Secure.getIntForUser(cr, Settings.Secure.USER_SETUP_COMPLETE, 0, mainUserId) == 0; - final boolean secureFrp = android.security.Flags.frpEnforcement() + final boolean isFrpActive = android.security.Flags.frpEnforcement() ? mStorage.isFactoryResetProtectionActive() - : (Settings.Global.getInt(cr, Settings.Global.SECURE_FRP_MODE, 0) == 1); + : (Settings.Global.getInt(cr, Settings.Global.SECURE_FRP_MODE, 0) == 1) + && inSetupWizard; - if (inSetupWizard && secureFrp) { - throw new SecurityException("Cannot change credential in SUW while factory reset" - + " protection is not resolved yet"); + if (isFrpActive) { + throw new SecurityException("Cannot change credential while factory reset protection" + + " is active"); } } @@ -1831,7 +1832,7 @@ public class LockSettingsService extends ILockSettings.Stub { final long identity = Binder.clearCallingIdentity(); try { - enforceFrpResolved(); + enforceFrpNotActive(); // When changing credential for profiles with unified challenge, some callers // will pass in empty credential while others will pass in the credential of // the parent user. setLockCredentialInternal() handles the formal case (empty diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java index 363684f618cc..09605fefe80e 100644 --- a/services/core/java/com/android/server/media/MediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java @@ -59,7 +59,7 @@ abstract class MediaRoute2Provider { public abstract void requestCreateSession( long requestId, String packageName, - String routeId, + String routeOriginalId, @Nullable Bundle sessionHints, @RoutingSessionInfo.TransferReason int transferReason, @NonNull UserHandle transferInitiatorUserHandle, @@ -77,13 +77,15 @@ abstract class MediaRoute2Provider { long requestId, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName, - String sessionId, - String routeId, + String sessionOriginalId, + String routeOriginalId, @RoutingSessionInfo.TransferReason int transferReason); - public abstract void setRouteVolume(long requestId, String routeId, int volume); - public abstract void setSessionVolume(long requestId, String sessionId, int volume); - public abstract void prepareReleaseSession(@NonNull String sessionId); + public abstract void setRouteVolume(long requestId, String routeOriginalId, int volume); + + public abstract void setSessionVolume(long requestId, String sessionOriginalId, int volume); + + public abstract void prepareReleaseSession(@NonNull String sessionUniqueId); @NonNull public String getUniqueId() { @@ -197,8 +199,8 @@ abstract class MediaRoute2Provider { */ public final long mRequestId; - /** The {@link MediaRoute2Info#getId() id} of the target route. */ - @NonNull public final String mTargetRouteId; + /** The {@link MediaRoute2Info#getOriginalId()} original id} of the target route. */ + @NonNull public final String mTargetOriginalRouteId; @RoutingSessionInfo.TransferReason public final int mTransferReason; @@ -209,23 +211,23 @@ abstract class MediaRoute2Provider { SessionCreationOrTransferRequest( long requestId, - @NonNull String routeId, + @NonNull String targetOriginalRouteId, @RoutingSessionInfo.TransferReason int transferReason, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName) { mRequestId = requestId; - mTargetRouteId = routeId; + mTargetOriginalRouteId = targetOriginalRouteId; mTransferReason = transferReason; mTransferInitiatorUserHandle = transferInitiatorUserHandle; mTransferInitiatorPackageName = transferInitiatorPackageName; } public boolean isTargetRoute(@Nullable MediaRoute2Info route2Info) { - return route2Info != null && mTargetRouteId.equals(route2Info.getId()); + return route2Info != null && mTargetOriginalRouteId.equals(route2Info.getOriginalId()); } - public boolean isTargetRouteIdInList(@NonNull List<String> routesList) { - return routesList.stream().anyMatch(mTargetRouteId::equals); + public boolean isTargetRouteIdInList(@NonNull List<String> routeOriginalIdList) { + return routeOriginalIdList.stream().anyMatch(mTargetOriginalRouteId::equals); } } } diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index 386657e99e36..71cbcb91100f 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -103,13 +103,14 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider public void requestCreateSession( long requestId, String packageName, - String routeId, + String routeOriginalId, Bundle sessionHints, @RoutingSessionInfo.TransferReason int transferReason, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName) { if (mConnectionReady) { - mActiveConnection.requestCreateSession(requestId, packageName, routeId, sessionHints); + mActiveConnection.requestCreateSession( + requestId, packageName, routeOriginalId, sessionHints); updateBinding(); } } @@ -153,35 +154,35 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider long requestId, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName, - String sessionId, - String routeId, + String sessionOriginalId, + String routeOriginalId, @RoutingSessionInfo.TransferReason int transferReason) { if (mConnectionReady) { - mActiveConnection.transferToRoute(requestId, sessionId, routeId); + mActiveConnection.transferToRoute(requestId, sessionOriginalId, routeOriginalId); } } @Override - public void setRouteVolume(long requestId, String routeId, int volume) { + public void setRouteVolume(long requestId, String routeOriginalId, int volume) { if (mConnectionReady) { - mActiveConnection.setRouteVolume(requestId, routeId, volume); + mActiveConnection.setRouteVolume(requestId, routeOriginalId, volume); updateBinding(); } } @Override - public void setSessionVolume(long requestId, String sessionId, int volume) { + public void setSessionVolume(long requestId, String sessionOriginalId, int volume) { if (mConnectionReady) { - mActiveConnection.setSessionVolume(requestId, sessionId, volume); + mActiveConnection.setSessionVolume(requestId, sessionOriginalId, volume); updateBinding(); } } @Override - public void prepareReleaseSession(@NonNull String sessionId) { + public void prepareReleaseSession(@NonNull String sessionUniqueId) { synchronized (mLock) { for (RoutingSessionInfo session : mSessionInfos) { - if (TextUtils.equals(session.getId(), sessionId)) { + if (TextUtils.equals(session.getId(), sessionUniqueId)) { mSessionInfos.remove(session); mReleasingSessions.add(session); break; diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 76930a003e46..6b409ee6f482 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -158,20 +158,20 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { public void requestCreateSession( long requestId, String packageName, - String routeId, + String routeOriginalId, Bundle sessionHints, @RoutingSessionInfo.TransferReason int transferReason, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName) { // Assume a router without MODIFY_AUDIO_ROUTING permission can't request with // a route ID different from the default route ID. The service should've filtered. - if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { + if (TextUtils.equals(routeOriginalId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { mCallback.onSessionCreated(this, requestId, mDefaultSessionInfo); return; } if (!Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { - if (TextUtils.equals(routeId, mSelectedRouteId)) { + if (TextUtils.equals(routeOriginalId, mSelectedRouteId)) { RoutingSessionInfo currentSessionInfo; synchronized (mLock) { currentSessionInfo = mSessionInfos.get(0); @@ -192,7 +192,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { mPendingSessionCreationOrTransferRequest = new SessionCreationOrTransferRequest( requestId, - routeId, + routeOriginalId, RoutingSessionInfo.TRANSFER_REASON_FALLBACK, transferInitiatorUserHandle, transferInitiatorPackageName); @@ -204,7 +204,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { transferInitiatorUserHandle, transferInitiatorPackageName, SYSTEM_SESSION_ID, - routeId, + routeOriginalId, transferReason); } @@ -234,15 +234,15 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { long requestId, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName, - String sessionId, - String routeId, + String sessionOriginalId, + String routeOriginalId, @RoutingSessionInfo.TransferReason int transferReason) { String selectedDeviceRouteId = mDeviceRouteController.getSelectedRoute().getId(); - if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { + if (TextUtils.equals(routeOriginalId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { // Transfer to the default route (which is the selected route). We replace the id to // be the selected route id so that the transfer reason gets updated. - routeId = selectedDeviceRouteId; + routeOriginalId = selectedDeviceRouteId; } else { Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT); return; @@ -254,18 +254,18 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { mPendingTransferRequest = new SessionCreationOrTransferRequest( requestId, - routeId, + routeOriginalId, transferReason, transferInitiatorUserHandle, transferInitiatorPackageName); } } - String finalRouteId = routeId; // Make a final copy to use it in the lambda. + String finalRouteId = routeOriginalId; // Make a final copy to use it in the lambda. boolean isAvailableDeviceRoute = mDeviceRouteController.getAvailableRoutes().stream() .anyMatch(it -> it.getId().equals(finalRouteId)); - boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRouteId); + boolean isSelectedDeviceRoute = TextUtils.equals(routeOriginalId, selectedDeviceRouteId); if (isSelectedDeviceRoute || isAvailableDeviceRoute) { // The requested route is managed by the device route controller. Note that the selected @@ -273,12 +273,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { // of the routing session). If the selected device route is transferred to, we need to // make the bluetooth routes inactive so that the device route becomes the selected // route of the routing session. - mDeviceRouteController.transferTo(routeId); + mDeviceRouteController.transferTo(routeOriginalId); mBluetoothRouteController.transferTo(null); } else { // The requested route is managed by the bluetooth route controller. mDeviceRouteController.transferTo(null); - mBluetoothRouteController.transferTo(routeId); + mBluetoothRouteController.transferTo(routeOriginalId); } if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses() @@ -288,20 +288,20 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } @Override - public void setRouteVolume(long requestId, String routeId, int volume) { - if (!TextUtils.equals(routeId, mSelectedRouteId)) { + public void setRouteVolume(long requestId, String routeOriginalId, int volume) { + if (!TextUtils.equals(routeOriginalId, mSelectedRouteId)) { return; } mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); } @Override - public void setSessionVolume(long requestId, String sessionId, int volume) { + public void setSessionVolume(long requestId, String sessionOriginalId, int volume) { // Do nothing since we don't support grouping volume yet. } @Override - public void prepareReleaseSession(String sessionId) { + public void prepareReleaseSession(String sessionUniqueId) { // Do nothing since the system session persists. } @@ -503,12 +503,13 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } long pendingRequestId = mPendingSessionCreationOrTransferRequest.mRequestId; - if (mPendingSessionCreationOrTransferRequest.mTargetRouteId.equals(mSelectedRouteId)) { + if (mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId.equals( + mSelectedRouteId)) { if (DEBUG) { Slog.w( TAG, "Session creation success to route " - + mPendingSessionCreationOrTransferRequest.mTargetRouteId); + + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId); } mPendingSessionCreationOrTransferRequest = null; mCallback.onSessionCreated(this, pendingRequestId, newSessionInfo); @@ -520,7 +521,8 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { Slog.w( TAG, "Session creation failed to route " - + mPendingSessionCreationOrTransferRequest.mTargetRouteId); + + mPendingSessionCreationOrTransferRequest + .mTargetOriginalRouteId); } mPendingSessionCreationOrTransferRequest = null; mCallback.onRequestFailed( @@ -529,7 +531,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { Slog.w( TAG, "Session creation waiting state to route " - + mPendingSessionCreationOrTransferRequest.mTargetRouteId); + + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId); } } } @@ -541,7 +543,8 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { // See b/307723189 for context for (MediaRoute2Info btRoute : mBluetoothRouteController.getAllBluetoothRoutes()) { if (TextUtils.equals( - btRoute.getId(), mPendingSessionCreationOrTransferRequest.mTargetRouteId)) { + btRoute.getId(), + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId)) { return true; } } diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java index 9f3104cbd7b0..10169d544b73 100644 --- a/services/core/java/com/android/server/notification/NotificationShellCmd.java +++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java @@ -66,7 +66,7 @@ public class NotificationShellCmd extends ShellCommand { + " disallow_listener COMPONENT [user_id (current user if not specified)]\n" + " allow_assistant COMPONENT [user_id (current user if not specified)]\n" + " remove_assistant COMPONENT [user_id (current user if not specified)]\n" - + " set_dnd [on|none (same as on)|priority|alarms|all|off (same as all)]" + + " set_dnd [on|none (same as on)|priority|alarms|all|off (same as all)]\n" + " allow_dnd PACKAGE [user_id (current user if not specified)]\n" + " disallow_dnd PACKAGE [user_id (current user if not specified)]\n" + " reset_assistant_user_set [user_id (current user if not specified)]\n" diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java index dd7603714718..f540f1db6952 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java @@ -925,10 +925,11 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } } - @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) + /** + * Reset the temporary services set in CTS tests, this method is primarily used to only revert + * the changes caused by CTS tests. + */ public void resetTemporaryServices() { - mContext.enforceCallingPermission( - Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); synchronized (mLock) { if (mTemporaryHandler != null) { mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); diff --git a/services/core/java/com/android/server/pm/InstantAppResolver.java b/services/core/java/com/android/server/pm/InstantAppResolver.java index 92d6a826b5f5..42efd6e60fc0 100644 --- a/services/core/java/com/android/server/pm/InstantAppResolver.java +++ b/services/core/java/com/android/server/pm/InstantAppResolver.java @@ -28,6 +28,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -296,6 +297,9 @@ public abstract class InstantAppResolver { if (needsPhaseTwo) { intent.setAction(Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE); } else { + ActivityOptions options = ActivityOptions.makeBasic() + .setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); // We have all of the data we need; just start the installer without a second phase if (failureIntent != null || installFailureActivity != null) { // Intent that is launched if the package couldn't be installed for any reason. @@ -322,7 +326,7 @@ public abstract class InstantAppResolver { PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE, - null /*bOptions*/, userId); + options.toBundle(), userId); IntentSender failureSender = new IntentSender(failureIntentTarget); // TODO(b/72700831): remove populating old extra intent.putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, failureSender); @@ -342,7 +346,7 @@ public abstract class InstantAppResolver { new String[] { resolvedType }, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE, - null /*bOptions*/, userId); + options.toBundle(), userId); IntentSender successSender = new IntentSender(successIntentTarget); intent.putExtra(Intent.EXTRA_INSTANT_APP_SUCCESS, successSender); } catch (RemoteException ignore) { /* ignore; same process */ } diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index dec97fb588a5..0d1095f5656d 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -704,7 +704,8 @@ public class PackageArchiver { return false; } - if (isAppOptedOutOfArchiving(packageName, ps.getAppId())) { + if (isAppOptedOutOfArchiving(packageName, + UserHandle.getUid(userId, ps.getAppId()))) { return false; } diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 1b6af7170756..efaa7a8598c0 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -3698,7 +3698,7 @@ public class BatteryStatsImpl extends BatteryStats { } return mTotalTimeUs + (mNesting > 0 ? (curBatteryRealtimeUs - mUpdateTimeUs) - / (mTimerPool != null ? mTimerPool.size() : 1) + / (mTimerPool != null && mTimerPool.size() > 0 ? mTimerPool.size() : 1) : 0); } diff --git a/services/core/java/com/android/server/search/SearchManagerService.java b/services/core/java/com/android/server/search/SearchManagerService.java index ecfc040ae29c..9b39fa1e177c 100644 --- a/services/core/java/com/android/server/search/SearchManagerService.java +++ b/services/core/java/com/android/server/search/SearchManagerService.java @@ -61,6 +61,8 @@ public class SearchManagerService extends ISearchManager.Stub { private static final String TAG = "SearchManagerService"; final Handler mHandler; + private final MyPackageMonitor mMyPackageMonitor; + public static class Lifecycle extends SystemService { private SearchManagerService mService; @@ -95,7 +97,8 @@ public class SearchManagerService extends ISearchManager.Stub { */ public SearchManagerService(Context context) { mContext = context; - new MyPackageMonitor().register(context, null, UserHandle.ALL, true); + mMyPackageMonitor = new MyPackageMonitor(); + mMyPackageMonitor.register(context, null, UserHandle.ALL, true); new GlobalSearchProviderObserver(context.getContentResolver()); mHandler = BackgroundThread.getHandler(); } @@ -230,7 +233,6 @@ public class SearchManagerService extends ISearchManager.Stub { if (!shouldRebuildSearchableList(changingUserId)) { return; } - synchronized (mSearchables) { // Invalidate the searchable list. Searchables searchables = mSearchables.get(changingUserId); diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index e00e81371853..f4b61e7b2d4a 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -72,7 +72,6 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; -import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.Xml; @@ -88,6 +87,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.server.SystemService; import com.android.server.servicewatcher.CurrentUserServiceSupplier; import com.android.server.servicewatcher.ServiceWatcher; +import com.android.server.utils.Slogf; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -98,7 +98,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.List; - +import java.util.Objects; /** * Manages trust agents and trust listeners. @@ -362,6 +362,13 @@ public class TrustManagerService extends SystemService { } private void scheduleTrustTimeout(boolean override, boolean isTrustableTimeout) { + if (DEBUG) { + Slogf.d( + TAG, + "scheduleTrustTimeout(override=%s, isTrustable=%s)", + override, + isTrustableTimeout); + } int shouldOverride = override ? 1 : 0; int trustableTimeout = isTrustableTimeout ? 1 : 0; mHandler.obtainMessage(MSG_SCHEDULE_TRUST_TIMEOUT, shouldOverride, @@ -370,6 +377,13 @@ public class TrustManagerService extends SystemService { private void handleScheduleTrustTimeout(boolean shouldOverride, TimeoutType timeoutType) { int userId = mCurrentUser; + if (DEBUG) { + Slogf.d( + TAG, + "handleScheduleTrustTimeout(shouldOverride=%s, timeoutType=%s)", + shouldOverride, + timeoutType); + } if (timeoutType == TimeoutType.TRUSTABLE) { // don't override the hard timeout unless biometric or knowledge factor authentication // occurs which isn't where this is called from. Override the idle timeout what the @@ -383,6 +397,7 @@ public class TrustManagerService extends SystemService { /* Override both the idle and hard trustable timeouts */ private void refreshTrustableTimers(int userId) { + if (DEBUG) Slogf.d(TAG, "refreshTrustableTimers(userId=%s)", userId); handleScheduleTrustableTimeouts(userId, true /* overrideIdleTimeout */, true /* overrideHardTimeout */); } @@ -405,13 +420,20 @@ public class TrustManagerService extends SystemService { } private void handleScheduleTrustedTimeout(int userId, boolean shouldOverride) { + if (DEBUG) { + Slogf.d( + TAG, + "handleScheduleTrustedTimeout(userId=%s, shouldOverride=%s)", + userId, + shouldOverride); + } long when = SystemClock.elapsedRealtime() + TRUST_TIMEOUT_IN_MILLIS; TrustedTimeoutAlarmListener alarm = mTrustTimeoutAlarmListenerForUser.get(userId); // Cancel existing trust timeouts for this user if needed. if (alarm != null) { if (!shouldOverride && alarm.isQueued()) { - if (DEBUG) Slog.d(TAG, "Found existing trust timeout alarm. Skipping."); + if (DEBUG) Slogf.d(TAG, "Found existing trust timeout alarm. Skipping."); return; } mAlarmManager.cancel(alarm); @@ -420,7 +442,9 @@ public class TrustManagerService extends SystemService { mTrustTimeoutAlarmListenerForUser.put(userId, alarm); } - if (DEBUG) Slog.d(TAG, "\tSetting up trust timeout alarm"); + if (DEBUG) { + Slogf.d(TAG, "\tSetting up trust timeout alarm triggering at elapsedRealTime=%s", when); + } alarm.setQueued(true /* isQueued */); mAlarmManager.setExact( AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm, @@ -434,6 +458,13 @@ public class TrustManagerService extends SystemService { } private void setUpIdleTimeout(int userId, boolean overrideIdleTimeout) { + if (DEBUG) { + Slogf.d( + TAG, + "setUpIdleTimeout(userId=%s, overrideIdleTimeout=%s)", + userId, + overrideIdleTimeout); + } long when = SystemClock.elapsedRealtime() + TRUSTABLE_IDLE_TIMEOUT_IN_MILLIS; TrustableTimeoutAlarmListener alarm = mIdleTrustableTimeoutAlarmListenerForUser.get(userId); mContext.enforceCallingOrSelfPermission(Manifest.permission.SCHEDULE_EXACT_ALARM, null); @@ -441,7 +472,7 @@ public class TrustManagerService extends SystemService { // Cancel existing trustable timeouts for this user if needed. if (alarm != null) { if (!overrideIdleTimeout && alarm.isQueued()) { - if (DEBUG) Slog.d(TAG, "Found existing trustable timeout alarm. Skipping."); + if (DEBUG) Slogf.d(TAG, "Found existing trustable timeout alarm. Skipping."); return; } mAlarmManager.cancel(alarm); @@ -450,7 +481,12 @@ public class TrustManagerService extends SystemService { mIdleTrustableTimeoutAlarmListenerForUser.put(userId, alarm); } - if (DEBUG) Slog.d(TAG, "\tSetting up trustable idle timeout alarm"); + if (DEBUG) { + Slogf.d( + TAG, + "\tSetting up trustable idle timeout alarm triggering at elapsedRealTime=%s", + when); + } alarm.setQueued(true /* isQueued */); mAlarmManager.setExact( AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm, @@ -458,6 +494,13 @@ public class TrustManagerService extends SystemService { } private void setUpHardTimeout(int userId, boolean overrideHardTimeout) { + if (DEBUG) { + Slogf.i( + TAG, + "setUpHardTimeout(userId=%s, overrideHardTimeout=%s)", + userId, + overrideHardTimeout); + } mContext.enforceCallingOrSelfPermission(Manifest.permission.SCHEDULE_EXACT_ALARM, null); TrustableTimeoutAlarmListener alarm = mTrustableTimeoutAlarmListenerForUser.get(userId); @@ -472,7 +515,13 @@ public class TrustManagerService extends SystemService { } else if (overrideHardTimeout) { mAlarmManager.cancel(alarm); } - if (DEBUG) Slog.d(TAG, "\tSetting up trustable hard timeout alarm"); + if (DEBUG) { + Slogf.d( + TAG, + "\tSetting up trustable hard timeout alarm triggering at " + + "elapsedRealTime=%s", + when); + } alarm.setQueued(true /* isQueued */); mAlarmManager.setExact( AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm, @@ -503,6 +552,12 @@ public class TrustManagerService extends SystemService { public int hashCode() { return component.hashCode() * 31 + userId; } + + @Override + public String toString() { + return String.format( + "AgentInfo{label=%s, component=%s, userId=%s}", label, component, userId); + } } private void updateTrustAll() { @@ -532,6 +587,15 @@ public class TrustManagerService extends SystemService { int flags, boolean isFromUnlock, @Nullable AndroidFuture<GrantTrustResult> resultCallback) { + if (DEBUG) { + Slogf.d( + TAG, + "updateTrust(userId=%s, flags=%s, isFromUnlock=%s, resultCallbackPresent=%s)", + userId, + flags, + isFromUnlock, + Objects.isNull(resultCallback)); + } boolean managed = aggregateIsTrustManaged(userId); dispatchOnTrustManagedChanged(managed, userId); if (mStrongAuthTracker.isTrustAllowedForUser(userId) @@ -559,27 +623,50 @@ public class TrustManagerService extends SystemService { (flags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) != 0); boolean canMoveToTrusted = alreadyUnlocked || isFromUnlock || renewingTrust || isAutomotive(); - boolean upgradingTrustForCurrentUser = (userId == mCurrentUser); + boolean updatingTrustForCurrentUser = (userId == mCurrentUser); + + if (DEBUG) { + Slogf.d( + TAG, + "updateTrust: alreadyUnlocked=%s, wasTrusted=%s, wasTrustable=%s, " + + "renewingTrust=%s, canMoveToTrusted=%s, " + + "updatingTrustForCurrentUser=%s", + alreadyUnlocked, + wasTrusted, + wasTrustable, + renewingTrust, + canMoveToTrusted, + updatingTrustForCurrentUser); + } if (trustedByAtLeastOneAgent && wasTrusted) { // no change return; - } else if (trustedByAtLeastOneAgent && canMoveToTrusted - && upgradingTrustForCurrentUser) { + } else if (trustedByAtLeastOneAgent + && canMoveToTrusted + && updatingTrustForCurrentUser) { pendingTrustState = TrustState.TRUSTED; - } else if (trustableByAtLeastOneAgent && (wasTrusted || wasTrustable) - && upgradingTrustForCurrentUser) { + } else if (trustableByAtLeastOneAgent + && (wasTrusted || wasTrustable) + && updatingTrustForCurrentUser) { pendingTrustState = TrustState.TRUSTABLE; } else { pendingTrustState = TrustState.UNTRUSTED; } + if (DEBUG) Slogf.d(TAG, "updateTrust: pendingTrustState=%s", pendingTrustState); mUserTrustState.put(userId, pendingTrustState); } - if (DEBUG) Slog.d(TAG, "pendingTrustState: " + pendingTrustState); boolean isNowTrusted = pendingTrustState == TrustState.TRUSTED; boolean newlyUnlocked = !alreadyUnlocked && isNowTrusted; + if (DEBUG) { + Slogf.d( + TAG, + "updateTrust: isNowTrusted=%s, newlyUnlocked=%s", + isNowTrusted, + newlyUnlocked); + } maybeActiveUnlockRunningChanged(userId); dispatchOnTrustChanged( isNowTrusted, newlyUnlocked, userId, flags, getTrustGrantedMessages(userId)); @@ -598,13 +685,13 @@ public class TrustManagerService extends SystemService { boolean shouldSendCallback = newlyUnlocked; if (shouldSendCallback) { if (resultCallback != null) { - if (DEBUG) Slog.d(TAG, "calling back with UNLOCKED_BY_GRANT"); + if (DEBUG) Slogf.d(TAG, "calling back with UNLOCKED_BY_GRANT"); resultCallback.complete(new GrantTrustResult(STATUS_UNLOCKED_BY_GRANT)); } } if ((wasTrusted || wasTrustable) && pendingTrustState == TrustState.UNTRUSTED) { - if (DEBUG) Slog.d(TAG, "Trust was revoked, destroy trustable alarms"); + if (DEBUG) Slogf.d(TAG, "Trust was revoked, destroy trustable alarms"); cancelBothTrustableAlarms(userId); } } @@ -650,7 +737,7 @@ public class TrustManagerService extends SystemService { try { WindowManagerGlobal.getWindowManagerService().lockNow(null); } catch (RemoteException e) { - Slog.e(TAG, "Error locking screen when called from trust agent"); + Slogf.e(TAG, "Error locking screen when called from trust agent"); } } @@ -659,8 +746,9 @@ public class TrustManagerService extends SystemService { } void refreshAgentList(int userIdOrAll) { - if (DEBUG) Slog.d(TAG, "refreshAgentList(" + userIdOrAll + ")"); + if (DEBUG) Slogf.d(TAG, "refreshAgentList(userIdOrAll=%s)", userIdOrAll); if (!mTrustAgentsCanRun) { + if (DEBUG) Slogf.d(TAG, "Did not refresh agent list because agents cannot run."); return; } if (userIdOrAll != UserHandle.USER_ALL && userIdOrAll < UserHandle.USER_SYSTEM) { @@ -686,18 +774,30 @@ public class TrustManagerService extends SystemService { if (userInfo == null || userInfo.partial || !userInfo.isEnabled() || userInfo.guestToRemove) continue; if (!userInfo.supportsSwitchToByUser()) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": switchToByUser=false"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: switchToByUser=false", + userInfo.id); + } continue; } if (!mActivityManager.isUserRunning(userInfo.id)) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": user not started"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: user not started", + userInfo.id); + } continue; } if (!lockPatternUtils.isSecure(userInfo.id)) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": no secure credential"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: no secure credential", + userInfo.id); + } continue; } @@ -708,8 +808,12 @@ public class TrustManagerService extends SystemService { List<ComponentName> enabledAgents = lockPatternUtils.getEnabledTrustAgents(userInfo.id); if (enabledAgents.isEmpty()) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": no agents enabled by user"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: no agents enabled by user", + userInfo.id); + } continue; } List<ResolveInfo> resolveInfos = resolveAllowedTrustAgents(pm, userInfo.id); @@ -717,9 +821,13 @@ public class TrustManagerService extends SystemService { ComponentName name = getComponentName(resolveInfo); if (!enabledAgents.contains(name)) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping " - + name.flattenToShortString() + " u"+ userInfo.id - + ": not enabled by user"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping %s u%s: not enabled by user", + name.flattenToShortString(), + userInfo.id); + } continue; } if (disableTrustAgents) { @@ -727,9 +835,13 @@ public class TrustManagerService extends SystemService { dpm.getTrustAgentConfiguration(null /* admin */, name, userInfo.id); // Disable agent if no features are enabled. if (config == null || config.isEmpty()) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping " - + name.flattenToShortString() + " u"+ userInfo.id - + ": not allowed by DPM"); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping %s u%s: not allowed by DPM", + name.flattenToShortString(), + userInfo.id); + } continue; } } @@ -752,15 +864,26 @@ public class TrustManagerService extends SystemService { } if (directUnlock) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: trustagent " + name - + "of user " + userInfo.id + "can unlock user profile."); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: trustagent %s of user %s can unlock user " + + "profile.", + name, + userInfo.id); + } } if (!mUserManager.isUserUnlockingOrUnlocked(userInfo.id) && !directUnlock) { - if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + "'s trust agent " + name + ": FBE still locked and " - + " the agent cannot unlock user profile."); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s's trust agent %s: FBE still " + + "locked and the agent cannot unlock user profile.", + userInfo.id, + name); + } continue; } @@ -769,11 +892,16 @@ public class TrustManagerService extends SystemService { if (flag != StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) { if (flag != StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT || !directUnlock) { - if (DEBUG) - Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id - + ": prevented by StrongAuthTracker = 0x" - + Integer.toHexString(mStrongAuthTracker.getStrongAuthForUser( - userInfo.id))); + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: skipping user %s: prevented by " + + "StrongAuthTracker = 0x%s", + userInfo.id, + Integer.toHexString( + mStrongAuthTracker.getStrongAuthForUser( + userInfo.id))); + } continue; } } @@ -804,6 +932,15 @@ public class TrustManagerService extends SystemService { } } + if (DEBUG) { + Slogf.d( + TAG, + "refreshAgentList: userInfos=%s, obsoleteAgents=%s, trustMayHaveChanged=%s", + userInfos, + obsoleteAgents, + trustMayHaveChanged); + } + if (trustMayHaveChanged) { if (userIdOrAll == UserHandle.USER_ALL) { updateTrustAll(); @@ -1044,7 +1181,7 @@ public class TrustManagerService extends SystemService { parser = resolveInfo.serviceInfo.loadXmlMetaData(pm, TrustAgentService.TRUST_AGENT_META_DATA); if (parser == null) { - Slog.w(TAG, "Can't find " + TrustAgentService.TRUST_AGENT_META_DATA + " meta-data"); + Slogf.w(TAG, "Can't find %s meta-data", TrustAgentService.TRUST_AGENT_META_DATA); return null; } Resources res = pm.getResourcesForApplication(resolveInfo.serviceInfo.applicationInfo); @@ -1056,7 +1193,7 @@ public class TrustManagerService extends SystemService { } String nodeName = parser.getName(); if (!"trust-agent".equals(nodeName)) { - Slog.w(TAG, "Meta-data does not start with trust-agent tag"); + Slogf.w(TAG, "Meta-data does not start with trust-agent tag"); return null; } TypedArray sa = res @@ -1075,7 +1212,11 @@ public class TrustManagerService extends SystemService { if (parser != null) parser.close(); } if (caughtException != null) { - Slog.w(TAG, "Error parsing : " + resolveInfo.serviceInfo.packageName, caughtException); + Slogf.w( + TAG, + caughtException, + "Error parsing : %s", + resolveInfo.serviceInfo.packageName); return null; } if (cn == null) { @@ -1242,13 +1383,18 @@ public class TrustManagerService extends SystemService { // Agent dispatch and aggregation private boolean aggregateIsTrusted(int userId) { + if (DEBUG) Slogf.d(TAG, "aggregateIsTrusted(userId=%s)", userId); if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) { + if (DEBUG) { + Slogf.d(TAG, "not trusted because trust not allowed for userId=%s", userId); + } return false; } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); if (info.userId == userId) { if (info.agent.isTrusted()) { + if (DEBUG) Slogf.d(TAG, "trusted by %s", info); return true; } } @@ -1257,13 +1403,18 @@ public class TrustManagerService extends SystemService { } private boolean aggregateIsTrustable(int userId) { + if (DEBUG) Slogf.d(TAG, "aggregateIsTrustable(userId=%s)", userId); if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) { + if (DEBUG) { + Slogf.d(TAG, "not trustable because trust not allowed for userId=%s", userId); + } return false; } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); if (info.userId == userId) { if (info.agent.isTrustable()) { + if (DEBUG) Slogf.d(TAG, "trustable by %s", info); return true; } } @@ -1328,20 +1479,31 @@ public class TrustManagerService extends SystemService { private boolean aggregateIsTrustManaged(int userId) { if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) { + if (DEBUG) { + Slogf.d( + TAG, + "trust not managed due to trust not being allowed for userId=%s", + userId); + } return false; } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); if (info.userId == userId) { if (info.agent.isManagingTrust()) { + if (DEBUG) Slogf.d(TAG, "trust managed for userId=%s", userId); return true; } } } + if (DEBUG) Slogf.d(TAG, "trust not managed for userId=%s", userId); return false; } private void dispatchUnlockAttempt(boolean successful, int userId) { + if (DEBUG) { + Slogf.d(TAG, "dispatchUnlockAttempt(successful=%s, userId=%s)", successful, userId); + } if (successful) { mStrongAuthTracker.allowTrustFromUnlock(userId); // Allow the presence of trust on a successful unlock attempt to extend unlock @@ -1359,8 +1521,11 @@ public class TrustManagerService extends SystemService { private void dispatchUserRequestedUnlock(int userId, boolean dismissKeyguard) { if (DEBUG) { - Slog.d(TAG, "dispatchUserRequestedUnlock(user=" + userId + ", dismissKeyguard=" - + dismissKeyguard + ")"); + Slogf.d( + TAG, + "dispatchUserRequestedUnlock(user=%s, dismissKeyguard=%s)", + userId, + dismissKeyguard); } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); @@ -1372,7 +1537,7 @@ public class TrustManagerService extends SystemService { private void dispatchUserMayRequestUnlock(int userId) { if (DEBUG) { - Slog.d(TAG, "dispatchUserMayRequestUnlock(user=" + userId + ")"); + Slogf.d(TAG, "dispatchUserMayRequestUnlock(user=%s)", userId); } for (int i = 0; i < mActiveAgents.size(); i++) { AgentInfo info = mActiveAgents.valueAt(i); @@ -1405,9 +1570,9 @@ public class TrustManagerService extends SystemService { try { listener.onIsActiveUnlockRunningChanged(isRunning, userId); } catch (DeadObjectException e) { - Slog.d(TAG, "TrustListener dead while trying to notify Active Unlock running state"); + Slogf.d(TAG, "TrustListener dead while trying to notify Active Unlock running state"); } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } @@ -1445,11 +1610,11 @@ public class TrustManagerService extends SystemService { mTrustListeners.get(i).onTrustChanged( enabled, newlyUnlocked, userId, flags, trustGrantedMessages); } catch (DeadObjectException e) { - Slog.d(TAG, "Removing dead TrustListener."); + Slogf.d(TAG, "Removing dead TrustListener."); mTrustListeners.remove(i); i--; } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } } @@ -1462,11 +1627,11 @@ public class TrustManagerService extends SystemService { try { mTrustListeners.get(i).onEnabledTrustAgentsChanged(userId); } catch (DeadObjectException e) { - Slog.d(TAG, "Removing dead TrustListener."); + Slogf.d(TAG, "Removing dead TrustListener."); mTrustListeners.remove(i); i--; } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } } @@ -1479,11 +1644,11 @@ public class TrustManagerService extends SystemService { try { mTrustListeners.get(i).onTrustManagedChanged(managed, userId); } catch (DeadObjectException e) { - Slog.d(TAG, "Removing dead TrustListener."); + Slogf.d(TAG, "Removing dead TrustListener."); mTrustListeners.remove(i); i--; } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } } @@ -1496,11 +1661,11 @@ public class TrustManagerService extends SystemService { try { mTrustListeners.get(i).onTrustError(message); } catch (DeadObjectException e) { - Slog.d(TAG, "Removing dead TrustListener."); + Slogf.d(TAG, "Removing dead TrustListener."); mTrustListeners.remove(i); i--; } catch (RemoteException e) { - Slog.e(TAG, "Exception while notifying TrustListener.", e); + Slogf.e(TAG, "Exception while notifying TrustListener.", e); } } } @@ -1535,7 +1700,7 @@ public class TrustManagerService extends SystemService { && mFingerprintManager.hasEnrolledTemplates(userId) && isWeakOrConvenienceSensor( mFingerprintManager.getSensorProperties().get(0))) { - Slog.i(TAG, "User is unlockable by non-strong fingerprint auth"); + Slogf.i(TAG, "User is unlockable by non-strong fingerprint auth"); return true; } @@ -1543,7 +1708,7 @@ public class TrustManagerService extends SystemService { && (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_FACE) == 0 && mFaceManager.hasEnrolledTemplates(userId) && isWeakOrConvenienceSensor(mFaceManager.getSensorProperties().get(0))) { - Slog.i(TAG, "User is unlockable by non-strong face auth"); + Slogf.i(TAG, "User is unlockable by non-strong face auth"); return true; } } @@ -1551,7 +1716,7 @@ public class TrustManagerService extends SystemService { // Check whether it's possible for the device to be actively unlocked by a trust agent. if (getUserTrustStateInner(userId) == TrustState.TRUSTABLE || (isAutomotive() && isTrustUsuallyManagedInternal(userId))) { - Slog.i(TAG, "User is unlockable by trust agent"); + Slogf.i(TAG, "User is unlockable by trust agent"); return true; } @@ -1595,6 +1760,13 @@ public class TrustManagerService extends SystemService { private final IBinder mService = new ITrustManager.Stub() { @Override public void reportUnlockAttempt(boolean authenticated, int userId) throws RemoteException { + if (DEBUG) { + Slogf.d( + TAG, + "reportUnlockAttempt(authenticated=%s, userId=%s)", + authenticated, + userId); + } enforceReportPermission(); mHandler.obtainMessage(MSG_DISPATCH_UNLOCK_ATTEMPT, authenticated ? 1 : 0, userId) .sendToTarget(); @@ -1611,7 +1783,8 @@ public class TrustManagerService extends SystemService { @Override public void reportUserMayRequestUnlock(int userId) throws RemoteException { enforceReportPermission(); - mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId, /*arg2=*/ 0).sendToTarget(); + mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId, /* arg2= */ 0) + .sendToTarget(); } @Override @@ -1932,6 +2105,7 @@ public class TrustManagerService extends SystemService { return new Handler(looper) { @Override public void handleMessage(Message msg) { + if (DEBUG) Slogf.d(TAG, "handler: %s", msg.what); switch (msg.what) { case MSG_REGISTER_LISTENER: addListener((ITrustListener) msg.obj); @@ -2002,8 +2176,24 @@ public class TrustManagerService extends SystemService { handleScheduleTrustTimeout(shouldOverride, timeoutType); break; case MSG_REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH: + if (DEBUG) { + Slogf.d(TAG, "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH userId=%s", msg.arg1); + } TrustableTimeoutAlarmListener trustableAlarm = mTrustableTimeoutAlarmListenerForUser.get(msg.arg1); + if (DEBUG) { + if (trustableAlarm != null) { + Slogf.d( + TAG, + "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH trustable alarm " + + "isQueued=%s", + trustableAlarm.mIsQueued); + } else { + Slogf.d( + TAG, + "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH no trustable alarm"); + } + } if (trustableAlarm != null && trustableAlarm.isQueued()) { refreshTrustableTimers(msg.arg1); } @@ -2194,7 +2384,7 @@ public class TrustManagerService extends SystemService { handleAlarm(); // Only fire if trust can unlock. if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) { - if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout"); + if (DEBUG) Slogf.d(TAG, "Revoking all trust because of trust timeout"); mLockPatternUtils.requireStrongAuth( mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 434e92f7978a..2d2a88a866ba 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3964,7 +3964,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (isCurrentVisible) { - if (isNextNotYetVisible || delayRemoval) { + if (isNextNotYetVisible || delayRemoval || (next != null && isInTransition())) { // Add this activity to the list of stopping activities. It will be processed and // destroyed when the next activity reports idle. addToStopping(false /* scheduleIdle */, false /* idleDelayed */, @@ -7644,6 +7644,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // This could only happen when the window is removed from hierarchy. So do not keep its // reference anymore. mStartingWindow = null; + mStartingData = null; + mStartingSurface = null; } if (mChildren.size() == 0 && mVisibleSetFromTransferredStartingWindow) { // We set the visible state to true for the token from a transferred starting @@ -8551,7 +8553,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds // are already calculated in resolveFixedOrientationConfiguration. // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer. - if (!isLetterboxedForFixedOrientationAndAspectRatio() + if (Flags.immersiveAppRepositioning() && !isLetterboxedForFixedOrientationAndAspectRatio() && !mLetterboxUiController.hasFullscreenOverride()) { resolveAspectRatioRestriction(newParentConfiguration); } @@ -8568,6 +8570,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A computeConfigByResolveHint(resolvedConfig, newParentConfiguration); } } + // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds + // are already calculated in resolveFixedOrientationConfiguration, or if in size compat + // mode, it should already be calculated in resolveSizeCompatModeConfiguration. + // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer. + if (!Flags.immersiveAppRepositioning() && !isLetterboxedForFixedOrientationAndAspectRatio() + && !mInSizeCompatModeForBounds && !mLetterboxUiController.hasFullscreenOverride()) { + resolveAspectRatioRestriction(newParentConfiguration); + } if (isFixedOrientationLetterboxAllowed || compatDisplayInsets != null // In fullscreen, can be letterboxed for aspect ratio. @@ -8903,7 +8913,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } boolean isImmersiveMode(@NonNull Rect parentBounds) { - if (!mResolveConfigHint.mUseOverrideInsetsForConfig) { + if (!Flags.immersiveAppRepositioning()) { + return false; + } + if (!mResolveConfigHint.mUseOverrideInsetsForConfig + && mWmService.mFlags.mInsetsDecoupledConfiguration) { return false; } final Insets navBarInsets = mDisplayContent.getInsetsStateController() @@ -9247,17 +9261,35 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @NonNull CompatDisplayInsets compatDisplayInsets) { final Configuration resolvedConfig = getResolvedOverrideConfiguration(); final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); + final Insets insets; + if (mResolveConfigHint.mUseOverrideInsetsForConfig) { + // TODO(b/343197837): Add test to verify SCM behaviour with new bound configuration + // Insets are decoupled from configuration by default from V+, use legacy + // compatibility behaviour for apps targeting SDK earlier than 35 + // (see applySizeOverrideIfNeeded). + insets = Insets.of(mDisplayContent.getDisplayPolicy() + .getDecorInsetsInfo(mDisplayContent.mDisplayFrames.mRotation, + mDisplayContent.mDisplayFrames.mWidth, + mDisplayContent.mDisplayFrames.mHeight).mOverrideNonDecorInsets); + } else { + insets = Insets.NONE; + } // When an activity needs to be letterboxed because of fixed orientation, use fixed // orientation bounds (stored in resolved bounds) instead of parent bounds since the // activity will be displayed within them even if it is in size compat mode. They should be // saved here before resolved bounds are overridden below. - final Rect containerBounds = isAspectRatioApplied() + final boolean useResolvedBounds = Flags.immersiveAppRepositioning() + ? isAspectRatioApplied() : isLetterboxedForFixedOrientationAndAspectRatio(); + final Rect containerBounds = useResolvedBounds ? new Rect(resolvedBounds) : newParentConfiguration.windowConfiguration.getBounds(); - final Rect containerAppBounds = isAspectRatioApplied() + final Rect parentAppBounds = + newParentConfiguration.windowConfiguration.getAppBounds(); + parentAppBounds.inset(insets); + final Rect containerAppBounds = useResolvedBounds ? new Rect(resolvedConfig.windowConfiguration.getAppBounds()) - : newParentConfiguration.windowConfiguration.getAppBounds(); + : parentAppBounds; final int requestedOrientation = getRequestedConfigurationOrientation(); final boolean orientationRequested = requestedOrientation != ORIENTATION_UNDEFINED; diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 72f592b577da..e07b72a05123 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -33,6 +33,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.Process.SYSTEM_UID; import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE; +import static android.view.WindowInsets.Type.mandatorySystemGestures; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; @@ -60,6 +61,8 @@ import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.Rect; import android.os.Environment; import android.os.IBinder; import android.os.RemoteException; @@ -71,7 +74,9 @@ import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; +import android.view.InsetsState; import android.view.MotionEvent; +import android.view.WindowInsets; import android.view.WindowManagerPolicyConstants.PointerEventListener; import com.android.internal.annotations.VisibleForTesting; @@ -208,6 +213,7 @@ class RecentTasks { private final HashMap<ComponentName, ActivityInfo> mTmpAvailActCache = new HashMap<>(); private final HashMap<String, ApplicationInfo> mTmpAvailAppCache = new HashMap<>(); private final SparseBooleanArray mTmpQuietProfileUserIds = new SparseBooleanArray(); + private final Rect mTmpRect = new Rect(); // TODO(b/127498985): This is currently a rough heuristic for interaction inside an app private final PointerEventListener mListener = new PointerEventListener() { @@ -229,12 +235,27 @@ class RecentTasks { if (win == null) { return; } + + // Verify the touch is within the mandatory system gesture inset bounds of the + // window, use the raw insets state to ignore window z-order + final InsetsState insetsState = dc.getInsetsStateController() + .getRawInsetsState(); + mTmpRect.set(win.getFrame()); + mTmpRect.inset(insetsState.calculateInsets(win.getFrame(), + mandatorySystemGestures(), false /* ignoreVisibility */)); + if (!mTmpRect.contains(x, y)) { + return; + } + // Unfreeze the task list once we touch down in a task final boolean isAppWindowTouch = FIRST_APPLICATION_WINDOW <= win.mAttrs.type && win.mAttrs.type <= LAST_APPLICATION_WINDOW; if (isAppWindowTouch) { final Task stack = mService.getTopDisplayFocusedRootTask(); final Task topTask = stack != null ? stack.getTopMostTask() : null; + ProtoLog.i(WM_DEBUG_TASKS, "Resetting frozen recents task list" + + " reason=app touch win=%s x=%d y=%d insetFrame=%s", win, x, y, + mTmpRect); resetFreezeTaskListReordering(topTask); } } @@ -301,6 +322,8 @@ class RecentTasks { mFreezeTaskListReordering = true; } + ProtoLog.i(WM_DEBUG_TASKS, "Setting frozen recents task list"); + // Always update the reordering time when this is called to ensure that the timeout // is reset mService.mH.removeCallbacks(mResetFreezeTaskListOnTimeoutRunnable); @@ -344,6 +367,7 @@ class RecentTasks { final Task focusedStack = mService.getTopDisplayFocusedRootTask(); final Task topTask = focusedStack != null ? focusedStack.getTopMostTask() : null; final Task reorderToEndTask = topTask != null && topTask.hasChild() ? topTask : null; + ProtoLog.i(WM_DEBUG_TASKS, "Resetting frozen recents task list reason=timeout"); resetFreezeTaskListReordering(reorderToEndTask); } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 9dc9ad4a2ba2..6c48e9586fd9 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -1931,6 +1931,9 @@ class Task extends TaskFragment { if (td.getSystemBarsAppearance() == 0) { td.setSystemBarsAppearance(atd.getSystemBarsAppearance()); } + if (td.getTopOpaqueSystemBarsAppearance() == 0 && r.fillsParent()) { + td.setTopOpaqueSystemBarsAppearance(atd.getSystemBarsAppearance()); + } if (td.getNavigationBarColor() == 0) { td.setNavigationBarColor(atd.getNavigationBarColor()); td.setEnsureNavigationBarContrastWhenTransparent( diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index d8e4aa25dd77..c972eee84ea3 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -3787,7 +3787,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (changeInfo.mRotation != wc.mDisplayContent.getRotation()) { // This isn't cheap, so only do it for rotation change. changeInfo.mSnapshotLuma = TransitionAnimation.getBorderLuma( - buffer, screenshotBuffer.getColorSpace()); + buffer, screenshotBuffer.getColorSpace(), wc.mSurfaceControl); } SurfaceControl.Transaction t = wc.mWmService.mTransactionFactory.get(); TransitionAnimation.configureScreenshotLayer(t, snapshotSurface, screenshotBuffer); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 0bf1c88d5b4f..a00b6fc47de7 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1525,7 +1525,7 @@ public class WindowManagerService extends IWindowManager.Stub InputChannel outInputChannel, InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { - outActiveControls.set(null); + outActiveControls.set(null, false /* copyControls */); int[] appOp = new int[1]; final boolean isRoundedCornerOverlay = (attrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0; @@ -1927,7 +1927,7 @@ public class WindowManagerService extends IWindowManager.Stub displayContent.getInsetsStateController().updateAboveInsetsState( false /* notifyInsetsChanged */); - outInsetsState.set(win.getCompatInsetsState(), true /* copySources */); + win.fillInsetsState(outInsetsState, true /* copySources */); getInsetsSourceControls(win, outActiveControls); if (win.mLayoutAttached) { @@ -2317,7 +2317,7 @@ public class WindowManagerService extends IWindowManager.Stub InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls, Bundle outBundle, WindowRelayoutResult outRelayoutResult) { if (outActiveControls != null) { - outActiveControls.set(null); + outActiveControls.set(null, false /* copyControls */); } int result = 0; boolean configChanged = false; @@ -2680,7 +2680,7 @@ public class WindowManagerService extends IWindowManager.Stub } if (outInsetsState != null) { - outInsetsState.set(win.getCompatInsetsState(), true /* copySources */); + win.fillInsetsState(outInsetsState, true /* copySources */); } ProtoLog.v(WM_DEBUG_FOCUS, "Relayout of %s: focusMayChange=%b", @@ -2743,25 +2743,14 @@ public class WindowManagerService extends IWindowManager.Stub } private void getInsetsSourceControls(WindowState win, InsetsSourceControl.Array outArray) { - final InsetsSourceControl[] controls = - win.getDisplayContent().getInsetsStateController().getControlsForDispatch(win); - if (controls != null) { - final int length = controls.length; - final InsetsSourceControl[] outControls = new InsetsSourceControl[length]; - for (int i = 0; i < length; i++) { - // We will leave the critical section before returning the leash to the client, - // so we need to copy the leash to prevent others release the one that we are - // about to return. - if (controls[i] != null) { - // This source control is an extra copy if the client is not local. By setting - // PARCELABLE_WRITE_RETURN_VALUE, the leash will be released at the end of - // SurfaceControl.writeToParcel. - outControls[i] = new InsetsSourceControl(controls[i]); - outControls[i].setParcelableFlags(PARCELABLE_WRITE_RETURN_VALUE); - } - } - outArray.set(outControls); - } + // We will leave the critical section before returning the leash to the client, + // so we need to copy the leash to prevent others release the one that we are + // about to return. + win.fillInsetsSourceControls(outArray, true /* copyControls */); + // This source control is an extra copy if the client is not local. By setting + // PARCELABLE_WRITE_RETURN_VALUE, the leash will be released at the end of + // SurfaceControl.writeToParcel. + outArray.setParcelableFlags(PARCELABLE_WRITE_RETURN_VALUE); } private void tryStartExitingAnimation(WindowState win, WindowStateAnimator winAnimator) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 6953c60d0d74..d1efc27d9661 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -434,7 +434,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP /** @see #isLastConfigReportedToClient() */ private boolean mLastConfigReportedToClient; - // TODO(b/339380439): Ensure to use the same object for IWindowSession#relayout + private final ClientWindowFrames mLastReportedFrames = new ClientWindowFrames(); + + private final InsetsState mLastReportedInsetsState = new InsetsState(); + private final InsetsSourceControl.Array mLastReportedActiveControls = new InsetsSourceControl.Array(); @@ -495,8 +498,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP private final WindowFrames mWindowFrames = new WindowFrames(); - private final ClientWindowFrames mClientWindowFrames = new ClientWindowFrames(); - /** * List of rects where system gestures should be ignored. * @@ -3650,8 +3651,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP outFrames.attachedFrame.scale(mInvGlobalScale); } } - outFrames.compatScale = getCompatScaleForClient(); + if (mLastReportedFrames != outFrames) { + mLastReportedFrames.setTo(outFrames); + } // Note: in the cases where the window is tied to an activity, we should not send a // configuration update when the window has requested to be hidden. Doing so can lead to @@ -3678,6 +3681,25 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mLastConfigReportedToClient = true; } + void fillInsetsState(@NonNull InsetsState outInsetsState, boolean copySources) { + outInsetsState.set(getCompatInsetsState(), copySources); + if (outInsetsState != mLastReportedInsetsState) { + // No need to copy for the recorded. + mLastReportedInsetsState.set(outInsetsState, false /* copySources */); + } + } + + void fillInsetsSourceControls(@NonNull InsetsSourceControl.Array outArray, + boolean copyControls) { + final InsetsSourceControl[] controls = + getDisplayContent().getInsetsStateController().getControlsForDispatch(this); + outArray.set(controls, copyControls); + if (outArray != mLastReportedActiveControls) { + // No need to copy for the recorded. + mLastReportedActiveControls.setTo(outArray, false /* copyControls */); + } + } + void reportResized() { // If the activity is scheduled to relaunch, skip sending the resized to ViewRootImpl now // since it will be destroyed anyway. This also prevents the client from receiving @@ -3712,9 +3734,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int prevRotation = mLastReportedConfiguration .getMergedConfiguration().windowConfiguration.getRotation(); - fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration, + fillClientWindowFramesAndConfiguration(mLastReportedFrames, mLastReportedConfiguration, mLastReportedActivityWindowInfo, true /* useLatestConfig */, false /* relayoutVisible */); + fillInsetsState(mLastReportedInsetsState, false /* copySources */); final boolean syncRedraw = shouldSendRedrawForSync(); final boolean syncWithBuffers = syncRedraw && shouldSyncWithBuffers(); final boolean reportDraw = syncRedraw || drawPending; @@ -3734,8 +3757,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (Flags.bundleClientTransactionFlag()) { getProcess().scheduleClientTransactionItem( - WindowStateResizeItem.obtain(mClient, mClientWindowFrames, reportDraw, - mLastReportedConfiguration, getCompatInsetsState(), forceRelayout, + WindowStateResizeItem.obtain(mClient, mLastReportedFrames, reportDraw, + mLastReportedConfiguration, mLastReportedInsetsState, forceRelayout, alwaysConsumeSystemBars, displayId, syncWithBuffers ? mSyncSeqId : -1, isDragResizing, mLastReportedActivityWindowInfo)); @@ -3743,8 +3766,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } else { // TODO(b/301870955): cleanup after launch try { - mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration, - getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId, + mClient.resized(mLastReportedFrames, reportDraw, mLastReportedConfiguration, + mLastReportedInsetsState, forceRelayout, alwaysConsumeSystemBars, displayId, syncWithBuffers ? mSyncSeqId : -1, isDragResizing, mLastReportedActivityWindowInfo); onResizePostDispatched(drawPending, prevRotation, displayId); @@ -3817,16 +3840,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mRemoved) { return; } - final InsetsStateController stateController = - getDisplayContent().getInsetsStateController(); - final InsetsState insetsState = getCompatInsetsState(); - mLastReportedActiveControls.set(stateController.getControlsForDispatch(this)); + fillInsetsState(mLastReportedInsetsState, false /* copySources */); + fillInsetsSourceControls(mLastReportedActiveControls, false /* copyControls */); if (Flags.insetsControlChangedItem()) { getProcess().scheduleClientTransactionItem(WindowStateInsetsControlChangeItem.obtain( - mClient, insetsState, mLastReportedActiveControls)); + mClient, mLastReportedInsetsState, mLastReportedActiveControls)); } else { try { - mClient.insetsControlChanged(insetsState, mLastReportedActiveControls); + mClient.insetsControlChanged(mLastReportedInsetsState, mLastReportedActiveControls); } catch (RemoteException e) { Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e); } diff --git a/services/core/jni/BroadcastRadio/convert.cpp b/services/core/jni/BroadcastRadio/convert.cpp index ddbc5354358c..e42f7f8be0ca 100644 --- a/services/core/jni/BroadcastRadio/convert.cpp +++ b/services/core/jni/BroadcastRadio/convert.cpp @@ -433,7 +433,7 @@ static JavaRef<jobject> BandDescriptorFromHal(JNIEnv *env, const V1_0::BandConfi gjni.AmBandDescriptor.clazz, gjni.AmBandDescriptor.cstor, region, config.type, config.lowerLimit, config.upperLimit, spacing, am.stereo)); } else { - ALOGE("Unsupported band type: %d", config.type); + ALOGE("Unsupported band type: %d", static_cast<int>(config.type)); return nullptr; } } @@ -451,7 +451,7 @@ JavaRef<jobject> BandConfigFromHal(JNIEnv *env, const V1_0::BandConfig &config, return make_javaref(env, env->NewObject( gjni.AmBandConfig.clazz, gjni.AmBandConfig.cstor, descriptor.get())); } else { - ALOGE("Unsupported band type: %d", config.type); + ALOGE("Unsupported band type: %d", static_cast<int>(config.type)); return nullptr; } } @@ -539,9 +539,9 @@ JavaRef<jobject> MetadataFromHal(JNIEnv *env, const hidl_vec<V1_0::MetaData> &me item.clockValue.timezoneOffsetInMinutes); break; default: - ALOGW("invalid metadata type %d", item.type); + ALOGW("invalid metadata type %d", static_cast<int>(item.type)); } - ALOGE_IF(status != 0, "Failed inserting metadata %d (of type %d)", key, item.type); + ALOGE_IF(status != 0, "Failed inserting metadata %d (of type %d)", key, static_cast<int>(item.type)); } return jMetadata; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index e763c9eccceb..669a999c921e 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -1588,7 +1588,7 @@ final class DevicePolicyEngine { private Set<EnforcingAdmin> getEnforcingAdminsOnUser(int userId) { synchronized (mLock) { return mEnforcingAdmins.contains(userId) - ? mEnforcingAdmins.get(userId) : Collections.emptySet(); + ? new HashSet<>(mEnforcingAdmins.get(userId)) : Collections.emptySet(); } } diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java index c16c61271280..cc340c0a5f79 100644 --- a/services/midi/java/com/android/server/midi/MidiService.java +++ b/services/midi/java/com/android/server/midi/MidiService.java @@ -287,6 +287,7 @@ public class MidiService extends IMidiManager.Stub { } public void deviceAdded(Device device) { + Log.d(TAG, "deviceAdded() " + device.getUserId() + " userId:" + getUserId()); // ignore devices that our client cannot access if (!device.isUidAllowed(mUid) || !device.isUserIdAllowed(getUserId())) return; @@ -301,6 +302,7 @@ public class MidiService extends IMidiManager.Stub { } public void deviceRemoved(Device device) { + Log.d(TAG, "deviceRemoved() " + device.getUserId() + " userId:" + getUserId()); // ignore devices that our client cannot access if (!device.isUidAllowed(mUid) || !device.isUserIdAllowed(getUserId())) return; @@ -315,6 +317,7 @@ public class MidiService extends IMidiManager.Stub { } public void deviceStatusChanged(Device device, MidiDeviceStatus status) { + Log.d(TAG, "deviceStatusChanged() " + device.getUserId() + " userId:" + getUserId()); // ignore devices that our client cannot access if (!device.isUidAllowed(mUid) || !device.isUserIdAllowed(getUserId())) return; @@ -1303,7 +1306,7 @@ public class MidiService extends IMidiManager.Stub { String[] inputPortNames, String[] outputPortNames, Bundle properties, IMidiDeviceServer server, ServiceInfo serviceInfo, boolean isPrivate, int uid, int defaultProtocol, int userId) { - Log.d(TAG, "addDeviceLocked()" + uid + " type:" + type); + Log.d(TAG, "addDeviceLocked() " + uid + " type:" + type + " userId:" + userId); // Limit the number of devices per app. int deviceCountForApp = 0; diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java index 488fe57cf6f8..9f9764853bef 100644 --- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java +++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java @@ -370,18 +370,18 @@ public final class ProfcollectForwardingService extends SystemService { } private static void createAndUploadReport(ProfcollectForwardingService pfs) { - String reportName; - try { - reportName = pfs.mIProfcollect.report(pfs.mUsageSetting) + ".zip"; - } catch (RemoteException e) { - Log.e(LOG_TAG, "Failed to create report: " + e.getMessage()); - return; - } - if (!pfs.mUploadEnabled) { - Log.i(LOG_TAG, "Upload is not enabled."); - return; - } BackgroundThread.get().getThreadHandler().post(() -> { + String reportName; + try { + reportName = pfs.mIProfcollect.report(pfs.mUsageSetting) + ".zip"; + } catch (RemoteException e) { + Log.e(LOG_TAG, "Failed to create report: " + e.getMessage()); + return; + } + if (!pfs.mUploadEnabled) { + Log.i(LOG_TAG, "Upload is not enabled."); + return; + } Intent intent = new Intent() .setPackage("com.android.shell") .setAction("com.android.shell.action.PROFCOLLECT_UPLOAD") diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 98f572d81adf..8fd1e6baf522 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -1538,6 +1538,8 @@ public final class DisplayPowerControllerTest { @Test public void testDozeScreenStateOverride_toSupportedOffloadStateFromDoze_DisplayStateChanges() { + when(mDisplayManagerFlagsMock.isOffloadDozeOverrideHoldsWakelockEnabled()).thenReturn(true); + // set up. int initState = Display.STATE_DOZE; int supportedTargetState = Display.STATE_DOZE_SUSPEND; @@ -1556,10 +1558,15 @@ public final class DisplayPowerControllerTest { mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); advanceTime(1); // Run updatePowerState + reset(mHolder.wakelockController); mHolder.dpc.overrideDozeScreenState( supportedTargetState, Display.STATE_REASON_DEFAULT_POLICY); advanceTime(1); // Run updatePowerState + // Should get a wakelock to notify powermanager + verify(mHolder.wakelockController, atLeastOnce()).acquireWakelock( + eq(WakelockController.WAKE_LOCK_UNFINISHED_BUSINESS)); + verify(mHolder.displayPowerState) .setScreenState(supportedTargetState, Display.STATE_REASON_DEFAULT_POLICY); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index 12050e1beaed..01ff35fc088c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -1142,6 +1142,20 @@ public class LocalDisplayAdapterTest { } @Test + public void test_createLocalExternalDisplay_displayManagementEnabled_doesNotCrash() + throws Exception { + FakeDisplay display = new FakeDisplay(PORT_A); + display.info.isInternal = false; + setUpDisplay(display); + updateAvailableDisplays(); + mAdapter.registerLocked(); + when(mSurfaceControlProxy.getDesiredDisplayModeSpecs(display.token)).thenReturn(null); + mInjector.getTransmitter().sendHotplug(display, /* connected */ true); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + assertThat(mListener.addedDisplays.size()).isEqualTo(1); + } + + @Test public void test_createLocalExternalDisplay_displayManagementEnabled_shouldHaveDefaultGroup() throws Exception { FakeDisplay display = new FakeDisplay(PORT_A); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java index 52b33db556e6..d4f0d5aa7ef6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java @@ -593,6 +593,12 @@ public class ProxyManagerTest { } @Override + public void onMagnificationSystemUIConnectionChanged(boolean connected) + throws RemoteException { + + } + + @Override public void onMagnificationChanged(int displayId, Region region, MagnificationConfig config) throws RemoteException { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java index 87fe6cf8f283..0de5807067e6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java @@ -59,6 +59,7 @@ import android.view.accessibility.MagnificationAnimationCallback; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.FlakyTest; +import com.android.compatibility.common.util.TestUtils; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityTraceManager; @@ -76,6 +77,7 @@ import org.mockito.invocation.InvocationOnMock; */ public class MagnificationConnectionManagerTest { + private static final int WAIT_CONNECTION_TIMEOUT_SECOND = 1; private static final int CURRENT_USER_ID = UserHandle.USER_SYSTEM; private static final int SERVICE_ID = 1; @@ -115,17 +117,19 @@ public class MagnificationConnectionManagerTest { private void stubSetConnection(boolean needDelay) { doAnswer((InvocationOnMock invocation) -> { final boolean connect = (Boolean) invocation.getArguments()[0]; - // Simulates setConnection() called by another process. + // Use post to simulate setConnection() called by another process. + final Context context = ApplicationProvider.getApplicationContext(); if (needDelay) { - final Context context = ApplicationProvider.getApplicationContext(); context.getMainThreadHandler().postDelayed( () -> { mMagnificationConnectionManager.setConnection( connect ? mMockConnection.getConnection() : null); }, 10); } else { - mMagnificationConnectionManager.setConnection( - connect ? mMockConnection.getConnection() : null); + context.getMainThreadHandler().post(() -> { + mMagnificationConnectionManager.setConnection( + connect ? mMockConnection.getConnection() : null); + }); } return true; }).when(mMockStatusBarManagerInternal).requestMagnificationConnection(anyBoolean()); @@ -629,9 +633,10 @@ public class MagnificationConnectionManagerTest { } @Test - public void isConnected_requestConnection_expectedValue() throws RemoteException { + public void isConnected_requestConnection_expectedValue() throws Exception { mMagnificationConnectionManager.requestConnection(true); - assertTrue(mMagnificationConnectionManager.isConnected()); + TestUtils.waitUntil("connection is not ready", WAIT_CONNECTION_TIMEOUT_SECOND, + () -> mMagnificationConnectionManager.isConnected()); mMagnificationConnectionManager.requestConnection(false); assertFalse(mMagnificationConnectionManager.isConnected()); diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java index e756082bc912..758c84a26dcd 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.AppOpsManager; import android.content.Context; import android.content.res.Resources; import android.media.AudioDeviceAttributes; @@ -37,6 +38,7 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.IAudioDeviceVolumeDispatcher; import android.media.VolumeInfo; +import android.os.PermissionEnforcer; import android.os.RemoteException; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -98,7 +100,8 @@ public class AbsoluteVolumeBehaviorTest { mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, - mTestLooper.getLooper()) { + mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), + mock(AudioServerPermissionProvider.class)) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java index 3623012b348f..2cb02bdd2806 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java @@ -23,12 +23,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.app.AppOpsManager; import android.content.Context; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.AudioSystem; import android.media.VolumeInfo; +import android.os.PermissionEnforcer; import android.os.test.TestLooper; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; @@ -75,7 +77,8 @@ public class AudioDeviceVolumeManagerTest { mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase(); mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, - mTestLooper.getLooper()) { + mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), + mock(AudioServerPermissionProvider.class)) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java new file mode 100644 index 000000000000..8d772ad5c124 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java @@ -0,0 +1,308 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.audio; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.media.permission.INativePermissionController; +import com.android.media.permission.UidPackageState; +import com.android.server.pm.pkg.PackageState; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@RunWith(AndroidJUnit4.class) +@Presubmit +public final class AudioServerPermissionProviderTest { + + // Class under test + private AudioServerPermissionProvider mPermissionProvider; + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock public INativePermissionController mMockPc; + + @Mock public PackageState mMockPackageStateOne_10000_one; + @Mock public PackageState mMockPackageStateTwo_10001_two; + @Mock public PackageState mMockPackageStateThree_10000_one; + @Mock public PackageState mMockPackageStateFour_10000_three; + @Mock public PackageState mMockPackageStateFive_10001_four; + @Mock public PackageState mMockPackageStateSix_10000_two; + + public List<UidPackageState> mInitPackageListExpected; + + // Argument matcher which matches that the state is equal even if the package names are out of + // order (since they are logically a set). + public static final class UidPackageStateMatcher implements ArgumentMatcher<UidPackageState> { + private final int mUid; + private final List<String> mSortedPackages; + + public UidPackageStateMatcher(int uid, List<String> packageNames) { + mUid = uid; + if (packageNames != null) { + mSortedPackages = new ArrayList(packageNames); + Collections.sort(mSortedPackages); + } else { + mSortedPackages = null; + } + } + + public UidPackageStateMatcher(UidPackageState toMatch) { + this(toMatch.uid, toMatch.packageNames); + } + + @Override + public boolean matches(UidPackageState state) { + if (state == null) return false; + if (state.uid != mUid) return false; + if ((state.packageNames == null) != (mSortedPackages == null)) return false; + var copy = new ArrayList(state.packageNames); + Collections.sort(copy); + return mSortedPackages.equals(copy); + } + + @Override + public String toString() { + return "Matcher for UidState with uid: " + mUid + ": " + mSortedPackages; + } + } + + public static final class PackageStateListMatcher + implements ArgumentMatcher<List<UidPackageState>> { + + private final List<UidPackageState> mToMatch; + + public PackageStateListMatcher(List<UidPackageState> toMatch) { + mToMatch = Objects.requireNonNull(toMatch); + } + + @Override + public boolean matches(List<UidPackageState> other) { + if (other == null) return false; + if (other.size() != mToMatch.size()) return false; + for (int i = 0; i < mToMatch.size(); i++) { + var matcher = new UidPackageStateMatcher(mToMatch.get(i)); + if (!matcher.matches(other.get(i))) return false; + } + return true; + } + + @Override + public String toString() { + return "Matcher for List<UidState> with uid: " + mToMatch; + } + } + + @Before + public void setup() { + when(mMockPackageStateOne_10000_one.getAppId()).thenReturn(10000); + when(mMockPackageStateOne_10000_one.getPackageName()).thenReturn("com.package.one"); + + when(mMockPackageStateTwo_10001_two.getAppId()).thenReturn(10001); + when(mMockPackageStateTwo_10001_two.getPackageName()).thenReturn("com.package.two"); + + // Same state as the first is intentional, emulating multi-user + when(mMockPackageStateThree_10000_one.getAppId()).thenReturn(10000); + when(mMockPackageStateThree_10000_one.getPackageName()).thenReturn("com.package.one"); + + when(mMockPackageStateFour_10000_three.getAppId()).thenReturn(10000); + when(mMockPackageStateFour_10000_three.getPackageName()).thenReturn("com.package.three"); + + when(mMockPackageStateFive_10001_four.getAppId()).thenReturn(10001); + when(mMockPackageStateFive_10001_four.getPackageName()).thenReturn("com.package.four"); + + when(mMockPackageStateSix_10000_two.getAppId()).thenReturn(10000); + when(mMockPackageStateSix_10000_two.getPackageName()).thenReturn("com.package.two"); + } + + @Test + public void testInitialPackagePopulation() throws Exception { + var initPackageListData = + List.of( + mMockPackageStateOne_10000_one, + mMockPackageStateTwo_10001_two, + mMockPackageStateThree_10000_one, + mMockPackageStateFour_10000_three, + mMockPackageStateFive_10001_four, + mMockPackageStateSix_10000_two); + var expectedPackageList = + List.of( + createUidPackageState( + 10000, + List.of("com.package.one", "com.package.two", "com.package.three")), + createUidPackageState( + 10001, List.of("com.package.two", "com.package.four"))); + + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + verify(mMockPc) + .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList))); + } + + @Test + public void testOnModifyPackageState_whenNewUid() throws Exception { + // 10000: one | 10001: two + var initPackageListData = + List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // new uid, including user component + mPermissionProvider.onModifyPackageState(1_10002, "com.package.new", false /* isRemove */); + + verify(mMockPc) + .updatePackagesForUid( + argThat(new UidPackageStateMatcher(10002, List.of("com.package.new")))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnModifyPackageState_whenRemoveUid() throws Exception { + // 10000: one | 10001: two + var initPackageListData = + List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // Includes user-id + mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */); + + verify(mMockPc).updatePackagesForUid(argThat(new UidPackageStateMatcher(10000, List.of()))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnModifyPackageState_whenUpdatedUidAddition() throws Exception { + // 10000: one | 10001: two + var initPackageListData = + List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // Includes user-id + mPermissionProvider.onModifyPackageState(1_10000, "com.package.new", false /* isRemove */); + + verify(mMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + 10000, List.of("com.package.one", "com.package.new")))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnModifyPackageState_whenUpdateUidRemoval() throws Exception { + // 10000: one, two | 10001: two + var initPackageListData = + List.of( + mMockPackageStateOne_10000_one, + mMockPackageStateTwo_10001_two, + mMockPackageStateSix_10000_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + + // Includes user-id + mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */); + + verify(mMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + createUidPackageState(10000, List.of("com.package.two"))))); + verify(mMockPc).updatePackagesForUid(any()); // exactly once + } + + @Test + public void testOnServiceStart() throws Exception { + // 10000: one, two | 10001: two + var initPackageListData = + List.of( + mMockPackageStateOne_10000_one, + mMockPackageStateTwo_10001_two, + mMockPackageStateSix_10000_two); + mPermissionProvider = new AudioServerPermissionProvider(initPackageListData); + mPermissionProvider.onServiceStart(mMockPc); + mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */); + verify(mMockPc) + .updatePackagesForUid( + argThat(new UidPackageStateMatcher(10000, List.of("com.package.two")))); + + verify(mMockPc).updatePackagesForUid(any()); // exactly once + mPermissionProvider.onModifyPackageState( + 1_10000, "com.package.three", false /* isRemove */); + verify(mMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + 10000, List.of("com.package.two", "com.package.three")))); + verify(mMockPc, times(2)).updatePackagesForUid(any()); // exactly twice + // state is now 10000: two, three | 10001: two + + // simulate restart of the service + mPermissionProvider.onServiceStart(null); // should handle null + var newMockPc = mock(INativePermissionController.class); + mPermissionProvider.onServiceStart(newMockPc); + + var expectedPackageList = + List.of( + createUidPackageState( + 10000, List.of("com.package.two", "com.package.three")), + createUidPackageState(10001, List.of("com.package.two"))); + + verify(newMockPc) + .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList))); + + verify(newMockPc, never()).updatePackagesForUid(any()); + // updates should still work after restart + mPermissionProvider.onModifyPackageState(10001, "com.package.four", false /* isRemove */); + verify(newMockPc) + .updatePackagesForUid( + argThat( + new UidPackageStateMatcher( + 10001, List.of("com.package.two", "com.package.four")))); + // exactly once + verify(newMockPc).updatePackagesForUid(any()); + } + + private static UidPackageState createUidPackageState(int uid, List<String> packages) { + var res = new UidPackageState(); + res.uid = uid; + res.packageNames = packages; + return res; + } +} diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java index 634877eb2539..037c3c00443c 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java @@ -66,6 +66,7 @@ public class AudioServiceTest { @Mock private AppOpsManager mMockAppOpsManager; @Mock private AudioPolicyFacade mMockAudioPolicy; @Mock private PermissionEnforcer mMockPermissionEnforcer; + @Mock private AudioServerPermissionProvider mMockPermissionProvider; // the class being unit-tested here private AudioService mAudioService; @@ -86,7 +87,7 @@ public class AudioServiceTest { .thenReturn(AppOpsManager.MODE_ALLOWED); mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null, - mMockAppOpsManager, mMockPermissionEnforcer); + mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider); } /** diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java index 8dfcc1843fed..27b552fa7cdd 100644 --- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java @@ -22,11 +22,13 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.mock; import android.annotation.NonNull; +import android.app.AppOpsManager; import android.content.Context; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.IDeviceVolumeBehaviorDispatcher; +import android.os.PermissionEnforcer; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -75,7 +77,8 @@ public class DeviceVolumeBehaviorTest { mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase(); mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, - mTestLooper.getLooper()); + mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), + mock(AudioServerPermissionProvider.class)); mTestLooper.dispatchAll(); } diff --git a/services/tests/servicestests/src/com/android/server/audio/ServiceHolderTest.java b/services/tests/servicestests/src/com/android/server/audio/ServiceHolderTest.java new file mode 100644 index 000000000000..39f19ae1b382 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/audio/ServiceHolderTest.java @@ -0,0 +1,284 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.audio; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.media.IAudioPolicyService; +import android.os.Binder; +import android.os.IBinder; +import android.os.IServiceCallback; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +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.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.function.Function; + +@RunWith(AndroidJUnit4.class) +@Presubmit +public class ServiceHolderTest { + + private static final String AUDIO_POLICY_SERVICE_NAME = "media.audio_policy"; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + // the actual class under test + private ServiceHolder<IAudioPolicyService> mServiceHolder; + + @Mock private ServiceHolder.ServiceProviderFacade mServiceProviderFacade; + + @Mock private IAudioPolicyService mAudioPolicyService; + + @Mock private IBinder mBinder; + + @Mock private Consumer<IAudioPolicyService> mTaskOne; + @Mock private Consumer<IAudioPolicyService> mTaskTwo; + + @Before + public void setUp() throws Exception { + mServiceHolder = + new ServiceHolder( + AUDIO_POLICY_SERVICE_NAME, + (Function<IBinder, IAudioPolicyService>) + binder -> { + if (binder == mBinder) { + return mAudioPolicyService; + } else { + return mock(IAudioPolicyService.class); + } + }, + r -> r.run(), + mServiceProviderFacade); + when(mAudioPolicyService.asBinder()).thenReturn(mBinder); + } + + @Test + public void testListenerRegistered_whenConstructed() { + verify(mServiceProviderFacade) + .registerForNotifications(eq(AUDIO_POLICY_SERVICE_NAME), ArgumentMatchers.any()); + } + + @Test + public void testServiceSuccessfullyPopulated_whenCallback() throws RemoteException { + initializeViaCallback(); + verify(mBinder).linkToDeath(any(), anyInt()); + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + } + + @Test + public void testCheckServiceCalled_whenUncached() { + when(mServiceProviderFacade.checkService(eq(AUDIO_POLICY_SERVICE_NAME))) + .thenReturn(mBinder); + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + } + + @Test + public void testCheckServiceTransmitsNull() { + assertThat(mServiceHolder.checkService()).isEqualTo(null); + } + + @Test + public void testWaitForServiceCalled_whenUncached() { + when(mServiceProviderFacade.waitForService(eq(AUDIO_POLICY_SERVICE_NAME))) + .thenReturn(mBinder); + assertThat(mServiceHolder.waitForService()).isEqualTo(mAudioPolicyService); + } + + @Test + public void testCheckServiceNotCalled_whenCached() { + initializeViaCallback(); + mServiceHolder.checkService(); + verify(mServiceProviderFacade, never()).checkService(any()); + } + + @Test + public void testWaitForServiceNotCalled_whenCached() { + initializeViaCallback(); + mServiceHolder.waitForService(); + verify(mServiceProviderFacade, never()).waitForService(any()); + } + + @Test + public void testStartTaskCalled_onStart() { + mServiceHolder.registerOnStartTask(mTaskOne); + mServiceHolder.registerOnStartTask(mTaskTwo); + mServiceHolder.unregisterOnStartTask(mTaskOne); + when(mServiceProviderFacade.checkService(eq(AUDIO_POLICY_SERVICE_NAME))) + .thenReturn(mBinder); + + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + + verify(mTaskTwo).accept(eq(mAudioPolicyService)); + verify(mTaskOne, never()).accept(any()); + } + + @Test + public void testStartTaskCalled_onStartFromCallback() { + mServiceHolder.registerOnStartTask(mTaskOne); + mServiceHolder.registerOnStartTask(mTaskTwo); + mServiceHolder.unregisterOnStartTask(mTaskOne); + + initializeViaCallback(); + + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + verify(mTaskTwo).accept(eq(mAudioPolicyService)); + verify(mTaskOne, never()).accept(any()); + } + + @Test + public void testStartTaskCalled_onRegisterAfterStarted() { + initializeViaCallback(); + mServiceHolder.registerOnStartTask(mTaskOne); + verify(mTaskOne).accept(eq(mAudioPolicyService)); + } + + @Test + public void testBinderDied_clearsServiceAndUnlinks() { + initializeViaCallback(); + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + + mServiceHolder.binderDied(mBinder); + + verify(mBinder).unlinkToDeath(any(), anyInt()); + assertThat(mServiceHolder.checkService()).isEqualTo(null); + verify(mServiceProviderFacade).checkService(eq(AUDIO_POLICY_SERVICE_NAME)); + } + + @Test + public void testBinderDied_callsDeathTasks() { + mServiceHolder.registerOnDeathTask(mTaskOne); + mServiceHolder.registerOnDeathTask(mTaskTwo); + initializeViaCallback(); + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + mServiceHolder.unregisterOnDeathTask(mTaskOne); + + mServiceHolder.binderDied(mBinder); + + verify(mTaskTwo).accept(eq(mAudioPolicyService)); + verify(mTaskOne, never()).accept(any()); + } + + @Test + public void testAttemptClear_clearsServiceAndUnlinks() { + initializeViaCallback(); + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + + mServiceHolder.attemptClear(mBinder); + + verify(mBinder).unlinkToDeath(any(), anyInt()); + assertThat(mServiceHolder.checkService()).isEqualTo(null); + verify(mServiceProviderFacade).checkService(eq(AUDIO_POLICY_SERVICE_NAME)); + } + + @Test + public void testAttemptClear_callsDeathTasks() { + mServiceHolder.registerOnDeathTask(mTaskOne); + mServiceHolder.registerOnDeathTask(mTaskTwo); + initializeViaCallback(); + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + mServiceHolder.unregisterOnDeathTask(mTaskOne); + + mServiceHolder.attemptClear(mBinder); + + verify(mTaskTwo).accept(eq(mAudioPolicyService)); + verify(mTaskOne, never()).accept(any()); + } + + @Test + public void testSet_whenServiceSet_isIgnored() { + mServiceHolder.registerOnStartTask(mTaskOne); + when(mServiceProviderFacade.checkService(eq(AUDIO_POLICY_SERVICE_NAME))) + .thenReturn(mBinder); + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + + verify(mTaskOne).accept(eq(mAudioPolicyService)); + + // get the callback + ArgumentCaptor<IServiceCallback> cb = ArgumentCaptor.forClass(IServiceCallback.class); + verify(mServiceProviderFacade) + .registerForNotifications(eq(AUDIO_POLICY_SERVICE_NAME), cb.capture()); + + // Simulate a service callback with a different instance + try { + cb.getValue().onRegistration(AUDIO_POLICY_SERVICE_NAME, new Binder()); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + + // No additional start task call (i.e. only the first verify) + verify(mTaskOne).accept(any()); + // Same instance + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + + } + + @Test + public void testClear_whenServiceCleared_isIgnored() { + mServiceHolder.registerOnDeathTask(mTaskOne); + mServiceHolder.attemptClear(mBinder); + verify(mTaskOne, never()).accept(any()); + } + + @Test + public void testClear_withDifferentCookie_isIgnored() { + mServiceHolder.registerOnDeathTask(mTaskOne); + initializeViaCallback(); + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + + // Notif for stale cookie + mServiceHolder.attemptClear(new Binder()); + + // Service shouldn't be cleared + assertThat(mServiceHolder.checkService()).isEqualTo(mAudioPolicyService); + // No death tasks should fire + verify(mTaskOne, never()).accept(any()); + } + + private void initializeViaCallback() { + ArgumentCaptor<IServiceCallback> cb = ArgumentCaptor.forClass(IServiceCallback.class); + verify(mServiceProviderFacade) + .registerForNotifications(eq(AUDIO_POLICY_SERVICE_NAME), cb.capture()); + + try { + cb.getValue().onRegistration(AUDIO_POLICY_SERVICE_NAME, mBinder); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java index 23728db34c34..8e34ee1b6a42 100644 --- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java @@ -40,6 +40,7 @@ import static android.view.KeyEvent.ACTION_DOWN; import static android.view.KeyEvent.KEYCODE_VOLUME_UP; import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME; +import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -132,9 +133,12 @@ public class VolumeHelperTest { @Mock private PermissionEnforcer mMockPermissionEnforcer; @Mock + private AudioServerPermissionProvider mMockPermissionProvider; + @Mock private AudioVolumeGroupHelperBase mAudioVolumeGroupHelper; - private final AudioPolicyFacade mFakeAudioPolicy = lookbackAudio -> false; + @Mock + private AudioPolicyFacade mMockAudioPolicy; private AudioVolumeGroup mAudioMusicVolumeGroup; @@ -153,9 +157,10 @@ public class VolumeHelperTest { SystemServerAdapter systemServer, SettingsAdapter settings, AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, @Nullable Looper looper, AppOpsManager appOps, - @NonNull PermissionEnforcer enforcer) { + @NonNull PermissionEnforcer enforcer, + AudioServerPermissionProvider permissionProvider) { super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper, - audioPolicy, looper, appOps, enforcer); + audioPolicy, looper, appOps, enforcer, permissionProvider); } public void setDeviceForStream(int stream, int device) { @@ -209,8 +214,9 @@ public class VolumeHelperTest { mAm = mContext.getSystemService(AudioManager.class); mAudioService = new MyAudioService(mContext, mSpyAudioSystem, mSpySystemServer, - mSettingsAdapter, mAudioVolumeGroupHelper, mFakeAudioPolicy, - mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer); + mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, + mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer, + mMockPermissionProvider); mTestLooper.dispatchAll(); prepareAudioServiceState(); @@ -552,7 +558,7 @@ public class VolumeHelperTest { } @Test - @RequiresFlagsDisabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME) + @RequiresFlagsDisabled({FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME, FLAG_ABS_VOLUME_INDEX_FIX}) public void configurablePreScaleAbsoluteVolume_checkIndex() throws Exception { final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC); final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC); @@ -607,6 +613,7 @@ public class VolumeHelperTest { @Test @RequiresFlagsEnabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME) + @RequiresFlagsDisabled(FLAG_ABS_VOLUME_INDEX_FIX) public void disablePreScaleAbsoluteVolume_checkIndex() throws Exception { final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC); final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC); diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java index 7dd1847114c8..50cfa753ebdb 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java @@ -20,7 +20,6 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; -import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -52,6 +51,7 @@ import android.util.ArraySet; import android.util.SparseArray; import android.util.Xml; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.content.PackageMonitor; @@ -70,6 +70,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; @@ -95,21 +96,21 @@ public class LocaleManagerBackupRestoreTest { private static final int DEFAULT_USER_ID = 0; private static final int WORK_PROFILE_USER_ID = 10; private static final int DEFAULT_UID = Binder.getCallingUid() + 100; + private static final int WORK_PROFILE_UID = Binder.getCallingUid() + 1000100; private static final long DEFAULT_CREATION_TIME_MILLIS = 1000; private static final Duration RETENTION_PERIOD = Duration.ofDays(3); private static final LocaleList DEFAULT_LOCALES = LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS); private static final Map<String, LocalesInfo> DEFAULT_PACKAGE_LOCALES_INFO_MAP = Map.of( DEFAULT_PACKAGE_NAME, new LocalesInfo(DEFAULT_LOCALE_TAGS, false)); - private static final SparseArray<LocaleManagerBackupHelper.StagedData> STAGE_DATA = - new SparseArray<>(); + private final SparseArray<File> mStagedDataFiles = new SparseArray<>(); + private File mArchivedPackageFile; private LocaleManagerBackupHelper mBackupHelper; private long mCurrentTimeMillis; + private Context mContext = spy(ApplicationProvider.getApplicationContext()); @Mock - private Context mMockContext; - @Mock private PackageManager mMockPackageManager; @Mock private LocaleManagerService mMockLocaleManagerService; @@ -138,23 +139,28 @@ public class LocaleManagerBackupRestoreTest { @Before public void setUp() throws Exception { - mMockContext = mock(Context.class); mMockPackageManager = mock(PackageManager.class); mMockLocaleManagerService = mock(LocaleManagerService.class); mMockDelegateAppLocalePackages = mock(SharedPreferences.class); mMockSpEditor = mock(SharedPreferences.Editor.class); SystemAppUpdateTracker systemAppUpdateTracker = mock(SystemAppUpdateTracker.class); - doReturn(mMockPackageManager).when(mMockContext).getPackageManager(); + doReturn(mMockPackageManager).when(mContext).getPackageManager(); doReturn(mMockSpEditor).when(mMockDelegateAppLocalePackages).edit(); HandlerThread broadcastHandlerThread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND); broadcastHandlerThread.start(); - mBackupHelper = spy(new ShadowLocaleManagerBackupHelper(mMockContext, - mMockLocaleManagerService, mMockPackageManager, mClock, STAGE_DATA, - broadcastHandlerThread, mMockDelegateAppLocalePackages)); + File file0 = new File(mContext.getCacheDir(), "file_user_0.txt"); + File file10 = new File(mContext.getCacheDir(), "file_user_10.txt"); + mStagedDataFiles.put(DEFAULT_USER_ID, file0); + mStagedDataFiles.put(WORK_PROFILE_USER_ID, file10); + mArchivedPackageFile = new File(mContext.getCacheDir(), "file_archived.txt"); + + mBackupHelper = spy(new ShadowLocaleManagerBackupHelper(mContext, + mMockLocaleManagerService, mMockPackageManager, mClock, broadcastHandlerThread, + mStagedDataFiles, mArchivedPackageFile, mMockDelegateAppLocalePackages)); doNothing().when(mBackupHelper).notifyBackupManager(); mUserMonitor = mBackupHelper.getUserMonitor(); @@ -165,7 +171,16 @@ public class LocaleManagerBackupRestoreTest { @After public void tearDown() throws Exception { - STAGE_DATA.clear(); + for (int i = 0; i < mStagedDataFiles.size(); i++) { + int userId = mStagedDataFiles.keyAt(i); + File file = mStagedDataFiles.get(userId); + SharedPreferences sp = mContext.getSharedPreferences(file, Context.MODE_PRIVATE); + sp.edit().clear().commit(); + if (file.exists()) { + file.delete(); + } + } + mStagedDataFiles.clear(); } @Test @@ -543,17 +558,21 @@ public class LocaleManagerBackupRestoreTest { mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, bundle); mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, bundle); + checkArchivedFileExists(); + mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID); verifyNothingRestored(); setUpPackageInstalled(pkgNameA); - mPackageMonitor.onPackageUpdateFinished(pkgNameA, DEFAULT_UID); + mBackupHelper.onPackageUpdateFinished(pkgNameA, DEFAULT_UID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); + checkArchivedFileExists(); + mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameA, false, false); @@ -565,11 +584,12 @@ public class LocaleManagerBackupRestoreTest { setUpPackageInstalled(pkgNameB); - mPackageMonitor.onPackageUpdateFinished(pkgNameB, DEFAULT_UID); + mBackupHelper.onPackageUpdateFinished(pkgNameB, DEFAULT_UID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); + checkArchivedFileDoesNotExist(); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameB, true, false); @@ -723,7 +743,7 @@ public class LocaleManagerBackupRestoreTest { Intent intent = new Intent(); intent.setAction(Intent.ACTION_USER_REMOVED); intent.putExtra(Intent.EXTRA_USER_HANDLE, DEFAULT_USER_ID); - mUserMonitor.onReceive(mMockContext, intent); + mUserMonitor.onReceive(mContext, intent); // Stage data should be removed only for DEFAULT_USER_ID. checkStageDataDoesNotExist(DEFAULT_USER_ID); @@ -732,6 +752,72 @@ public class LocaleManagerBackupRestoreTest { } @Test + public void testRestore_multipleProfile_restoresFromStage_ArchiveEnabled() throws Exception { + final ByteArrayOutputStream outDefault = new ByteArrayOutputStream(); + writeTestPayload(outDefault, DEFAULT_PACKAGE_LOCALES_INFO_MAP); + final ByteArrayOutputStream outWorkProfile = new ByteArrayOutputStream(); + String anotherPackage = "com.android.anotherapp"; + String anotherLangTags = "mr,zh"; + LocalesInfo localesInfo = new LocalesInfo(anotherLangTags, true); + HashMap<String, LocalesInfo> pkgLocalesMapWorkProfile = new HashMap<>(); + pkgLocalesMapWorkProfile.put(anotherPackage, localesInfo); + writeTestPayload(outWorkProfile, pkgLocalesMapWorkProfile); + // DEFAULT_PACKAGE_NAME is NOT installed on the device. + setUpPackageNotInstalled(DEFAULT_PACKAGE_NAME); + setUpPackageNotInstalled(anotherPackage); + setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.getEmptyLocaleList()); + setUpLocalesForPackage(anotherPackage, LocaleList.getEmptyLocaleList()); + setUpPackageNamesForSp(new ArraySet<>()); + + Bundle bundle = new Bundle(); + bundle.putBoolean(Intent.EXTRA_ARCHIVAL, true); + mPackageMonitor.onPackageAddedWithExtras(DEFAULT_PACKAGE_NAME, DEFAULT_UID, bundle); + mPackageMonitor.onPackageAddedWithExtras(anotherPackage, WORK_PROFILE_UID, bundle); + + checkArchivedFileExists(); + + mBackupHelper.stageAndApplyRestoredPayload(outDefault.toByteArray(), DEFAULT_USER_ID); + mBackupHelper.stageAndApplyRestoredPayload(outWorkProfile.toByteArray(), + WORK_PROFILE_USER_ID); + + verifyNothingRestored(); + verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_INFO_MAP, + DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID); + verifyStageDataForUser(pkgLocalesMapWorkProfile, + DEFAULT_CREATION_TIME_MILLIS, WORK_PROFILE_USER_ID); + + setUpPackageInstalled(DEFAULT_PACKAGE_NAME); + mBackupHelper.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID); + + verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME, + DEFAULT_USER_ID, + LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS), false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); + checkArchivedFileExists(); + checkStageDataDoesNotExist(DEFAULT_USER_ID); + + mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, false, + false); + + verify(mMockSpEditor, times(0)).putStringSet(anyString(), any()); + + setUpPackageInstalled(anotherPackage); + mBackupHelper.onPackageUpdateFinished(anotherPackage, WORK_PROFILE_UID); + + verify(mMockLocaleManagerService, times(1)).setApplicationLocales(anotherPackage, + WORK_PROFILE_USER_ID, + LocaleList.forLanguageTags(anotherLangTags), true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); + checkArchivedFileDoesNotExist(); + + mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, anotherPackage, true, false); + + verify(mMockSpEditor, times(1)).putStringSet(Integer.toString(DEFAULT_USER_ID), + new ArraySet<>(Arrays.asList(anotherPackage))); + checkStageDataDoesNotExist(WORK_PROFILE_USER_ID); + } + + @Test public void testPackageRemoved_noInfoInSp() throws Exception { String pkgNameA = "com.android.myAppA"; String pkgNameB = "com.android.myAppB"; @@ -858,10 +944,22 @@ public class LocaleManagerBackupRestoreTest { private void verifyStageDataForUser(Map<String, LocalesInfo> expectedPkgLocalesMap, long expectedCreationTimeMillis, int userId) { - LocaleManagerBackupHelper.StagedData stagedDataForUser = STAGE_DATA.get(userId); - assertNotNull(stagedDataForUser); - assertEquals(expectedCreationTimeMillis, stagedDataForUser.mCreationTimeMillis); - verifyStageData(expectedPkgLocalesMap, stagedDataForUser.mPackageStates); + SharedPreferences sp = mContext.getSharedPreferences(mStagedDataFiles.get(userId), + Context.MODE_PRIVATE); + assertTrue(sp.getAll().size() > 0); + assertEquals(expectedCreationTimeMillis, sp.getLong("staged_data_time", -1)); + verifyStageData(expectedPkgLocalesMap, sp); + } + + private static void verifyStageData(Map<String, LocalesInfo> expectedPkgLocalesMap, + SharedPreferences sp) { + for (String pkg : expectedPkgLocalesMap.keySet()) { + assertTrue(!sp.getString(pkg, "").isEmpty()); + String[] info = sp.getString(pkg, "").split(" s:"); + assertEquals(expectedPkgLocalesMap.get(pkg).mLocales, info[0]); + assertEquals(expectedPkgLocalesMap.get(pkg).mSetFromDelegate, + Boolean.parseBoolean(info[1])); + } } private static void verifyStageData(Map<String, LocalesInfo> expectedPkgLocalesMap, @@ -875,11 +973,19 @@ public class LocaleManagerBackupRestoreTest { } } - private static void checkStageDataExists(int userId) { - assertNotNull(STAGE_DATA.get(userId)); + private void checkStageDataExists(int userId) { + assertTrue(mStagedDataFiles.get(userId) != null && mStagedDataFiles.get(userId).exists()); + } + + private void checkStageDataDoesNotExist(int userId) { + assertTrue(mStagedDataFiles.get(userId) == null || !mStagedDataFiles.get(userId).exists()); + } + + private void checkArchivedFileExists() { + assertTrue(mArchivedPackageFile.exists()); } - private static void checkStageDataDoesNotExist(int userId) { - assertNull(STAGE_DATA.get(userId)); + private void checkArchivedFileDoesNotExist() { + assertTrue(!mArchivedPackageFile.exists()); } -} +}
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java index 9f7cbe3170f0..b46902d9904a 100644 --- a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java +++ b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java @@ -22,6 +22,7 @@ import android.content.pm.PackageManager; import android.os.HandlerThread; import android.util.SparseArray; +import java.io.File; import java.time.Clock; /** @@ -33,9 +34,9 @@ public class ShadowLocaleManagerBackupHelper extends LocaleManagerBackupHelper { ShadowLocaleManagerBackupHelper(Context context, LocaleManagerService localeManagerService, PackageManager packageManager, Clock clock, - SparseArray<LocaleManagerBackupHelper.StagedData> stagedData, - HandlerThread broadcastHandlerThread, SharedPreferences delegateAppLocalePackages) { - super(context, localeManagerService, packageManager, clock, stagedData, - broadcastHandlerThread, delegateAppLocalePackages); + HandlerThread broadcastHandlerThread, SparseArray<File> stagedDataFiles, + File archivedPackagesFile, SharedPreferences delegateAppLocalePackages) { + super(context, localeManagerService, packageManager, clock, broadcastHandlerThread, + stagedDataFiles, archivedPackagesFile, delegateAppLocalePackages); } } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java index 4b22652a3f21..601a01624189 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java @@ -43,6 +43,8 @@ import android.app.PropertyInvalidatedCache; import android.content.Intent; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.service.gatekeeper.GateKeeperResponse; @@ -483,18 +485,31 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { setSecureFrpMode(true); try { mService.setLockCredential(newPassword("1234"), nonePassword(), PRIMARY_USER_ID); - fail("Password shouldn't be changeable before FRP unlock"); + fail("Password shouldn't be changeable while FRP is active"); } catch (SecurityException e) { } } @Test - public void testSetCredentialPossibleInSecureFrpModeAfterSuw() throws RemoteException { + @DisableFlags(android.security.Flags.FLAG_FRP_ENFORCEMENT) + public void testSetCredentialPossibleInSecureFrpModeAfterSuw_FlagOff() throws RemoteException { setUserSetupComplete(true); setSecureFrpMode(true); setCredential(PRIMARY_USER_ID, newPassword("1234")); } @Test + @EnableFlags(android.security.Flags.FLAG_FRP_ENFORCEMENT) + public void testSetCredentialNotPossibleInSecureFrpModeAfterSuw_FlagOn() + throws RemoteException { + setUserSetupComplete(true); + setSecureFrpMode(true); + try { + mService.setLockCredential(newPassword("1234"), nonePassword(), PRIMARY_USER_ID); + fail("Password shouldn't be changeable after SUW while FRP is active"); + } catch (SecurityException e) { } + } + + @Test public void testPasswordHistoryDisabledByDefault() throws Exception { final int userId = PRIMARY_USER_ID; checkPasswordHistoryLength(userId, 0); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 4a9760bc3317..e91fd3794a48 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2726,6 +2726,19 @@ public class ActivityRecordTests extends WindowTestsBase { assertNoStartingWindow(activity); } + @Test + public void testPostCleanupStartingWindow() { + registerTestStartingWindowOrganizer(); + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + activity.addStartingWindow(mPackageName, android.R.style.Theme, null, true, true, false, + true, false, false, false); + waitUntilHandlersIdle(); + assertHasStartingWindow(activity); + // Simulate Shell remove starting window actively. + activity.mStartingWindow.removeImmediately(); + assertNoStartingWindow(activity); + } + private void testLegacySplashScreen(int targetSdk, int verifyType) { final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); activity.mTargetSdk = targetSdk; diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 96ddfe8d5ac9..7ced9d50ab3f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -107,6 +107,7 @@ import android.graphics.Rect; import android.os.Binder; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsDisabled; import android.provider.DeviceConfig; @@ -401,6 +402,7 @@ public class SizeCompatTests extends WindowTestsBase { // TODO(b/333663877): Enable test after fix @Test @RequiresFlagsDisabled({Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION}) + @EnableFlags(Flags.FLAG_IMMERSIVE_APP_REPOSITIONING) public void testRepositionLandscapeImmersiveAppWithDisplayCutout() { final int dw = 2100; final int dh = 2000; @@ -4059,6 +4061,7 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_IMMERSIVE_APP_REPOSITIONING) public void testImmersiveLetterboxAlignedToBottom_OverlappingNavbar() { assertLandscapeActivityAlignedToBottomWithNavbar(true /* immersive */); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 1d014201cf46..a7dbecbb5255 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -794,9 +794,8 @@ public class VoiceInteractionManagerService extends SystemService { if (curService != null && !curService.isEmpty()) { try { serviceComponent = ComponentName.unflattenFromString(curService); - serviceInfo = AppGlobals.getPackageManager() - .getServiceInfo(serviceComponent, 0, mCurUser); - } catch (RuntimeException | RemoteException e) { + serviceInfo = getValidVoiceInteractionServiceInfo(serviceComponent); + } catch (RuntimeException e) { Slog.wtf(TAG, "Bad voice interaction service name " + curService, e); serviceComponent = null; serviceInfo = null; @@ -834,6 +833,27 @@ public class VoiceInteractionManagerService extends SystemService { } } + @Nullable + private ServiceInfo getValidVoiceInteractionServiceInfo( + @Nullable ComponentName serviceComponent) { + if (serviceComponent == null) { + return null; + } + List<ResolveInfo> services = queryInteractorServices( + mCurUser, serviceComponent.getPackageName()); + for (int i = 0; i < services.size(); i++) { + ResolveInfo service = services.get(i); + VoiceInteractionServiceInfo info = new VoiceInteractionServiceInfo( + mContext.getPackageManager(), service.serviceInfo); + ServiceInfo candidateInfo = info.getServiceInfo(); + if (candidateInfo != null + && candidateInfo.getComponentName().equals(serviceComponent)) { + return candidateInfo; + } + } + return null; + } + private List<ResolveInfo> queryInteractorServices( @UserIdInt int user, @Nullable String packageName) { diff --git a/telephony/java/android/telephony/CarrierRestrictionRules.java b/telephony/java/android/telephony/CarrierRestrictionRules.java index d5db61233230..65e8e13036a6 100644 --- a/telephony/java/android/telephony/CarrierRestrictionRules.java +++ b/telephony/java/android/telephony/CarrierRestrictionRules.java @@ -555,10 +555,11 @@ public final class CarrierRestrictionRules implements Parcelable { * Set the device's carrier restriction status * * @param carrierRestrictionStatus device restriction status - * @hide */ public @NonNull - Builder setCarrierRestrictionStatus(int carrierRestrictionStatus) { + @FlaggedApi(Flags.FLAG_SET_CARRIER_RESTRICTION_STATUS) + Builder setCarrierRestrictionStatus( + @CarrierRestrictionStatus int carrierRestrictionStatus) { mRules.mCarrierRestrictionStatus = carrierRestrictionStatus; return this; } diff --git a/tests/FlickerTests/ActivityEmbedding/Android.bp b/tests/FlickerTests/ActivityEmbedding/Android.bp index 2cdf54248ebc..e09fbf6adc02 100644 --- a/tests/FlickerTests/ActivityEmbedding/Android.bp +++ b/tests/FlickerTests/ActivityEmbedding/Android.bp @@ -20,17 +20,65 @@ package { // all of the 'license_kinds' from "frameworks_base_license" // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 + default_team: "trendy_team_windowing_sdk", default_applicable_licenses: ["frameworks_base_license"], } -android_test { - name: "FlickerTestsOther", +filegroup { + name: "FlickerTestsOtherCommon-src", + srcs: ["src/**/ActivityEmbeddingTestBase.kt"], +} + +filegroup { + name: "FlickerTestsOtherOpen-src", + srcs: ["src/**/open/*"], +} + +filegroup { + name: "FlickerTestsOtherRotation-src", + srcs: ["src/**/rotation/*"], +} + +java_library { + name: "FlickerTestsOtherCommon", + defaults: ["FlickerTestsDefault"], + srcs: [":FlickerTestsOtherCommon-src"], + static_libs: ["FlickerTestsBase"], +} + +java_defaults { + name: "FlickerTestsOtherDefaults", defaults: ["FlickerTestsDefault"], 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"], + static_libs: [ + "FlickerTestsBase", + "FlickerTestsOtherCommon", + ], data: ["trace_config/*"], } + +android_test { + name: "FlickerTestsOtherOpen", + defaults: ["FlickerTestsOtherDefaults"], + srcs: [":FlickerTestsOtherOpen-src"], +} + +android_test { + name: "FlickerTestsOtherRotation", + defaults: ["FlickerTestsOtherDefaults"], + srcs: [":FlickerTestsOtherRotation-src"], +} + +android_test { + name: "FlickerTestsOther", + defaults: ["FlickerTestsOtherDefaults"], + srcs: ["src/**/*"], + exclude_srcs: [ + ":FlickerTestsOtherOpen-src", + ":FlickerTestsOtherRotation-src", + ":FlickerTestsOtherCommon-src", + ], +} diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt index 8a241de32a2b..209a14b3657d 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt @@ -17,6 +17,7 @@ package com.android.server.wm.flicker.service import android.app.Instrumentation +import android.platform.test.rule.DisableNotificationCooldownSettingRule import android.platform.test.rule.NavigationModeRule import android.platform.test.rule.PressHomeRule import android.platform.test.rule.UnlockScreenRule @@ -48,6 +49,7 @@ object Utils { clearCacheAfterParsing = false ) ) + .around(DisableNotificationCooldownSettingRule()) .around(PressHomeRule()) } } diff --git a/tests/FlickerTests/IME/Android.bp b/tests/FlickerTests/IME/Android.bp index 3538949cbc8d..ccc3683f0b93 100644 --- a/tests/FlickerTests/IME/Android.bp +++ b/tests/FlickerTests/IME/Android.bp @@ -39,6 +39,10 @@ android_test { defaults: ["FlickerTestsDefault"], manifest: "AndroidManifest.xml", test_config_template: "AndroidTestTemplate.xml", + test_suites: [ + "device-tests", + "device-platinum-tests", + ], srcs: ["src/**/*"], static_libs: ["FlickerTestsBase"], data: ["trace_config/*"], diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt index dc5013519dbf..ed6e8df3e293 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt @@ -23,6 +23,7 @@ import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.traces.component.ComponentNameMatcher +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import org.junit.FixMethodOrder @@ -77,6 +78,7 @@ class CloseImeShownOnAppStartToAppOnPressBackTest(flicker: LegacyFlickerTest) : @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible() + @FlakyTest(bugId = 330486656) @Presubmit @Test fun imeAppLayerIsAlwaysVisible() { diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt index c29e71ce4c79..07fc2300286a 100644 --- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt +++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt @@ -18,6 +18,7 @@ package com.android.server.wm.flicker.notification import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit +import android.platform.test.rule.DisableNotificationCooldownSettingRule import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.FlickerTestData @@ -37,6 +38,7 @@ import com.android.server.wm.flicker.navBarWindowIsVisibleAtEnd import com.android.server.wm.flicker.taskBarLayerIsVisibleAtEnd import com.android.server.wm.flicker.taskBarWindowIsVisibleAtEnd import org.junit.Assume +import org.junit.ClassRule import org.junit.FixMethodOrder import org.junit.Ignore import org.junit.Test @@ -208,5 +210,10 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams() = LegacyFlickerTestFactory.nonRotationTests() + + /** Ensures that posted notifications will alert and HUN even just after boot. */ + @ClassRule + @JvmField + val disablenotificationCooldown = DisableNotificationCooldownSettingRule() } } diff --git a/tests/TrustTests/AndroidManifest.xml b/tests/TrustTests/AndroidManifest.xml index 30cf345db34d..2f6c0dd14adc 100644 --- a/tests/TrustTests/AndroidManifest.xml +++ b/tests/TrustTests/AndroidManifest.xml @@ -78,6 +78,7 @@ <action android:name="android.service.trust.TrustAgentService" /> </intent-filter> </service> + <service android:name=".IsActiveUnlockRunningTrustAgent" android:exported="true" @@ -88,6 +89,16 @@ </intent-filter> </service> + <service + android:name=".UnlockAttemptTrustAgent" + android:exported="true" + android:label="Test Agent" + android:permission="android.permission.BIND_TRUST_AGENT"> + <intent-filter> + <action android:name="android.service.trust.TrustAgentService" /> + </intent-filter> + </service> + </application> <!-- self-instrumenting test package. --> diff --git a/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt new file mode 100644 index 000000000000..2c9361df63fd --- /dev/null +++ b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.trust.test + +import android.app.trust.TrustManager +import android.content.Context +import android.trust.BaseTrustAgentService +import android.trust.TrustTestActivity +import android.trust.test.lib.LockStateTrackingRule +import android.trust.test.lib.ScreenLockRule +import android.trust.test.lib.TestTrustListener +import android.trust.test.lib.TrustAgentRule +import android.util.Log +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +/** + * Test for the impacts of reporting unlock attempts. + * + * atest TrustTests:UnlockAttemptTest + */ +@RunWith(AndroidJUnit4::class) +class UnlockAttemptTest { + private val context = getApplicationContext<Context>() + private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager + private val userId = context.userId + private val activityScenarioRule = ActivityScenarioRule(TrustTestActivity::class.java) + private val screenLockRule = ScreenLockRule(requireStrongAuth = true) + private val lockStateTrackingRule = LockStateTrackingRule() + private val trustAgentRule = + TrustAgentRule<UnlockAttemptTrustAgent>(startUnlocked = false, startEnabled = false) + + private val trustListener = UnlockAttemptTrustListener() + private val agent get() = trustAgentRule.agent + + @get:Rule + val rule: RuleChain = + RuleChain.outerRule(activityScenarioRule) + .around(screenLockRule) + .around(lockStateTrackingRule) + .around(trustAgentRule) + + @Before + fun setUp() { + trustManager.registerTrustListener(trustListener) + } + + @Test + fun successfulUnlockAttempt_allowsTrustAgentToStart() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) { + trustAgentRule.enableTrustAgent() + + triggerSuccessfulUnlock() + + trustAgentRule.verifyAgentIsRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START) + } + + @Test + fun successfulUnlockAttempt_notifiesTrustAgent() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldSuccessfulCount = agent.successfulUnlockCallCount + val oldFailedCount = agent.failedUnlockCallCount + + triggerSuccessfulUnlock() + + assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount + 1) + assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount) + } + + @Test + fun successfulUnlockAttempt_notifiesTrustListenerOfManagedTrust() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0 + + triggerSuccessfulUnlock() + + assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo( + oldTrustManagedChangedCount + 1 + ) + } + + @Test + fun failedUnlockAttempt_doesNotAllowTrustAgentToStart() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) { + trustAgentRule.enableTrustAgent() + + triggerFailedUnlock() + + trustAgentRule.ensureAgentIsNotRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START) + } + + @Test + fun failedUnlockAttempt_notifiesTrustAgent() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldSuccessfulCount = agent.successfulUnlockCallCount + val oldFailedCount = agent.failedUnlockCallCount + + triggerFailedUnlock() + + assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount) + assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount + 1) + } + + @Test + fun failedUnlockAttempt_doesNotNotifyTrustListenerOfManagedTrust() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0 + + triggerFailedUnlock() + + assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo( + oldTrustManagedChangedCount + ) + } + + private fun runUnlockAttemptTest( + enableAndVerifyTrustAgent: Boolean, + managingTrust: Boolean, + testBlock: () -> Unit, + ) { + if (enableAndVerifyTrustAgent) { + Log.i(TAG, "Triggering successful unlock") + triggerSuccessfulUnlock() + Log.i(TAG, "Enabling and waiting for trust agent") + trustAgentRule.enableAndVerifyTrustAgentIsRunning( + MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START + ) + Log.i(TAG, "Managing trust: $managingTrust") + agent.setManagingTrust(managingTrust) + await() + } + testBlock() + } + + private fun triggerSuccessfulUnlock() { + screenLockRule.successfulScreenLockAttempt() + trustAgentRule.reportSuccessfulUnlock() + await() + } + + private fun triggerFailedUnlock() { + screenLockRule.failedScreenLockAttempt() + trustAgentRule.reportFailedUnlock() + await() + } + + companion object { + private const val TAG = "UnlockAttemptTest" + private fun await(millis: Long = 500) = Thread.sleep(millis) + private const val MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START = 10000L + } +} + +class UnlockAttemptTrustAgent : BaseTrustAgentService() { + var successfulUnlockCallCount: Long = 0 + private set + var failedUnlockCallCount: Long = 0 + private set + + override fun onUnlockAttempt(successful: Boolean) { + super.onUnlockAttempt(successful) + if (successful) { + successfulUnlockCallCount++ + } else { + failedUnlockCallCount++ + } + } +} + +private class UnlockAttemptTrustListener : TestTrustListener() { + var enabledTrustAgentsChangedCount = mutableMapOf<Int, Int>() + var onTrustManagedChangedCount = mutableMapOf<Int, Int>() + + override fun onEnabledTrustAgentsChanged(userId: Int) { + enabledTrustAgentsChangedCount.compute(userId) { _: Int, curr: Int? -> + if (curr == null) 0 else curr + 1 + } + } + + data class TrustChangedParams( + val enabled: Boolean, + val newlyUnlocked: Boolean, + val userId: Int, + val flags: Int, + val trustGrantedMessages: MutableList<String>? + ) + + val onTrustChangedCalls = mutableListOf<TrustChangedParams>() + + override fun onTrustChanged( + enabled: Boolean, + newlyUnlocked: Boolean, + userId: Int, + flags: Int, + trustGrantedMessages: MutableList<String> + ) { + onTrustChangedCalls += TrustChangedParams( + enabled, newlyUnlocked, userId, flags, trustGrantedMessages + ) + } + + override fun onTrustManagedChanged(enabled: Boolean, userId: Int) { + onTrustManagedChangedCount.compute(userId) { _: Int, curr: Int? -> + if (curr == null) 0 else curr + 1 + } + } +} diff --git a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt index f1edca3ff86e..1ccdcc623c5b 100644 --- a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt +++ b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt @@ -24,6 +24,8 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN import com.android.internal.widget.LockscreenCredential import com.google.common.truth.Truth.assertWithMessage import org.junit.rules.TestRule @@ -32,13 +34,18 @@ import org.junit.runners.model.Statement /** * Sets a screen lock on the device for the duration of the test. + * + * @param requireStrongAuth Whether a strong auth is required at the beginning. + * If true, trust agents will not be available until the user verifies their credentials. */ -class ScreenLockRule : TestRule { +class ScreenLockRule(val requireStrongAuth: Boolean = false) : TestRule { private val context: Context = getApplicationContext() + private val userId = context.userId private val uiDevice = UiDevice.getInstance(getInstrumentation()) private val windowManager = checkNotNull(WindowManagerGlobal.getWindowManagerService()) private val lockPatternUtils = LockPatternUtils(context) private var instantLockSavedValue = false + private var strongAuthSavedValue: Int = 0 override fun apply(base: Statement, description: Description) = object : Statement() { override fun evaluate() { @@ -46,10 +53,12 @@ class ScreenLockRule : TestRule { dismissKeyguard() setScreenLock() setLockOnPowerButton() + configureStrongAuthState() try { base.evaluate() } finally { + restoreStrongAuthState() removeScreenLock() revertLockOnPowerButton() dismissKeyguard() @@ -57,6 +66,22 @@ class ScreenLockRule : TestRule { } } + private fun configureStrongAuthState() { + strongAuthSavedValue = lockPatternUtils.getStrongAuthForUser(userId) + if (requireStrongAuth) { + Log.d(TAG, "Triggering strong auth due to simulated lockdown") + lockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, userId) + wait("strong auth required after lockdown") { + lockPatternUtils.getStrongAuthForUser(userId) == + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN + } + } + } + + private fun restoreStrongAuthState() { + lockPatternUtils.requireStrongAuth(strongAuthSavedValue, userId) + } + private fun verifyNoScreenLockAlreadySet() { assertWithMessage("Screen Lock must not already be set on device") .that(lockPatternUtils.isSecure(context.userId)) @@ -82,6 +107,22 @@ class ScreenLockRule : TestRule { } } + fun successfulScreenLockAttempt() { + lockPatternUtils.verifyCredential(LockscreenCredential.createPin(PIN), context.userId, 0) + lockPatternUtils.userPresent(context.userId) + wait("strong auth not required") { + lockPatternUtils.getStrongAuthForUser(context.userId) == STRONG_AUTH_NOT_REQUIRED + } + } + + fun failedScreenLockAttempt() { + lockPatternUtils.verifyCredential( + LockscreenCredential.createPin(WRONG_PIN), + context.userId, + 0 + ) + } + private fun setScreenLock() { lockPatternUtils.setLockCredential( LockscreenCredential.createPin(PIN), @@ -121,5 +162,6 @@ class ScreenLockRule : TestRule { companion object { private const val TAG = "ScreenLockRule" private const val PIN = "0000" + private const val WRONG_PIN = "0001" } } diff --git a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt index 18bc029b6845..404c6d968b3a 100644 --- a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt +++ b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt @@ -20,14 +20,15 @@ import android.app.trust.TrustManager import android.content.ComponentName import android.content.Context import android.trust.BaseTrustAgentService +import android.trust.test.lib.TrustAgentRule.Companion.invoke import android.util.Log import androidx.test.core.app.ApplicationProvider.getApplicationContext import com.android.internal.widget.LockPatternUtils import com.google.common.truth.Truth.assertWithMessage +import kotlin.reflect.KClass import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import kotlin.reflect.KClass /** * Enables a trust agent and causes the system service to bind to it. @@ -37,7 +38,9 @@ import kotlin.reflect.KClass * @constructor Creates the rule. Do not use; instead, use [invoke]. */ class TrustAgentRule<T : BaseTrustAgentService>( - private val serviceClass: KClass<T> + private val serviceClass: KClass<T>, + private val startUnlocked: Boolean, + private val startEnabled: Boolean, ) : TestRule { private val context: Context = getApplicationContext() private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager @@ -48,11 +51,18 @@ class TrustAgentRule<T : BaseTrustAgentService>( override fun apply(base: Statement, description: Description) = object : Statement() { override fun evaluate() { verifyTrustServiceRunning() - unlockDeviceWithCredential() - enableTrustAgent() + if (startUnlocked) { + reportSuccessfulUnlock() + } else { + Log.i(TAG, "Trust manager not starting in unlocked state") + } try { - verifyAgentIsRunning() + if (startEnabled) { + enableAndVerifyTrustAgentIsRunning() + } else { + Log.i(TAG, "Trust agent ${serviceClass.simpleName} not enabled") + } base.evaluate() } finally { disableTrustAgent() @@ -64,12 +74,22 @@ class TrustAgentRule<T : BaseTrustAgentService>( assertWithMessage("Trust service is not running").that(trustManager).isNotNull() } - private fun unlockDeviceWithCredential() { - Log.d(TAG, "Unlocking device with credential") + fun reportSuccessfulUnlock() { + Log.i(TAG, "Reporting successful unlock") trustManager.reportUnlockAttempt(true, context.userId) } - private fun enableTrustAgent() { + fun reportFailedUnlock() { + Log.i(TAG, "Reporting failed unlock") + trustManager.reportUnlockAttempt(false, context.userId) + } + + fun enableAndVerifyTrustAgentIsRunning(maxWait: Long = 30000L) { + enableTrustAgent() + verifyAgentIsRunning(maxWait) + } + + fun enableTrustAgent() { val componentName = ComponentName(context, serviceClass.java) val userId = context.userId Log.i(TAG, "Enabling trust agent ${componentName.flattenToString()} for user $userId") @@ -79,12 +99,18 @@ class TrustAgentRule<T : BaseTrustAgentService>( lockPatternUtils.setEnabledTrustAgents(agents, userId) } - private fun verifyAgentIsRunning() { - wait("${serviceClass.simpleName} to be running") { + fun verifyAgentIsRunning(maxWait: Long = 30000L) { + wait("${serviceClass.simpleName} to be running", maxWait) { BaseTrustAgentService.instance(serviceClass) != null } } + fun ensureAgentIsNotRunning(window: Long = 30000L) { + ensure("${serviceClass.simpleName} is not running", window) { + BaseTrustAgentService.instance(serviceClass) == null + } + } + private fun disableTrustAgent() { val componentName = ComponentName(context, serviceClass.java) val userId = context.userId @@ -97,13 +123,23 @@ class TrustAgentRule<T : BaseTrustAgentService>( companion object { /** - * Creates a new rule for the specified agent class. Example usage: + * Creates a new rule for the specified agent class. Starts with the device unlocked and + * the trust agent enabled. Example usage: * ``` * @get:Rule val rule = TrustAgentRule<MyTestAgent>() * ``` + * + * Also supports setting different device lock and trust agent enablement states: + * ``` + * @get:Rule val rule = TrustAgentRule<MyTestAgent>(startUnlocked = false, startEnabled = false) + * ``` */ - inline operator fun <reified T : BaseTrustAgentService> invoke() = - TrustAgentRule(T::class) + inline operator fun <reified T : BaseTrustAgentService> invoke( + startUnlocked: Boolean = true, + startEnabled: Boolean = true, + ) = + TrustAgentRule(T::class, startUnlocked, startEnabled) + private const val TAG = "TrustAgentRule" } diff --git a/tests/TrustTests/src/android/trust/test/lib/utils.kt b/tests/TrustTests/src/android/trust/test/lib/Utils.kt index e047202f6740..3b32b47a6160 100644 --- a/tests/TrustTests/src/android/trust/test/lib/utils.kt +++ b/tests/TrustTests/src/android/trust/test/lib/Utils.kt @@ -39,7 +39,7 @@ internal fun wait( ) { var waited = 0L var count = 0 - while (!conditionFunction.invoke(count)) { + while (!conditionFunction(count)) { assertWithMessage("Condition exceeded maximum wait time of $maxWait ms: $description") .that(waited <= maxWait) .isTrue() @@ -49,3 +49,34 @@ internal fun wait( Thread.sleep(rate) } } + +/** + * Ensures that [conditionFunction] is true with a failed assertion if it is not within [window] + * ms. + * + * The condition function can perform additional logic (for example, logging or attempting to make + * the condition become true). + * + * @param conditionFunction function which takes the attempt count & returns whether the condition + * is met + */ +internal fun ensure( + description: String? = null, + window: Long = 30000L, + rate: Long = 50L, + conditionFunction: (count: Int) -> Boolean +) { + var waited = 0L + var count = 0 + while (waited <= window) { + assertWithMessage("Condition failed within $window ms: $description").that( + conditionFunction( + count + ) + ).isTrue() + waited += rate + count++ + Log.i(TAG, "Ensuring $description ($waited/$window) #$count") + Thread.sleep(rate) + } +} |