diff options
74 files changed, 2006 insertions, 699 deletions
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 431755e092e3..17bb797bf8c5 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -39,6 +39,12 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.RemoteServiceException.BadForegroundServiceNotificationException; +import android.app.RemoteServiceException.CannotDeliverBroadcastException; +import android.app.RemoteServiceException.CannotPostForegroundServiceNotificationException; +import android.app.RemoteServiceException.CrashedByAdbException; +import android.app.RemoteServiceException.ForegroundServiceDidNotStartInTimeException; +import android.app.RemoteServiceException.MissingRequestPasswordComplexityPermissionException; import android.app.assist.AssistContent; import android.app.assist.AssistStructure; import android.app.backup.BackupAgent; @@ -1921,11 +1927,27 @@ public final class ActivityThread extends ClientTransactionHandler private void throwRemoteServiceException(String message, int typeId) { // Use a switch to ensure all the type IDs are unique. switch (typeId) { - case ForegroundServiceDidNotStartInTimeException.TYPE_ID: // 1 + case ForegroundServiceDidNotStartInTimeException.TYPE_ID: throw new ForegroundServiceDidNotStartInTimeException(message); - case RemoteServiceException.TYPE_ID: // 0 + + case CannotDeliverBroadcastException.TYPE_ID: + throw new CannotDeliverBroadcastException(message); + + case CannotPostForegroundServiceNotificationException.TYPE_ID: + throw new CannotPostForegroundServiceNotificationException(message); + + case BadForegroundServiceNotificationException.TYPE_ID: + throw new BadForegroundServiceNotificationException(message); + + case MissingRequestPasswordComplexityPermissionException.TYPE_ID: + throw new MissingRequestPasswordComplexityPermissionException(message); + + case CrashedByAdbException.TYPE_ID: + throw new CrashedByAdbException(message); + default: - throw new RemoteServiceException(message); + throw new RemoteServiceException(message + + " (with unwknown typeId:" + typeId + ")"); } } diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 0210a94eafcc..9e9e28b8bd4c 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -319,8 +319,6 @@ interface IActivityManager { void handleApplicationStrictModeViolation(in IBinder app, int penaltyMask, in StrictMode.ViolationInfo crashInfo); boolean isTopActivityImmersive(); - void crashApplication(int uid, int initialPid, in String packageName, int userId, - in String message, boolean force); void crashApplicationWithType(int uid, int initialPid, in String packageName, int userId, in String message, boolean force, int exceptionTypeId); /** @deprecated -- use getProviderMimeTypeAsync */ diff --git a/core/java/android/app/RemoteServiceException.java b/core/java/android/app/RemoteServiceException.java index 4b32463e2996..1038530d92d3 100644 --- a/core/java/android/app/RemoteServiceException.java +++ b/core/java/android/app/RemoteServiceException.java @@ -19,20 +19,109 @@ package android.app; import android.util.AndroidRuntimeException; /** - * Exception used by {@link ActivityThread} to crash an app process. + * Exception used by {@link ActivityThread} to crash an app process for an unknown cause. + * An exception of this class is no longer supposed to be thrown. Instead, we use fine-grained + * sub-exceptions. + * + * Subclasses must be registered in + * {@link android.app.ActivityThread#throwRemoteServiceException(java.lang.String, int)}. * * @hide */ public class RemoteServiceException extends AndroidRuntimeException { + public RemoteServiceException(String msg) { + super(msg); + } + /** - * The type ID passed to {@link IApplicationThread#scheduleCrash}. + * Exception used to crash an app process when it didn't call {@link Service#startForeground} + * in time after the service was started with + * {@link android.content.Context#startForegroundService}. * - * Assign a unique ID to each subclass. See the above method for the numbers that are already - * taken. + * @hide */ - public static final int TYPE_ID = 0; + public static class ForegroundServiceDidNotStartInTimeException extends RemoteServiceException { + /** The type ID passed to {@link IApplicationThread#scheduleCrash}. */ + public static final int TYPE_ID = 1; - public RemoteServiceException(String msg) { - super(msg); + public ForegroundServiceDidNotStartInTimeException(String msg) { + super(msg); + } + } + + /** + * Exception used to crash an app process when the system received a RemoteException + * while delivering a broadcast to an app process. + * + * @hide + */ + public static class CannotDeliverBroadcastException extends RemoteServiceException { + /** The type ID passed to {@link IApplicationThread#scheduleCrash}. */ + public static final int TYPE_ID = 2; + + public CannotDeliverBroadcastException(String msg) { + super(msg); + } + } + + /** + * Exception used to crash an app process when the system received a RemoteException + * while posting a notification of a foreground service. + * + * @hide + */ + public static class CannotPostForegroundServiceNotificationException + extends RemoteServiceException { + /** The type ID passed to {@link IApplicationThread#scheduleCrash}. */ + public static final int TYPE_ID = 3; + + public CannotPostForegroundServiceNotificationException(String msg) { + super(msg); + } + } + + /** + * Exception used to crash an app process when the system finds an error in a foreground service + * notification. + * + * @hide + */ + public static class BadForegroundServiceNotificationException extends RemoteServiceException { + /** The type ID passed to {@link IApplicationThread#scheduleCrash}. */ + public static final int TYPE_ID = 4; + + public BadForegroundServiceNotificationException(String msg) { + super(msg); + } + } + + /** + * Exception used to crash an app process when it calls a setting activity that requires + * the {@code REQUEST_PASSWORD_COMPLEXITY} permission. + * + * @hide + */ + public static class MissingRequestPasswordComplexityPermissionException + extends RemoteServiceException { + /** The type ID passed to {@link IApplicationThread#scheduleCrash}. */ + public static final int TYPE_ID = 5; + + public MissingRequestPasswordComplexityPermissionException(String msg) { + super(msg); + } + } + + /** + * Exception used to crash an app process by {@code adb shell am crash}. + * + * @hide + */ + public static class CrashedByAdbException extends RemoteServiceException { + /** The type ID passed to {@link IApplicationThread#scheduleCrash}. */ + public static final int TYPE_ID = 6; + + public CrashedByAdbException(String msg) { + super(msg); + } } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1b6a03c7a736..50155f992aaf 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9604,6 +9604,14 @@ public final class Settings { public static final String LOCKSCREEN_SHOW_WALLET = "lockscreen_show_wallet"; /** + * Whether to use the lockscreen double-line clock + * + * @hide + */ + public static final String LOCKSCREEN_USE_DOUBLE_LINE_CLOCK = + "lockscreen_use_double_line_clock"; + + /** * Specifies whether the web action API is enabled. * * @hide diff --git a/core/java/android/service/wallpaper/EngineWindowPage.java b/core/java/android/service/wallpaper/EngineWindowPage.java index 5ed0ad6f2aeb..006e3cdf03f8 100644 --- a/core/java/android/service/wallpaper/EngineWindowPage.java +++ b/core/java/android/service/wallpaper/EngineWindowPage.java @@ -24,7 +24,6 @@ import android.util.ArraySet; import java.util.Map; import java.util.Set; -import java.util.function.Consumer; /** * This class represents a page of a launcher page used by the wallpaper @@ -84,11 +83,6 @@ public class EngineWindowPage { return mCallbackAreas; } - /** run operations on this page */ - public synchronized void execSync(Consumer<EngineWindowPage> run) { - run.accept(this); - } - /** nullify the area color */ public void removeColor(RectF colorArea) { mRectFColors.remove(colorArea); diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index c77399f692f0..7b8410bba6ec 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -1490,7 +1490,7 @@ public abstract class WallpaperService extends Service { //below is the default implementation if (xOffset % xOffsetStep > MIN_PAGE_ALLOWED_MARGIN || !mSurfaceHolder.getSurface().isValid()) return; - int xPage; + int xCurrentPage; int xPages; if (!validStep(xOffsetStep)) { if (DEBUG) { @@ -1498,30 +1498,34 @@ public abstract class WallpaperService extends Service { } xOffset = 0; xOffsetStep = 1; - xPage = 0; + xCurrentPage = 0; xPages = 1; } else { xPages = Math.round(1 / xOffsetStep) + 1; xOffsetStep = (float) 1 / (float) xPages; float shrink = (float) (xPages - 1) / (float) xPages; xOffset *= shrink; - xPage = Math.round(xOffset / xOffsetStep); + xCurrentPage = Math.round(xOffset / xOffsetStep); } if (DEBUG) { - Log.d(TAG, "xPages " + xPages + " xPage " + xPage); + Log.d(TAG, "xPages " + xPages + " xPage " + xCurrentPage); Log.d(TAG, "xOffsetStep " + xOffsetStep + " xOffset " + xOffset); } - EngineWindowPage current; - synchronized (mLock) { + + float finalXOffsetStep = xOffsetStep; + float finalXOffset = xOffset; + mHandler.post(() -> { + int xPage = xCurrentPage; + EngineWindowPage current; if (mWindowPages.length == 0 || (mWindowPages.length != xPages)) { mWindowPages = new EngineWindowPage[xPages]; - initWindowPages(mWindowPages, xOffsetStep); + initWindowPages(mWindowPages, finalXOffsetStep); } if (mLocalColorsToAdd.size() != 0) { for (RectF colorArea : mLocalColorsToAdd) { if (!isValid(colorArea)) continue; mLocalColorAreas.add(colorArea); - int colorPage = getRectFPage(colorArea, xOffsetStep); + int colorPage = getRectFPage(colorArea, finalXOffsetStep); EngineWindowPage currentPage = mWindowPages[colorPage]; if (currentPage == null) { currentPage = new EngineWindowPage(); @@ -1539,7 +1543,8 @@ public abstract class WallpaperService extends Service { Log.e(TAG, "error xPage >= mWindowPages.length page: " + xPage); Log.e(TAG, "error on page " + xPage + " out of " + xPages); Log.e(TAG, - "error on xOffsetStep " + xOffsetStep + " xOffset " + xOffset); + "error on xOffsetStep " + finalXOffsetStep + + " xOffset " + finalXOffset); } xPage = mWindowPages.length - 1; } @@ -1547,13 +1552,14 @@ public abstract class WallpaperService extends Service { if (current == null) { if (DEBUG) Log.d(TAG, "making page " + xPage + " out of " + xPages); if (DEBUG) { - Log.d(TAG, "xOffsetStep " + xOffsetStep + " xOffset " + xOffset); + Log.d(TAG, "xOffsetStep " + finalXOffsetStep + " xOffset " + + finalXOffset); } current = new EngineWindowPage(); mWindowPages[xPage] = current; } - } - updatePage(current, xPage, xPages, xOffsetStep); + updatePage(current, xPage, xPages, finalXOffsetStep); + }); } private void initWindowPages(EngineWindowPage[] windowPages, float step) { @@ -1603,10 +1609,8 @@ public abstract class WallpaperService extends Service { if (DEBUG) Log.d(TAG, "result of pixel copy is " + res); if (res != PixelCopy.SUCCESS) { Bitmap lastBitmap = currentPage.getBitmap(); - currentPage.execSync((p) -> { - // assign the last bitmap taken for now - p.setBitmap(mLastScreenshot); - }); + // assign the last bitmap taken for now + currentPage.setBitmap(mLastScreenshot); Bitmap lastScreenshot = mLastScreenshot; if (lastScreenshot != null && !lastScreenshot.isRecycled() && !Objects.equals(lastBitmap, lastScreenshot)) { @@ -1615,10 +1619,8 @@ public abstract class WallpaperService extends Service { } else { mLastScreenshot = finalScreenShot; // going to hold this lock for a while - currentPage.execSync((p) -> { - p.setBitmap(finalScreenShot); - p.setLastUpdateTime(current); - }); + currentPage.setBitmap(finalScreenShot); + currentPage.setLastUpdateTime(current); updatePageColors(currentPage, pageIndx, numPages, xOffsetStep); } }, mHandler); @@ -1698,16 +1700,14 @@ public abstract class WallpaperService extends Service { private void resetWindowPages() { if (supportsLocalColorExtraction()) return; mLastWindowPage = -1; - synchronized (mLock) { + mHandler.post(() -> { for (int i = 0; i < mWindowPages.length; i++) { EngineWindowPage page = mWindowPages[i]; if (page != null) { - page.execSync((p) -> { - p.setLastUpdateTime(0L); - }); + page.setLastUpdateTime(0L); } } - } + }); } private int getRectFPage(RectF area, float step) { @@ -1730,10 +1730,10 @@ public abstract class WallpaperService extends Service { if (DEBUG) { Log.d(TAG, "addLocalColorsAreas adding local color areas " + regions); } - float step = mPendingXOffsetStep; List<WallpaperColors> colors = getLocalWallpaperColors(regions); - synchronized (mLock) { + mHandler.post(() -> { + float step = mPendingXOffsetStep; if (!validStep(step)) { step = 0; } @@ -1749,26 +1749,25 @@ public abstract class WallpaperService extends Service { page.addArea(area); WallpaperColors color = colors.get(i); if (color != null && !color.equals(page.getColors(area))) { - page.execSync(p -> { - p.addWallpaperColors(area, color); - }); + page.addWallpaperColors(area, color); } } else { mLocalColorsToAdd.add(area); } } - } - - for (int i = 0; i < colors.size() && colors.get(i) != null; i++) { - try { - mConnection.onLocalWallpaperColorsChanged(regions.get(i), colors.get(i), - mDisplayContext.getDisplayId()); - } catch (RemoteException e) { - Log.e(TAG, "Error calling Connection.onLocalWallpaperColorsChanged", e); - return; + for (int i = 0; i < colors.size() && colors.get(i) != null; i++) { + try { + mConnection.onLocalWallpaperColorsChanged(regions.get(i), colors.get(i), + mDisplayContext.getDisplayId()); + } catch (RemoteException e) { + Log.e(TAG, "Error calling Connection.onLocalWallpaperColorsChanged", e); + return; + } } - } - processLocalColors(mPendingXOffset, mPendingYOffset); + processLocalColors(mPendingXOffset, mPendingYOffset); + }); + + } /** @@ -1778,7 +1777,7 @@ public abstract class WallpaperService extends Service { */ public void removeLocalColorsAreas(@NonNull List<RectF> regions) { if (supportsLocalColorExtraction()) return; - synchronized (mLock) { + mHandler.post(() -> { float step = mPendingXOffsetStep; mLocalColorsToAdd.removeAll(regions); mLocalColorAreas.removeAll(regions); @@ -1792,12 +1791,10 @@ public abstract class WallpaperService extends Service { // no page should be null EngineWindowPage page = mWindowPages[pageInx]; if (page != null) { - page.execSync(p -> { - p.removeArea(area); - }); + page.removeArea(area); } } - } + }); } private @NonNull List<WallpaperColors> getLocalWallpaperColors(@NonNull List<RectF> areas) { diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index f724285df9dc..3d4d9eca6b16 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -338,7 +338,10 @@ public class RemoteViews implements Parcelable, Filter { * Maps bitmaps to unique indicies to avoid Bitmap duplication. */ @UnsupportedAppUsage - private BitmapCache mBitmapCache; + private BitmapCache mBitmapCache = new BitmapCache(); + + /** Cache of ApplicationInfos used by collection items. */ + private ApplicationInfoCache mApplicationInfoCache = new ApplicationInfoCache(); /** * Indicates whether or not this RemoteViews object is contained as a child of any other @@ -576,7 +579,7 @@ public class RemoteViews implements Parcelable, Filter { return 0; } - public void setBitmapCache(BitmapCache bitmapCache) { + public void setHierarchyRootData(HierarchyRootData root) { // Do nothing } @@ -605,14 +608,6 @@ public class RemoteViews implements Parcelable, Filter { return false; } - /** - * Overridden by subclasses which have (or inherit) an ApplicationInfo instance - * as member variable - */ - public boolean hasSameAppInfo(ApplicationInfo parentInfo) { - return true; - } - public void visitUris(@NonNull Consumer<Uri> visitor) { // Nothing to visit by default } @@ -690,9 +685,8 @@ public class RemoteViews implements Parcelable, Filter { } } - // Because pruning can remove the need for bitmaps, we reconstruct the bitmap cache - mBitmapCache = new BitmapCache(); - setBitmapCache(mBitmapCache); + // Because pruning can remove the need for bitmaps, we reconstruct the caches. + reconstructCaches(); } /** @@ -938,23 +932,70 @@ public class RemoteViews implements Parcelable, Filter { ArrayList<RemoteViews> list; } - private static class SetRemoteCollectionItemListAdapterAction extends Action { + /** + * Cache of {@link ApplicationInfo}s that can be used to ensure that the same + * {@link ApplicationInfo} instance is used throughout the RemoteViews. + */ + private static class ApplicationInfoCache { + private final Map<Pair<String, Integer>, ApplicationInfo> mPackageUserToApplicationInfo; + + ApplicationInfoCache() { + mPackageUserToApplicationInfo = new ArrayMap<>(); + } + + /** + * Adds the {@link ApplicationInfo} to the cache if it's not present. Returns either the + * provided {@code applicationInfo} or a previously added value with the same package name + * and uid. + */ + @Nullable + ApplicationInfo getOrPut(@Nullable ApplicationInfo applicationInfo) { + Pair<String, Integer> key = getPackageUserKey(applicationInfo); + if (key == null) return null; + return mPackageUserToApplicationInfo.computeIfAbsent(key, ignored -> applicationInfo); + } + + /** Puts the {@link ApplicationInfo} in the cache, replacing any previously stored value. */ + void put(@Nullable ApplicationInfo applicationInfo) { + Pair<String, Integer> key = getPackageUserKey(applicationInfo); + if (key == null) return; + mPackageUserToApplicationInfo.put(key, applicationInfo); + } + + /** + * Returns the currently stored {@link ApplicationInfo} from the cache matching + * {@code applicationInfo}, or null if there wasn't any. + */ + @Nullable ApplicationInfo get(@Nullable ApplicationInfo applicationInfo) { + Pair<String, Integer> key = getPackageUserKey(applicationInfo); + if (key == null) return null; + return mPackageUserToApplicationInfo.get(key); + } + } + + private class SetRemoteCollectionItemListAdapterAction extends Action { private final RemoteCollectionItems mItems; SetRemoteCollectionItemListAdapterAction(@IdRes int id, RemoteCollectionItems items) { viewId = id; mItems = items; + mItems.setHierarchyRootData(getHierarchyRootData()); } SetRemoteCollectionItemListAdapterAction(Parcel parcel) { viewId = parcel.readInt(); - mItems = parcel.readTypedObject(RemoteCollectionItems.CREATOR); + mItems = new RemoteCollectionItems(parcel, getHierarchyRootData()); + } + + @Override + public void setHierarchyRootData(HierarchyRootData rootData) { + mItems.setHierarchyRootData(rootData); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(viewId); - dest.writeTypedObject(mItems, flags); + mItems.writeToParcel(dest, flags, /* attached= */ true); } @Override @@ -1602,8 +1643,8 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void setBitmapCache(BitmapCache bitmapCache) { - bitmapId = bitmapCache.getBitmapId(bitmap); + public void setHierarchyRootData(HierarchyRootData rootData) { + bitmapId = rootData.mBitmapCache.getBitmapId(bitmap); } @Override @@ -2220,15 +2261,6 @@ public class RemoteViews implements Parcelable, Filter { } } - private void configureRemoteViewsAsChild(RemoteViews rv) { - rv.setBitmapCache(mBitmapCache); - rv.setNotRoot(); - } - - void setNotRoot() { - mIsRoot = false; - } - private static boolean hasStableId(View view) { Object tag = view.getTag(com.android.internal.R.id.remote_views_stable_id); return tag != null; @@ -2302,17 +2334,14 @@ public class RemoteViews implements Parcelable, Filter { mNestedViews = nestedViews; mIndex = index; mStableId = stableId; - if (nestedViews != null) { - configureRemoteViewsAsChild(nestedViews); - } + nestedViews.configureAsChild(getHierarchyRootData()); } - ViewGroupActionAdd(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info, - int depth, Map<Class, Object> classCookies) { + ViewGroupActionAdd(Parcel parcel, ApplicationInfo info, int depth) { viewId = parcel.readInt(); mIndex = parcel.readInt(); mStableId = parcel.readInt(); - mNestedViews = new RemoteViews(parcel, bitmapCache, info, depth, classCookies); + mNestedViews = new RemoteViews(parcel, getHierarchyRootData(), info, depth); mNestedViews.addFlags(mApplyFlags); } @@ -2324,8 +2353,8 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public boolean hasSameAppInfo(ApplicationInfo parentInfo) { - return mNestedViews.hasSameAppInfo(parentInfo); + public void setHierarchyRootData(HierarchyRootData root) { + mNestedViews.configureAsChild(root); } private int findViewIndexToRecycle(ViewGroup target, RemoteViews newContent) { @@ -2494,11 +2523,6 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void setBitmapCache(BitmapCache bitmapCache) { - mNestedViews.setBitmapCache(bitmapCache); - } - - @Override public int mergeBehavior() { return MERGE_APPEND; } @@ -3505,8 +3529,7 @@ public class RemoteViews implements Parcelable, Filter { protected RemoteViews(ApplicationInfo application, @LayoutRes int layoutId) { mApplication = application; mLayoutId = layoutId; - mBitmapCache = new BitmapCache(); - mClassCookies = null; + mApplicationInfoCache.put(application); } private boolean hasMultipleLayouts() { @@ -3562,12 +3585,10 @@ public class RemoteViews implements Parcelable, Filter { mLandscape = landscape; mPortrait = portrait; - mBitmapCache = new BitmapCache(); - configureRemoteViewsAsChild(landscape); - configureRemoteViewsAsChild(portrait); - mClassCookies = (portrait.mClassCookies != null) ? portrait.mClassCookies : landscape.mClassCookies; + + configureDescendantsAsChildren(); } /** @@ -3593,10 +3614,12 @@ public class RemoteViews implements Parcelable, Filter { throw new IllegalArgumentException("Too many RemoteViews in constructor"); } if (remoteViews.size() == 1) { - initializeFrom(remoteViews.values().iterator().next()); + // If the map only contains a single mapping, treat this as if that RemoteViews was + // passed as the top-level RemoteViews. + RemoteViews single = remoteViews.values().iterator().next(); + initializeFrom(single, /* hierarchyRoot= */ single); return; } - mBitmapCache = new BitmapCache(); mClassCookies = initializeSizedRemoteViews( remoteViews.entrySet().stream().map( entry -> { @@ -3611,6 +3634,8 @@ public class RemoteViews implements Parcelable, Filter { mLayoutId = smallestView.mLayoutId; mViewId = smallestView.mViewId; mLightBackgroundLayoutId = smallestView.mLightBackgroundLayoutId; + + configureDescendantsAsChildren(); } // Initialize mSizedRemoteViews and return the class cookies. @@ -3639,7 +3664,6 @@ public class RemoteViews implements Parcelable, Filter { } else { sizedRemoteViews.add(view); } - configureRemoteViewsAsChild(view); view.setIdealSize(size); if (classCookies == null) { classCookies = view.mClassCookies; @@ -3654,13 +3678,38 @@ public class RemoteViews implements Parcelable, Filter { * Creates a copy of another RemoteViews. */ public RemoteViews(RemoteViews src) { - initializeFrom(src); + initializeFrom(src, /* hierarchyRoot= */ null); } - private void initializeFrom(RemoteViews src) { - mBitmapCache = src.mBitmapCache; + /** + * No-arg constructor for use with {@link #initializeFrom(RemoteViews, RemoteViews)}. A + * constructor taking two RemoteViews parameters would clash with the landscape/portrait + * constructor. + */ + private RemoteViews() {} + + private static RemoteViews createInitializedFrom(@NonNull RemoteViews src, + @Nullable RemoteViews hierarchyRoot) { + RemoteViews child = new RemoteViews(); + child.initializeFrom(src, hierarchyRoot); + return child; + } + + private void initializeFrom(@NonNull RemoteViews src, @Nullable RemoteViews hierarchyRoot) { + if (hierarchyRoot == null || src.mIsRoot) { + // If there's no provided root, or if src was itself a root, then this RemoteViews is + // the root of the new hierarchy. + mIsRoot = true; + mBitmapCache = new BitmapCache(); + mApplicationInfoCache = new ApplicationInfoCache(); + hierarchyRoot = this; + } else { + // Otherwise, we're a descendant in the hierarchy. + mIsRoot = false; + mBitmapCache = hierarchyRoot.mBitmapCache; + mApplicationInfoCache = hierarchyRoot.mApplicationInfoCache; + } mApplication = src.mApplication; - mIsRoot = src.mIsRoot; mLayoutId = src.mLayoutId; mLightBackgroundLayoutId = src.mLightBackgroundLayoutId; mApplyFlags = src.mApplyFlags; @@ -3669,21 +3718,21 @@ public class RemoteViews implements Parcelable, Filter { mProviderInstanceId = src.mProviderInstanceId; if (src.hasLandscapeAndPortraitLayouts()) { - mLandscape = new RemoteViews(src.mLandscape); - mPortrait = new RemoteViews(src.mPortrait); + mLandscape = createInitializedFrom(src.mLandscape, hierarchyRoot); + mPortrait = createInitializedFrom(src.mPortrait, hierarchyRoot); } if (src.hasSizedRemoteViews()) { mSizedRemoteViews = new ArrayList<>(src.mSizedRemoteViews.size()); for (RemoteViews srcView : src.mSizedRemoteViews) { - mSizedRemoteViews.add(new RemoteViews(srcView)); + mSizedRemoteViews.add(createInitializedFrom(srcView, hierarchyRoot)); } } if (src.mActions != null) { Parcel p = Parcel.obtain(); p.putClassCookies(mClassCookies); - src.writeActionsToParcel(p); + src.writeActionsToParcel(p, /* flags= */ 0); p.setDataPosition(0); // Since src is already in memory, we do not care about stack overflow as it has // already been read once. @@ -3691,9 +3740,11 @@ public class RemoteViews implements Parcelable, Filter { p.recycle(); } - // Now that everything is initialized and duplicated, setting a new BitmapCache will - // re-initialize the cache. - setBitmapCache(new BitmapCache()); + // Now that everything is initialized and duplicated, create new caches for this + // RemoteViews and recursively set up all descendants. + if (mIsRoot) { + reconstructCaches(); + } } /** @@ -3702,11 +3753,11 @@ public class RemoteViews implements Parcelable, Filter { * @param parcel */ public RemoteViews(Parcel parcel) { - this(parcel, null, null, 0, null); + this(parcel, /* rootParent= */ null, /* info= */ null, /* depth= */ 0); } - private RemoteViews(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info, int depth, - Map<Class, Object> classCookies) { + private RemoteViews(@NonNull Parcel parcel, @Nullable HierarchyRootData rootData, + @Nullable ApplicationInfo info, int depth) { if (depth > MAX_NESTED_VIEWS && (UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)) { throw new IllegalArgumentException("Too many nested views."); @@ -3715,20 +3766,17 @@ public class RemoteViews implements Parcelable, Filter { int mode = parcel.readInt(); - // We only store a bitmap cache in the root of the RemoteViews. - if (bitmapCache == null) { + if (rootData == null) { + // We only store a bitmap cache in the root of the RemoteViews. mBitmapCache = new BitmapCache(parcel); // Store the class cookies such that they are available when we clone this RemoteView. mClassCookies = parcel.copyClassCookies(); } else { - setBitmapCache(bitmapCache); - mClassCookies = classCookies; - setNotRoot(); + configureAsChild(rootData); } if (mode == MODE_NORMAL) { - mApplication = parcel.readInt() == 0 ? info : - ApplicationInfo.CREATOR.createFromParcel(parcel); + mApplication = ApplicationInfo.CREATOR.createFromParcel(parcel); mIdealSize = parcel.readInt() == 0 ? null : SizeF.CREATOR.createFromParcel(parcel); mLayoutId = parcel.readInt(); mViewId = parcel.readInt(); @@ -3743,8 +3791,7 @@ public class RemoteViews implements Parcelable, Filter { } List<RemoteViews> remoteViews = new ArrayList<>(numViews); for (int i = 0; i < numViews; i++) { - RemoteViews view = new RemoteViews(parcel, mBitmapCache, info, depth, - mClassCookies); + RemoteViews view = new RemoteViews(parcel, getHierarchyRootData(), info, depth); info = view.mApplication; remoteViews.add(view); } @@ -3756,9 +3803,9 @@ public class RemoteViews implements Parcelable, Filter { mLightBackgroundLayoutId = smallestView.mLightBackgroundLayoutId; } else { // MODE_HAS_LANDSCAPE_AND_PORTRAIT - mLandscape = new RemoteViews(parcel, mBitmapCache, info, depth, mClassCookies); - mPortrait = new RemoteViews(parcel, mBitmapCache, mLandscape.mApplication, depth, - mClassCookies); + mLandscape = new RemoteViews(parcel, getHierarchyRootData(), info, depth); + mPortrait = + new RemoteViews(parcel, getHierarchyRootData(), mLandscape.mApplication, depth); mApplication = mPortrait.mApplication; mLayoutId = mPortrait.mLayoutId; mViewId = mPortrait.mViewId; @@ -3766,6 +3813,11 @@ public class RemoteViews implements Parcelable, Filter { } mApplyFlags = parcel.readInt(); mProviderInstanceId = parcel.readLong(); + + // Ensure that all descendants have their caches set up recursively. + if (mIsRoot) { + configureDescendantsAsChildren(); + } } private void readActionsFromParcel(Parcel parcel, int depth) { @@ -3788,8 +3840,7 @@ public class RemoteViews implements Parcelable, Filter { case REFLECTION_ACTION_TAG: return new ReflectionAction(parcel); case VIEW_GROUP_ACTION_ADD_TAG: - return new ViewGroupActionAdd(parcel, mBitmapCache, mApplication, depth, - mClassCookies); + return new ViewGroupActionAdd(parcel, mApplication, depth); case VIEW_GROUP_ACTION_REMOVE_TAG: return new ViewGroupActionRemove(parcel); case VIEW_CONTENT_NAVIGATION_TAG: @@ -3879,28 +3930,56 @@ public class RemoteViews implements Parcelable, Filter { } /** - * Recursively sets BitmapCache in the hierarchy and update the bitmap ids. + * Sets the root of the hierarchy and then recursively traverses the tree to update the root + * and populate caches for all descendants. + */ + private void configureAsChild(@NonNull HierarchyRootData rootData) { + mIsRoot = false; + mBitmapCache = rootData.mBitmapCache; + mApplicationInfoCache = rootData.mApplicationInfoCache; + mClassCookies = rootData.mClassCookies; + configureDescendantsAsChildren(); + } + + /** + * Recursively traverses the tree to update the root and populate caches for all descendants. */ - private void setBitmapCache(BitmapCache bitmapCache) { - mBitmapCache = bitmapCache; + private void configureDescendantsAsChildren() { + // Before propagating down the tree, replace our application from the root application info + // cache, to ensure the same instance is present throughout the hierarchy to allow for + // squashing. + mApplication = mApplicationInfoCache.getOrPut(mApplication); + + HierarchyRootData rootData = getHierarchyRootData(); if (hasSizedRemoteViews()) { for (RemoteViews remoteView : mSizedRemoteViews) { - remoteView.setBitmapCache(bitmapCache); + remoteView.configureAsChild(rootData); } } else if (hasLandscapeAndPortraitLayouts()) { - mLandscape.setBitmapCache(bitmapCache); - mPortrait.setBitmapCache(bitmapCache); + mLandscape.configureAsChild(rootData); + mPortrait.configureAsChild(rootData); } else { if (mActions != null) { - final int count = mActions.size(); - for (int i = 0; i < count; ++i) { - mActions.get(i).setBitmapCache(bitmapCache); + for (Action action : mActions) { + action.setHierarchyRootData(rootData); } } } } /** + * Recreates caches at the root level of the hierarchy, then recursively populates the caches + * down the hierarchy. + */ + private void reconstructCaches() { + if (!mIsRoot) return; + mBitmapCache = new BitmapCache(); + mApplicationInfoCache = new ApplicationInfoCache(); + mApplication = mApplicationInfoCache.getOrPut(mApplication); + configureDescendantsAsChildren(); + } + + /** * Returns an estimate of the bitmap heap memory usage for this RemoteViews. */ /** @hide */ @@ -5848,21 +5927,18 @@ public class RemoteViews implements Parcelable, Filter { /** @hide */ public void updateAppInfo(@NonNull ApplicationInfo info) { - if (mApplication != null && mApplication.sourceDir.equals(info.sourceDir)) { + ApplicationInfo existing = mApplicationInfoCache.get(info); + if (existing != null && !existing.sourceDir.equals(info.sourceDir)) { // Overlay paths are generated against a particular version of an application. // The overlays paths of a newly upgraded application are incompatible with the // old version of the application. - mApplication = info; - } - if (hasSizedRemoteViews()) { - for (RemoteViews layout : mSizedRemoteViews) { - layout.updateAppInfo(info); - } - } - if (hasLandscapeAndPortraitLayouts()) { - mLandscape.updateAppInfo(info); - mPortrait.updateAppInfo(info); + return; } + + // If we can update to the new AppInfo, put it in the cache and propagate the change + // throughout the hierarchy. + mApplicationInfoCache.put(info); + configureDescendantsAsChildren(); } private Context getContextForResourcesEnsuringCorrectCachedApkPaths(Context context) { @@ -6028,6 +6104,8 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { + boolean prevSquashingAllowed = dest.allowSquashing(); + if (!hasMultipleLayouts()) { dest.writeInt(MODE_NORMAL); // We only write the bitmap cache if we are the root RemoteViews, as this cache @@ -6035,12 +6113,7 @@ public class RemoteViews implements Parcelable, Filter { if (mIsRoot) { mBitmapCache.writeBitmapsToParcel(dest, flags); } - if (!mIsRoot && (flags & PARCELABLE_ELIDE_DUPLICATES) != 0) { - dest.writeInt(0); - } else { - dest.writeInt(1); - mApplication.writeToParcel(dest, flags); - } + mApplication.writeToParcel(dest, flags); if (mIsRoot || mIdealSize == null) { dest.writeInt(0); } else { @@ -6050,17 +6123,15 @@ public class RemoteViews implements Parcelable, Filter { dest.writeInt(mLayoutId); dest.writeInt(mViewId); dest.writeInt(mLightBackgroundLayoutId); - writeActionsToParcel(dest); + writeActionsToParcel(dest, flags); } else if (hasSizedRemoteViews()) { dest.writeInt(MODE_HAS_SIZED_REMOTEVIEWS); if (mIsRoot) { mBitmapCache.writeBitmapsToParcel(dest, flags); } - int childFlags = flags; dest.writeInt(mSizedRemoteViews.size()); for (RemoteViews view : mSizedRemoteViews) { - view.writeToParcel(dest, childFlags); - childFlags |= PARCELABLE_ELIDE_DUPLICATES; + view.writeToParcel(dest, flags); } } else { dest.writeInt(MODE_HAS_LANDSCAPE_AND_PORTRAIT); @@ -6071,13 +6142,15 @@ public class RemoteViews implements Parcelable, Filter { } mLandscape.writeToParcel(dest, flags); // Both RemoteViews already share the same package and user - mPortrait.writeToParcel(dest, flags | PARCELABLE_ELIDE_DUPLICATES); + mPortrait.writeToParcel(dest, flags); } dest.writeInt(mApplyFlags); dest.writeLong(mProviderInstanceId); + + dest.restoreAllowSquashing(prevSquashingAllowed); } - private void writeActionsToParcel(Parcel parcel) { + private void writeActionsToParcel(Parcel parcel, int flags) { int count; if (mActions != null) { count = mActions.size(); @@ -6088,8 +6161,7 @@ public class RemoteViews implements Parcelable, Filter { for (int i = 0; i < count; i++) { Action a = mActions.get(i); parcel.writeInt(a.getActionTag()); - a.writeToParcel(parcel, a.hasSameAppInfo(mApplication) - ? PARCELABLE_ELIDE_DUPLICATES : 0); + a.writeToParcel(parcel, flags); } } @@ -6568,6 +6640,8 @@ public class RemoteViews implements Parcelable, Filter { private final boolean mHasStableIds; private final int mViewTypeCount; + private HierarchyRootData mHierarchyRootData; + RemoteCollectionItems( long[] ids, RemoteViews[] views, boolean hasStableIds, int viewTypeCount) { mIds = ids; @@ -6590,16 +6664,53 @@ public class RemoteViews implements Parcelable, Filter { "View type count is set to " + viewTypeCount + ", but the collection " + "contains " + layoutIdCount + " different layout ids"); } + + // Until the collection items are attached to a parent, we configure the first item + // to be the root of the others to share caches and save space during serialization. + if (views.length > 0) { + setHierarchyRootData(views[0].getHierarchyRootData()); + views[0].mIsRoot = true; + } } - RemoteCollectionItems(Parcel in) { + RemoteCollectionItems(@NonNull Parcel in, @Nullable HierarchyRootData hierarchyRootData) { + mHasStableIds = in.readBoolean(); + mViewTypeCount = in.readInt(); int length = in.readInt(); mIds = new long[length]; in.readLongArray(mIds); + + boolean attached = in.readBoolean(); mViews = new RemoteViews[length]; - in.readTypedArray(mViews, RemoteViews.CREATOR); - mHasStableIds = in.readBoolean(); - mViewTypeCount = in.readInt(); + int firstChildIndex; + if (attached) { + if (hierarchyRootData == null) { + throw new IllegalStateException("Cannot unparcel a RemoteCollectionItems that " + + "was parceled as attached without providing data for a root " + + "RemoteViews"); + } + mHierarchyRootData = hierarchyRootData; + firstChildIndex = 0; + } else { + mViews[0] = new RemoteViews(in); + mHierarchyRootData = mViews[0].getHierarchyRootData(); + firstChildIndex = 1; + } + + for (int i = firstChildIndex; i < length; i++) { + mViews[i] = new RemoteViews( + in, + mHierarchyRootData, + /* info= */ null, + /* depth= */ 0); + } + } + + void setHierarchyRootData(@NonNull HierarchyRootData rootData) { + mHierarchyRootData = rootData; + for (RemoteViews view : mViews) { + view.configureAsChild(rootData); + } } @Override @@ -6609,11 +6720,39 @@ public class RemoteViews implements Parcelable, Filter { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeInt(mIds.length); - dest.writeLongArray(mIds); - dest.writeTypedArray(mViews, flags); + writeToParcel(dest, flags, /* attached= */ false); + } + + private void writeToParcel(@NonNull Parcel dest, int flags, boolean attached) { + boolean prevAllowSquashing = dest.allowSquashing(); + dest.writeBoolean(mHasStableIds); dest.writeInt(mViewTypeCount); + dest.writeInt(mIds.length); + dest.writeLongArray(mIds); + + if (attached && mHierarchyRootData == null) { + throw new IllegalStateException("Cannot call writeToParcelAttached for a " + + "RemoteCollectionItems without first calling setHierarchyRootData()"); + } + + // Write whether we parceled as attached or not. This allows cleaner validation and + // proper error messaging when unparceling later. + dest.writeBoolean(attached); + boolean restoreRoot = false; + if (!attached && mViews.length > 0 && !mViews[0].mIsRoot) { + // If we're writing unattached, temporarily set the first item as the root so that + // the bitmap cache is written to the parcel. + restoreRoot = true; + mViews[0].mIsRoot = true; + } + + for (RemoteViews view : mViews) { + view.writeToParcel(dest, flags); + } + + if (restoreRoot) mViews[0].mIsRoot = false; + dest.restoreAllowSquashing(prevAllowSquashing); } /** @@ -6671,7 +6810,7 @@ public class RemoteViews implements Parcelable, Filter { @NonNull @Override public RemoteCollectionItems createFromParcel(@NonNull Parcel source) { - return new RemoteCollectionItems(source); + return new RemoteCollectionItems(source, /* hierarchyRoot= */ null); } @NonNull @@ -6846,4 +6985,29 @@ public class RemoteViews implements Parcelable, Filter { viewId |= childId; return viewId; } + + @Nullable + private static Pair<String, Integer> getPackageUserKey(@Nullable ApplicationInfo info) { + if (info == null || info.packageName == null) return null; + return Pair.create(info.packageName, info.uid); + } + + private HierarchyRootData getHierarchyRootData() { + return new HierarchyRootData(mBitmapCache, mApplicationInfoCache, mClassCookies); + } + + private static final class HierarchyRootData { + final BitmapCache mBitmapCache; + final ApplicationInfoCache mApplicationInfoCache; + final Map<Class, Object> mClassCookies; + + HierarchyRootData( + BitmapCache bitmapCache, + ApplicationInfoCache applicationInfoCache, + Map<Class, Object> classCookies) { + mBitmapCache = bitmapCache; + mApplicationInfoCache = applicationInfoCache; + mClassCookies = classCookies; + } + } } diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index 6f17ea994699..4a7c50d8b934 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -304,18 +304,22 @@ public class ActivityThreadTest { final int numOfConfig = activity.mNumOfConfigChanges; final Configuration processConfigLandscape = new Configuration(); + processConfigLandscape.orientation = ORIENTATION_LANDSCAPE; processConfigLandscape.windowConfiguration.setBounds(new Rect(0, 0, 100, 60)); processConfigLandscape.seq = BASE_SEQ + 1; final Configuration activityConfigLandscape = new Configuration(); + activityConfigLandscape.orientation = ORIENTATION_LANDSCAPE; activityConfigLandscape.windowConfiguration.setBounds(new Rect(0, 0, 100, 50)); activityConfigLandscape.seq = BASE_SEQ + 2; final Configuration processConfigPortrait = new Configuration(); + processConfigPortrait.orientation = ORIENTATION_PORTRAIT; processConfigPortrait.windowConfiguration.setBounds(new Rect(0, 0, 60, 100)); processConfigPortrait.seq = BASE_SEQ + 3; final Configuration activityConfigPortrait = new Configuration(); + activityConfigPortrait.orientation = ORIENTATION_PORTRAIT; activityConfigPortrait.windowConfiguration.setBounds(new Rect(0, 0, 50, 100)); activityConfigPortrait.seq = BASE_SEQ + 4; @@ -343,7 +347,8 @@ public class ActivityThreadTest { assertEquals(activityConfigPortrait.windowConfiguration.getBounds(), bounds); // Ensure that Activity#onConfigurationChanged() not be called because the changes in - // WindowConfiguration shouldn't be reported. + // WindowConfiguration shouldn't be reported, and we only apply the latest Configuration + // update in transaction. assertEquals(numOfConfig, activity.mNumOfConfigChanges); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java index 194b6330d92c..89d7a407e459 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java @@ -39,6 +39,8 @@ class TaskFragmentAnimationAdapter { final Transformation mTransformation = new Transformation(); final float[] mMatrix = new float[9]; + final float[] mVecs = new float[4]; + final Rect mRect = new Rect(); private boolean mIsFirstFrame = true; TaskFragmentAnimationAdapter(@NonNull Animation animation, @@ -76,6 +78,22 @@ class TaskFragmentAnimationAdapter { mTarget.localBounds.left, mTarget.localBounds.top); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); + + // Open/close animation may scale up the surface. Apply an inverse scale to the window crop + // so that it will not be covering other windows. + mVecs[1] = mVecs[2] = 0; + mVecs[0] = mVecs[3] = 1; + mTransformation.getMatrix().mapVectors(mVecs); + mVecs[0] = 1.f / mVecs[0]; + mVecs[3] = 1.f / mVecs[3]; + final Rect clipRect = mTarget.localBounds; + mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f); + mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f); + mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f); + mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f); + mRect.offsetTo(Math.round(mTarget.localBounds.width() * (1 - mVecs[0]) / 2.f), + Math.round(mTarget.localBounds.height() * (1 - mVecs[3]) / 2.f)); + t.setWindowCrop(mLeash, mRect); } /** Called after animation finished. */ @@ -157,8 +175,6 @@ class TaskFragmentAnimationAdapter { * Should be used for the animation of the {@link RemoteAnimationTarget} that has size change. */ static class BoundsChangeAdapter extends TaskFragmentAnimationAdapter { - private final float[] mVecs = new float[4]; - private final Rect mRect = new Rect(); BoundsChangeAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { super(animation, target); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index 32d447ef1586..fe9ce971d4d9 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -18,9 +18,12 @@ package androidx.window.extensions.layout; import static android.view.Display.DEFAULT_DISPLAY; +import static androidx.window.common.DisplayFeature.COMMON_STATE_FLAT; +import static androidx.window.common.DisplayFeature.COMMON_STATE_HALF_OPENED; import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; +import android.annotation.Nullable; import android.app.Activity; import android.content.Context; import android.graphics.Rect; @@ -119,22 +122,45 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { return !mWindowLayoutChangeListeners.isEmpty(); } - private int getFeatureState(DisplayFeature feature) { + /** + * Calculate the {@link DisplayFeature.State} from the feature or the device posture producer. + * If the given {@link DisplayFeature.State} is not valid then {@code null} will be returned. + * The {@link FoldingFeature} should be ignored in the case of an invalid + * {@link DisplayFeature.State}. + * + * @param feature a {@link DisplayFeature} to provide the feature state if present. + * @return {@link DisplayFeature.State} of the hinge if present or the state from the posture + * produce if present. + */ + @Nullable + private Integer getFeatureState(DisplayFeature feature) { Integer featureState = feature.getState(); Optional<Integer> posture = mDevicePostureProducer.getData(); - int fallbackPosture = posture.orElse(DisplayFeature.COMMON_STATE_FLAT); - int displayFeatureState = featureState == null ? fallbackPosture : featureState; - return convertToExtensionState(displayFeatureState); + Integer state = featureState == null ? posture.orElse(null) : featureState; + return convertToExtensionState(state); } - private int convertToExtensionState(int state) { - switch (state) { - case DisplayFeature.COMMON_STATE_FLAT: - return FoldingFeature.STATE_FLAT; - case DisplayFeature.COMMON_STATE_HALF_OPENED: - return FoldingFeature.STATE_HALF_OPENED; + /** + * A convenience method to translate from the common feature state to the extensions feature + * state. More specifically, translates from {@link DisplayFeature.State} to + * {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED}. If it is not + * possible to translate, then we will return a {@code null} value. + * + * @param state if it matches a value in {@link DisplayFeature.State}, {@code null} otherwise. + * @return a {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED} if + * the given state matches a value in {@link DisplayFeature.State} and {@code null} otherwise. + */ + @Nullable + private Integer convertToExtensionState(@Nullable Integer state) { + if (state == null) { // The null check avoids a NullPointerException. + return null; + } else if (state == COMMON_STATE_FLAT) { + return FoldingFeature.STATE_FLAT; + } else if (state == COMMON_STATE_HALF_OPENED) { + return FoldingFeature.STATE_HALF_OPENED; + } else { + return null; } - return FoldingFeature.STATE_FLAT; } private void onDisplayFeaturesChanged() { @@ -151,6 +177,25 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { return new WindowLayoutInfo(displayFeatures); } + /** + * Translate from the {@link DisplayFeature} to + * {@link androidx.window.extensions.layout.DisplayFeature} for a given {@link Activity}. If a + * {@link DisplayFeature} is not valid then it will be omitted. + * + * For a {@link FoldingFeature} the bounds are localized into the {@link Activity} window + * coordinate space and the state is calculated either from {@link DisplayFeature#getState()} or + * {@link #mDisplayFeatureProducer}. The state from {@link #mDisplayFeatureProducer} may not be + * valid since {@link #mDisplayFeatureProducer} is a general state controller. If the state is + * not valid, the {@link FoldingFeature} is omitted from the {@link List} of + * {@link androidx.window.extensions.layout.DisplayFeature}. If the bounds are not valid, + * constructing a {@link FoldingFeature} will throw an {@link IllegalArgumentException} since + * this can cause negative UI effects down stream. + * + * @param activity a proxy for the {@link android.view.Window} that contains the + * {@link androidx.window.extensions.layout.DisplayFeature}. + * @return a {@link List} of valid {@link androidx.window.extensions.layout.DisplayFeature} that + * are within the {@link android.view.Window} of the {@link Activity} + */ private List<androidx.window.extensions.layout.DisplayFeature> getDisplayFeatures( @NonNull Activity activity) { List<androidx.window.extensions.layout.DisplayFeature> features = new ArrayList<>(); @@ -170,6 +215,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { if (storedFeatures.isPresent()) { for (DisplayFeature baseFeature : storedFeatures.get()) { + Integer state = getFeatureState(baseFeature); + if (state == null) { + continue; + } Rect featureRect = baseFeature.getRect(); rotateRectToDisplayRotation(displayId, featureRect); transformToWindowSpaceRect(activity, featureRect); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java index f5678776ed78..c3ce3627fb0b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java @@ -74,7 +74,7 @@ public class ShellInitImpl { Optional<PipTouchHandler> pipTouchHandlerOptional, FullscreenTaskListener fullscreenTaskListener, Optional<FullscreenUnfoldController> fullscreenUnfoldTransitionController, - Optional<Optional<FreeformTaskListener>> freeformTaskListenerOptional, + Optional<FreeformTaskListener> freeformTaskListenerOptional, Optional<RecentTasksController> recentTasks, Transitions transitions, StartingWindowController startingWindow, @@ -90,7 +90,7 @@ public class ShellInitImpl { mFullscreenTaskListener = fullscreenTaskListener; mPipTouchHandlerOptional = pipTouchHandlerOptional; mFullscreenUnfoldController = fullscreenUnfoldTransitionController; - mFreeformTaskListenerOptional = freeformTaskListenerOptional.flatMap(f -> f); + mFreeformTaskListenerOptional = freeformTaskListenerOptional; mRecentTasks = recentTasks; mTransitions = transitions; mMainExecutor = mainExecutor; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java new file mode 100644 index 000000000000..806f795d1015 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * This is a qualifier that Shell uses to workaround an issue with providing nullable optionals + * which are by default unbound. + * + * For example, ideally we would have this scenario: + * BaseModule: + * @BindsOptionalOf + * abstract Optional<Interface> optionalInterface(); + * + * SpecializedModule: + * @Provides + * static Interface providesInterface() { + * return new InterfaceImpl(); + * } + * + * However, if the interface is supposed to be provided dynamically, then Dagger is not able to bind + * the optional interface to a null instance, and @BindsOptionalOf does not support @Nullable + * instances of the interface provided by the specialized module. + * + * For example, this does not work: + * BaseModule: + * @BindsOptionalOf + * abstract Optional<Interface> optionalInterface(); + * + * SpecializedModule: + * @Provides + * static Interface providesInterface() { + * if (systemSupportsInterfaceFeature) { + * return new InterfaceImpl(); + * } else { + * return null; + * } + * } + * + * To workaround this, we can instead upstream the check (assuming it can be upstreamed into the + * base module), and then always provide a non-null instance in the specialized module. + * + * For example: + * BaseModule: + * @BindsOptionalOf + * @DynamicOverride + * abstract Interface dynamicInterface(); + * + * @Provides + * static Optional<Interface> providesOptionalInterface( + * @DynamicOverride Optional<Interface> interface) { + * if (systemSupportsInterfaceFeature) { + * return interface; + * } + * return Optional.empty(); + * } + * + * SpecializedModule: + * @Provides + * @DynamicOverride + * static Interface providesInterface() { + * return new InterfaceImpl(); + * } + * + * This is also useful in cases where there needs to be a default implementation in the base module + * which is also overridable in the specialized module. This isn't generally recommended, but + * due to the nature of Shell modules being referenced from a number of various projects, this + * can be useful for *required* components that + * 1) clearly identifies which are intended for overriding in the base module, and + * 2) allows us to declare a default implementation in the base module, without having to force + * every SysUI impl to explicitly provide it (if a large number of them share the default impl) + * + * For example, this uses the same setup as above, but the interface provided (if bound) is used + * otherwise the default is created: + * @BindsOptionalOf + * @DynamicOverride + * abstract Interface dynamicInterface(); + * + * @Provides + * static Optional<Interface> providesOptionalInterface( + * @DynamicOverride Optional<Interface> overrideInterfaceImpl) { + * if (overrideInterfaceImpl.isPresent()) { + * return overrideInterfaceImpl.get(); + * } + * return new DefaultImpl(); + * } + * + * SpecializedModule: + * @Provides + * @DynamicOverride + * static Interface providesInterface() { + * return new SuperSpecialImpl(); + * } + */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface DynamicOverride {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java index 6997d60c75f1..15bfeb297b41 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java @@ -16,25 +16,16 @@ package com.android.wm.shell.dagger; -import android.animation.AnimationHandler; -import android.content.Context; import android.view.IWindowManager; -import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.annotations.ChoreographerSfVsync; import com.android.wm.shell.common.annotations.ShellMainThread; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; import com.android.wm.shell.startingsurface.tv.TvStartingWindowTypeAlgorithm; -import com.android.wm.shell.transition.Transitions; import dagger.Module; import dagger.Provides; @@ -51,42 +42,12 @@ import dagger.Provides; public class TvWMShellModule { // - // Internal common - Components used internally by multiple shell features - // - - @WMSingleton - @Provides - static DisplayImeController provideDisplayImeController(IWindowManager wmService, - DisplayController displayController, DisplayInsetsController displayInsetsController, - @ShellMainThread ShellExecutor mainExecutor, TransactionPool transactionPool) { - return new DisplayImeController(wmService, displayController, displayInsetsController, - mainExecutor, transactionPool); - } - - // - // Split/multiwindow - // - - @WMSingleton - @Provides - static LegacySplitScreenController provideSplitScreen(Context context, - DisplayController displayController, SystemWindows systemWindows, - DisplayImeController displayImeController, TransactionPool transactionPool, - ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, - TaskStackListenerImpl taskStackListener, Transitions transitions, - @ShellMainThread ShellExecutor mainExecutor, - @ChoreographerSfVsync AnimationHandler sfVsyncAnimationHandler) { - return new LegacySplitScreenController(context, displayController, systemWindows, - displayImeController, transactionPool, shellTaskOrganizer, syncQueue, - taskStackListener, transitions, mainExecutor, sfVsyncAnimationHandler); - } - - // // Starting Windows (Splash Screen) // @WMSingleton @Provides + @DynamicOverride static StartingWindowTypeAlgorithm provideStartingWindowTypeAlgorithm() { return new TvStartingWindowTypeAlgorithm(); }; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index ac2e448eab58..d239e56bfd69 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -16,16 +16,16 @@ package com.android.wm.shell.dagger; +import static com.android.wm.shell.onehanded.OneHandedController.SUPPORT_ONE_HANDED_MODE; + import android.app.ActivityTaskManager; import android.content.Context; -import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.os.Handler; +import android.os.SystemProperties; import android.view.IWindowManager; -import android.view.WindowManager; import com.android.internal.logging.UiEventLogger; -import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; @@ -74,26 +74,23 @@ import com.android.wm.shell.pip.phone.PipAppOpsListener; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.sizecompatui.SizeCompatUI; import com.android.wm.shell.sizecompatui.SizeCompatUIController; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.splitscreen.StageTaskUnfoldController; import com.android.wm.shell.startingsurface.StartingSurface; import com.android.wm.shell.startingsurface.StartingWindowController; import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; +import com.android.wm.shell.startingsurface.phone.PhoneStartingWindowTypeAlgorithm; import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper; import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelperController; import com.android.wm.shell.transition.ShellTransitions; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; -import com.android.wm.shell.unfold.UnfoldBackgroundController; import java.util.Optional; -import javax.inject.Provider; - import dagger.BindsOptionalOf; -import dagger.Lazy; import dagger.Module; import dagger.Provides; @@ -128,6 +125,28 @@ public abstract class WMShellBaseModule { return new DisplayInsetsController(wmService, displayController, mainExecutor); } + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract DisplayImeController optionalDisplayImeController(); + + @WMSingleton + @Provides + static DisplayImeController provideDisplayImeController( + @DynamicOverride Optional<DisplayImeController> overrideDisplayImeController, + IWindowManager wmService, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + @ShellMainThread ShellExecutor mainExecutor, + TransactionPool transactionPool + ) { + if (overrideDisplayImeController.isPresent()) { + return overrideDisplayImeController.get(); + } + return new DisplayImeController(wmService, displayController, displayInsetsController, + mainExecutor, transactionPool); + } + @WMSingleton @Provides static DisplayLayout provideDisplayLayout() { @@ -153,11 +172,18 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static SizeCompatUI provideSizeCompatUI(SizeCompatUIController sizeCompatUIController) { + return sizeCompatUIController.asSizeCompatUI(); + } + + @WMSingleton + @Provides static SizeCompatUIController provideSizeCompatUIController(Context context, DisplayController displayController, DisplayInsetsController displayInsetsController, - DisplayImeController imeController, SyncTransactionQueue syncQueue) { + DisplayImeController imeController, SyncTransactionQueue syncQueue, + @ShellMainThread ShellExecutor mainExecutor) { return new SizeCompatUIController(context, displayController, displayInsetsController, - imeController, syncQueue); + imeController, syncQueue, mainExecutor); } @WMSingleton @@ -202,7 +228,7 @@ public abstract class WMShellBaseModule { } // - // Bubbles + // Bubbles (optional feature) // @WMSingleton @@ -211,27 +237,8 @@ public abstract class WMShellBaseModule { return bubbleController.map((controller) -> controller.asBubbles()); } - // Note: Handler needed for LauncherApps.register - @WMSingleton - @Provides - static Optional<BubbleController> provideBubbleController(Context context, - FloatingContentCoordinator floatingContentCoordinator, - IStatusBarService statusBarService, - WindowManager windowManager, - WindowManagerShellWrapper windowManagerShellWrapper, - LauncherApps launcherApps, - TaskStackListenerImpl taskStackListener, - UiEventLogger uiEventLogger, - ShellTaskOrganizer organizer, - DisplayController displayController, - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler, - SyncTransactionQueue syncQueue) { - return Optional.of(BubbleController.create(context, null /* synchronizer */, - floatingContentCoordinator, statusBarService, windowManager, - windowManagerShellWrapper, launcherApps, taskStackListener, - uiEventLogger, organizer, displayController, mainExecutor, mainHandler, syncQueue)); - } + @BindsOptionalOf + abstract BubbleController optionalBubblesController(); // // Fullscreen @@ -252,59 +259,45 @@ public abstract class WMShellBaseModule { // Unfold transition // - @WMSingleton - @Provides - static Optional<FullscreenUnfoldController> provideFullscreenUnfoldController( - Context context, - Optional<ShellUnfoldProgressProvider> progressProvider, - Lazy<UnfoldBackgroundController> unfoldBackgroundController, - DisplayInsetsController displayInsetsController, - @ShellMainThread ShellExecutor mainExecutor - ) { - return progressProvider.map(shellUnfoldTransitionProgressProvider -> - new FullscreenUnfoldController(context, mainExecutor, - unfoldBackgroundController.get(), shellUnfoldTransitionProgressProvider, - displayInsetsController)); - } + @BindsOptionalOf + abstract ShellUnfoldProgressProvider optionalShellUnfoldProgressProvider(); - @Provides - static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController( - Optional<ShellUnfoldProgressProvider> progressProvider, - Context context, - TransactionPool transactionPool, - Lazy<UnfoldBackgroundController> unfoldBackgroundController, - DisplayInsetsController displayInsetsController, - @ShellMainThread ShellExecutor mainExecutor - ) { - return progressProvider.map(shellUnfoldTransitionProgressProvider -> - new StageTaskUnfoldController( - context, - transactionPool, - shellUnfoldTransitionProgressProvider, - displayInsetsController, - unfoldBackgroundController.get(), - mainExecutor - )); - } + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract FullscreenUnfoldController optionalFullscreenUnfoldController(); @WMSingleton @Provides - static UnfoldBackgroundController provideUnfoldBackgroundController( - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - Context context - ) { - return new UnfoldBackgroundController( - context, - rootTaskDisplayAreaOrganizer - ); + static Optional<FullscreenUnfoldController> provideFullscreenUnfoldController( + @DynamicOverride Optional<FullscreenUnfoldController> fullscreenUnfoldController, + Optional<ShellUnfoldProgressProvider> progressProvider) { + if (progressProvider.isPresent() + && progressProvider.get() != ShellUnfoldProgressProvider.NO_PROVIDER) { + return fullscreenUnfoldController; + } + return Optional.empty(); } // // Freeform (optional feature) // + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} @BindsOptionalOf - abstract Optional<FreeformTaskListener> optionalFreeformTaskListener(); + @DynamicOverride + abstract FreeformTaskListener optionalFreeformTaskListener(); + + @WMSingleton + @Provides + static Optional<FreeformTaskListener> provideFreeformTaskListener( + @DynamicOverride Optional<FreeformTaskListener> freeformTaskListener, + Context context) { + if (FreeformTaskListener.isFreeformEnabled(context)) { + return freeformTaskListener; + } + return Optional.empty(); + } // // Hide display cutout @@ -335,20 +328,22 @@ public abstract class WMShellBaseModule { return oneHandedController.map((controller) -> controller.asOneHanded()); } - // Needs the shell main handler for ContentObserver callbacks + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract OneHandedController optionalOneHandedController(); + @WMSingleton @Provides - static Optional<OneHandedController> provideOneHandedController(Context context, - WindowManager windowManager, DisplayController displayController, - DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, - UiEventLogger uiEventLogger, - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler) { - return Optional.ofNullable(OneHandedController.create(context, windowManager, - displayController, displayLayout, taskStackListener, uiEventLogger, mainExecutor, - mainHandler)); + static Optional<OneHandedController> providesOneHandedController( + @DynamicOverride Optional<OneHandedController> oneHandedController) { + if (!SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) { + return oneHandedController; + } + return Optional.empty(); } + // // Task to Surface communication // @@ -366,15 +361,6 @@ public abstract class WMShellBaseModule { return Optional.ofNullable(new TaskSurfaceHelperController(taskOrganizer, mainExecutor)); } - @WMSingleton - @Provides - static Optional<DisplayAreaHelper> provideDisplayAreaHelper( - @ShellMainThread ShellExecutor mainExecutor, - RootDisplayAreaOrganizer rootDisplayAreaOrganizer) { - return Optional.ofNullable(new DisplayAreaHelperController(mainExecutor, - rootDisplayAreaOrganizer)); - } - // // Pip (optional feature) // @@ -460,7 +446,7 @@ public abstract class WMShellBaseModule { } // - // Split/multiwindow + // Display areas // @WMSingleton @@ -479,31 +465,38 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static Optional<DisplayAreaHelper> provideDisplayAreaHelper( + @ShellMainThread ShellExecutor mainExecutor, + RootDisplayAreaOrganizer rootDisplayAreaOrganizer) { + return Optional.of(new DisplayAreaHelperController(mainExecutor, + rootDisplayAreaOrganizer)); + } + + // + // Splitscreen (optional feature) + // + + @WMSingleton + @Provides static Optional<SplitScreen> provideSplitScreen( Optional<SplitScreenController> splitScreenController) { return splitScreenController.map((controller) -> controller.asSplitScreen()); } + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract SplitScreenController optionalSplitScreenController(); + @WMSingleton @Provides - static Optional<SplitScreenController> provideSplitScreenController( - ShellTaskOrganizer shellTaskOrganizer, - SyncTransactionQueue syncQueue, Context context, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - @ShellMainThread ShellExecutor mainExecutor, - DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController, Transitions transitions, - TransactionPool transactionPool, IconProvider iconProvider, - Optional<RecentTasksController> recentTasks, - Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) { + static Optional<SplitScreenController> providesSplitScreenController( + @DynamicOverride Optional<SplitScreenController> splitscreenController, + Context context) { if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) { - return Optional.of(new SplitScreenController(shellTaskOrganizer, syncQueue, context, - rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController, - displayInsetsController, transitions, transactionPool, iconProvider, - recentTasks, stageTaskUnfoldControllerProvider)); - } else { - return Optional.empty(); + return splitscreenController; } + return Optional.empty(); } // Legacy split (optional feature) @@ -529,7 +522,9 @@ public abstract class WMShellBaseModule { @BindsOptionalOf abstract AppPairsController optionalAppPairs(); + // // Starting window + // @WMSingleton @Provides @@ -548,6 +543,23 @@ public abstract class WMShellBaseModule { startingWindowTypeAlgorithm, iconProvider, pool); } + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract StartingWindowTypeAlgorithm optionalStartingWindowTypeAlgorithm(); + + @WMSingleton + @Provides + static StartingWindowTypeAlgorithm provideStartingWindowTypeAlgorithm( + @DynamicOverride Optional<StartingWindowTypeAlgorithm> startingWindowTypeAlgorithm + ) { + if (startingWindowTypeAlgorithm.isPresent()) { + return startingWindowTypeAlgorithm.get(); + } + // Default to phone starting window type + return new PhoneStartingWindowTypeAlgorithm(); + } + // // Task view factory // @@ -591,7 +603,7 @@ public abstract class WMShellBaseModule { Optional<PipTouchHandler> pipTouchHandlerOptional, FullscreenTaskListener fullscreenTaskListener, Optional<FullscreenUnfoldController> appUnfoldTransitionController, - Optional<Optional<FreeformTaskListener>> freeformTaskListener, + Optional<FreeformTaskListener> freeformTaskListener, Optional<RecentTasksController> recentTasksOptional, Transitions transitions, StartingWindowController startingWindow, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index ec701470354c..46c7b508d6e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -18,15 +18,22 @@ package com.android.wm.shell.dagger; import android.animation.AnimationHandler; import android.content.Context; +import android.content.pm.LauncherApps; import android.os.Handler; -import android.view.IWindowManager; +import android.view.WindowManager; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.statusbar.IStatusBarService; +import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.apppairs.AppPairsController; +import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -36,6 +43,7 @@ import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ChoreographerSfVsync; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.fullscreen.FullscreenUnfoldController; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; @@ -55,13 +63,18 @@ import com.android.wm.shell.pip.phone.PipAppOpsListener; import com.android.wm.shell.pip.phone.PipController; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.pip.phone.PipTouchHandler; +import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; -import com.android.wm.shell.startingsurface.phone.PhoneStartingWindowTypeAlgorithm; +import com.android.wm.shell.splitscreen.StageTaskUnfoldController; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.UnfoldBackgroundController; import java.util.Optional; +import javax.inject.Provider; + +import dagger.Lazy; import dagger.Module; import dagger.Provides; @@ -77,17 +90,29 @@ import dagger.Provides; public class WMShellModule { // - // Internal common - Components used internally by multiple shell features + // Bubbles // + // Note: Handler needed for LauncherApps.register @WMSingleton @Provides - static DisplayImeController provideDisplayImeController(IWindowManager wmService, - DisplayController displayController, DisplayInsetsController displayInsetsController, + static BubbleController provideBubbleController(Context context, + FloatingContentCoordinator floatingContentCoordinator, + IStatusBarService statusBarService, + WindowManager windowManager, + WindowManagerShellWrapper windowManagerShellWrapper, + LauncherApps launcherApps, + TaskStackListenerImpl taskStackListener, + UiEventLogger uiEventLogger, + ShellTaskOrganizer organizer, + DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor, - TransactionPool transactionPool) { - return new DisplayImeController(wmService, displayController, displayInsetsController, - mainExecutor, transactionPool); + @ShellMainThread Handler mainHandler, + SyncTransactionQueue syncQueue) { + return BubbleController.create(context, null /* synchronizer */, + floatingContentCoordinator, statusBarService, windowManager, + windowManagerShellWrapper, launcherApps, taskStackListener, + uiEventLogger, organizer, displayController, mainExecutor, mainHandler, syncQueue); } // @@ -96,18 +121,57 @@ public class WMShellModule { @WMSingleton @Provides - static Optional<FreeformTaskListener> provideFreeformTaskListener( - Context context, + @DynamicOverride + static FreeformTaskListener provideFreeformTaskListener( SyncTransactionQueue syncQueue) { - return Optional.ofNullable(FreeformTaskListener.create(context, syncQueue)); + return new FreeformTaskListener(syncQueue); + } + + // + // One handed mode + // + + + // Needs the shell main handler for ContentObserver callbacks + @WMSingleton + @Provides + @DynamicOverride + static OneHandedController provideOneHandedController(Context context, + WindowManager windowManager, DisplayController displayController, + DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, + UiEventLogger uiEventLogger, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { + return OneHandedController.create(context, windowManager, + displayController, displayLayout, taskStackListener, uiEventLogger, mainExecutor, + mainHandler); } // - // Split/multiwindow + // Splitscreen // @WMSingleton @Provides + @DynamicOverride + static SplitScreenController provideSplitScreenController( + ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, Context context, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + @ShellMainThread ShellExecutor mainExecutor, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, Transitions transitions, + TransactionPool transactionPool, IconProvider iconProvider, + Optional<RecentTasksController> recentTasks, + Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) { + return new SplitScreenController(shellTaskOrganizer, syncQueue, context, + rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController, + displayInsetsController, transitions, transactionPool, iconProvider, + recentTasks, stageTaskUnfoldControllerProvider); + } + + @WMSingleton + @Provides static LegacySplitScreenController provideLegacySplitScreen(Context context, DisplayController displayController, SystemWindows systemWindows, DisplayImeController displayImeController, TransactionPool transactionPool, @@ -258,12 +322,53 @@ public class WMShellModule { } // - // Starting Windows (Splash Screen) + // Unfold transition // @WMSingleton @Provides - static StartingWindowTypeAlgorithm provideStartingWindowTypeAlgorithm() { - return new PhoneStartingWindowTypeAlgorithm(); + @DynamicOverride + static FullscreenUnfoldController provideFullscreenUnfoldController( + Context context, + Optional<ShellUnfoldProgressProvider> progressProvider, + Lazy<UnfoldBackgroundController> unfoldBackgroundController, + DisplayInsetsController displayInsetsController, + @ShellMainThread ShellExecutor mainExecutor + ) { + return new FullscreenUnfoldController(context, mainExecutor, + unfoldBackgroundController.get(), progressProvider.get(), + displayInsetsController); + } + + @Provides + static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController( + Optional<ShellUnfoldProgressProvider> progressProvider, + Context context, + TransactionPool transactionPool, + Lazy<UnfoldBackgroundController> unfoldBackgroundController, + DisplayInsetsController displayInsetsController, + @ShellMainThread ShellExecutor mainExecutor + ) { + return progressProvider.map(shellUnfoldTransitionProgressProvider -> + new StageTaskUnfoldController( + context, + transactionPool, + shellUnfoldTransitionProgressProvider, + displayInsetsController, + unfoldBackgroundController.get(), + mainExecutor + )); + } + + @WMSingleton + @Provides + static UnfoldBackgroundController provideUnfoldBackgroundController( + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Context context + ) { + return new UnfoldBackgroundController( + context, + rootTaskDisplayAreaOrganizer + ); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 8a8d7c68d9f6..5c8e7d03eb01 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -141,16 +141,4 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { || Settings.Global.getInt(context.getContentResolver(), DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0; } - - /** - * Creates {@link FreeformTaskListener} if freeform is enabled. - */ - public static FreeformTaskListener create(Context context, - SyncTransactionQueue syncQueue) { - if (!isFreeformEnabled(context)) { - return null; - } - - return new FreeformTaskListener(syncQueue); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java index 80ab166d0649..67e487de0993 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java @@ -172,6 +172,14 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays }; mWindowManager = new DividerWindowManager(mSystemWindows); + + // No need to listen to display window container or create root tasks if the device is not + // using legacy split screen. + if (!context.getResources().getBoolean(com.android.internal.R.bool.config_useLegacySplit)) { + return; + } + + mDisplayController.addDisplayWindowListener(this); // Don't initialize the divider or anything until we get the default display. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index 900743712227..e0686146e821 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -46,7 +46,6 @@ import android.view.accessibility.AccessibilityManager; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.logging.UiEventLogger; @@ -76,7 +75,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, private static final int OVERLAY_ENABLED_DELAY_MS = 250; private static final int DISPLAY_AREA_READY_RETRY_MS = 10; - static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode"; + public static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode"; private volatile boolean mIsOneHandedEnabled; private volatile boolean mIsSwipeToNotificationEnabled; @@ -198,16 +197,10 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, /** * Creates {@link OneHandedController}, returns {@code null} if the feature is not supported. */ - @Nullable public static OneHandedController create( Context context, WindowManager windowManager, DisplayController displayController, DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, ShellExecutor mainExecutor, Handler mainHandler) { - if (!SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) { - Slog.w(TAG, "Device doesn't support OneHanded feature"); - return null; - } - OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil(); OneHandedAccessibilityUtil accessibilityUtil = new OneHandedAccessibilityUtil(context); OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 6ce43a041299..7cfba148a907 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -491,25 +491,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) { // Calculate the snap fraction of the current stack along the old movement bounds final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); - final float snapFraction = pipSnapAlgorithm.getSnapFraction(mPipBoundsState.getBounds(), - mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds()), + final Rect postChangeStackBounds = new Rect(mPipBoundsState.getBounds()); + final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeStackBounds, + mPipBoundsAlgorithm.getMovementBounds(postChangeStackBounds), mPipBoundsState.getStashedState()); updateDisplayLayout.run(); - final Rect postChangeStackBounds; - if (mPipBoundsState.getBounds() != null - && (mPipBoundsState.getBounds().width() > mPipBoundsState.getMaxSize().x - || mPipBoundsState.getBounds().height() > mPipBoundsState.getMaxSize().y)) { - postChangeStackBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x, - mPipBoundsState.getMaxSize().y); - } else if (mPipBoundsState.getBounds() != null - && (mPipBoundsState.getBounds().width() < mPipBoundsState.getMinSize().x - || mPipBoundsState.getBounds().height() < mPipBoundsState.getMinSize().y)) { - postChangeStackBounds = new Rect(0, 0, mPipBoundsState.getMinSize().x, - mPipBoundsState.getMinSize().y); - } else { - postChangeStackBounds = new Rect(mPipBoundsState.getBounds()); - } // Calculate the stack bounds in the new orientation based on same fraction along the // rotated movement bounds. @@ -521,7 +508,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.getDisplayBounds(), mPipBoundsState.getDisplayLayout().stableInsets()); - mTouchHandler.getMotionHelper().animateResizedBounds(postChangeStackBounds); + mTouchHandler.getMotionHelper().movePip(postChangeStackBounds); } else { updateDisplayLayout.run(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index c634b7f220b0..96fd59f0c911 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -69,7 +69,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, private static final int UNSTASH_DURATION = 250; private static final int LEAVE_PIP_DURATION = 300; private static final int SHIFT_DURATION = 300; - private static final int ANIMATE_PIP_RESIZE_ANIMATION = 250; /** Friction to use for PIP when it moves via physics fling animations. */ private static final float DEFAULT_FRICTION = 1.9f; @@ -549,14 +548,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, } /** - * Animates the PiP from an old bound to a new bound. This is mostly used when display - * has changed and PiP bounds needs to be changed. - */ - void animateResizedBounds(Rect newBounds) { - resizeAndAnimatePipUnchecked(newBounds, ANIMATE_PIP_RESIZE_ANIMATION); - } - - /** * Animates the PiP to offset it from the IME or shelf. */ void animateToOffset(Rect originalBounds, int offset) { diff --git a/core/java/android/app/ForegroundServiceDidNotStartInTimeException.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUI.java index 364291e69ad6..a703114194a0 100644 --- a/core/java/android/app/ForegroundServiceDidNotStartInTimeException.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUI.java @@ -14,20 +14,19 @@ * limitations under the License. */ -package android.app; +package com.android.wm.shell.sizecompatui; + +import com.android.wm.shell.common.annotations.ExternalThread; /** - * Exception used to crash an app process when it didn't call {@link Service#startForeground} - * in time after the service was started with - * {@link android.content.Context#startForegroundService}. - * - * @hide + * Interface to engage size compat UI. */ -public class ForegroundServiceDidNotStartInTimeException extends RemoteServiceException { - /** The type ID passed to {@link IApplicationThread#scheduleCrash}. */ - public static final int TYPE_ID = 1; - - public ForegroundServiceDidNotStartInTimeException(String msg) { - super(msg); - } +@ExternalThread +public interface SizeCompatUI { + /** + * Called when the keyguard occluded state changes. Removes all size compat UIs if the + * keyguard is now occluded. + * @param occluded indicates if the keyguard is now occluded. + */ + void onKeyguardOccludedChanged(boolean occluded); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java index 04d974a09cd7..e06070ab12e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java @@ -35,13 +35,16 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; 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.common.annotations.ExternalThread; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Predicate; /** * Controls to show/update restart-activity buttons on Tasks based on whether the foreground @@ -78,26 +81,37 @@ public class SizeCompatUIController implements OnDisplaysChangedListener, private final DisplayInsetsController mDisplayInsetsController; private final DisplayImeController mImeController; private final SyncTransactionQueue mSyncQueue; + private final ShellExecutor mMainExecutor; + private final SizeCompatUIImpl mImpl = new SizeCompatUIImpl(); private SizeCompatUICallback mCallback; /** Only show once automatically in the process life. */ private boolean mHasShownHint; + /** Indicates if the keyguard is currently occluded, in which case size compat UIs shouldn't + * be shown. */ + private boolean mKeyguardOccluded; public SizeCompatUIController(Context context, DisplayController displayController, DisplayInsetsController displayInsetsController, DisplayImeController imeController, - SyncTransactionQueue syncQueue) { + SyncTransactionQueue syncQueue, + ShellExecutor mainExecutor) { mContext = context; mDisplayController = displayController; mDisplayInsetsController = displayInsetsController; mImeController = imeController; mSyncQueue = syncQueue; + mMainExecutor = mainExecutor; mDisplayController.addDisplayWindowListener(this); mImeController.addPositionProcessor(this); } + public SizeCompatUI asSizeCompatUI() { + return mImpl; + } + /** Sets the callback for UI interactions. */ public void setSizeCompatUICallback(SizeCompatUICallback callback) { mCallback = callback; @@ -106,6 +120,7 @@ public class SizeCompatUIController implements OnDisplaysChangedListener, /** * Called when the Task info changed. Creates and updates the size compat UI if there is an * activity in size compat, or removes the UI if there is no size compat activity. + * * @param displayId display the task and activity are in. * @param taskId task the activity is in. * @param taskConfig task config to place the size compat UI with. @@ -180,7 +195,19 @@ public class SizeCompatUIController implements OnDisplaysChangedListener, } // Hide the size compat UIs when input method is showing. - forAllLayoutsOnDisplay(displayId, layout -> layout.updateImeVisibility(isShowing)); + forAllLayoutsOnDisplay(displayId, + layout -> layout.updateVisibility(showOnDisplay(displayId))); + } + + @VisibleForTesting + void onKeyguardOccludedChanged(boolean occluded) { + mKeyguardOccluded = occluded; + // Hide the size compat UIs when keyguard is occluded. + forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId()))); + } + + private boolean showOnDisplay(int displayId) { + return !mKeyguardOccluded && !isImeShowingOnDisplay(displayId); } private boolean isImeShowingOnDisplay(int displayId) { @@ -198,7 +225,7 @@ public class SizeCompatUIController implements OnDisplaysChangedListener, final SizeCompatUILayout layout = createLayout(context, displayId, taskId, taskConfig, taskListener); mActiveLayouts.put(taskId, layout); - layout.createSizeCompatButton(isImeShowingOnDisplay(displayId)); + layout.createSizeCompatButton(showOnDisplay(displayId)); } @VisibleForTesting @@ -218,8 +245,7 @@ public class SizeCompatUIController implements OnDisplaysChangedListener, if (layout == null) { return; } - layout.updateSizeCompatInfo(taskConfig, taskListener, - isImeShowingOnDisplay(layout.getDisplayId())); + layout.updateSizeCompatInfo(taskConfig, taskListener, showOnDisplay(layout.getDisplayId())); } private void removeLayout(int taskId) { @@ -250,15 +276,37 @@ public class SizeCompatUIController implements OnDisplaysChangedListener, } private void forAllLayoutsOnDisplay(int displayId, Consumer<SizeCompatUILayout> callback) { + forAllLayouts(layout -> layout.getDisplayId() == displayId, callback); + } + + private void forAllLayouts(Consumer<SizeCompatUILayout> callback) { + forAllLayouts(layout -> true, callback); + } + + private void forAllLayouts(Predicate<SizeCompatUILayout> condition, + Consumer<SizeCompatUILayout> callback) { for (int i = 0; i < mActiveLayouts.size(); i++) { final int taskId = mActiveLayouts.keyAt(i); final SizeCompatUILayout layout = mActiveLayouts.get(taskId); - if (layout != null && layout.getDisplayId() == displayId) { + if (layout != null && condition.test(layout)) { callback.accept(layout); } } } + /** + * The interface for calls from outside the Shell, within the host process. + */ + @ExternalThread + private class SizeCompatUIImpl implements SizeCompatUI { + @Override + public void onKeyguardOccludedChanged(boolean occluded) { + mMainExecutor.execute(() -> { + SizeCompatUIController.this.onKeyguardOccludedChanged(occluded); + }); + } + } + /** An implementation of {@link OnInsetsChangedListener} for a given display id. */ private class PerDisplayOnInsetsChangedListener implements OnInsetsChangedListener { final int mDisplayId; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java index 1a2c94fd4747..c35b89af6c1b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java @@ -103,9 +103,9 @@ class SizeCompatUILayout { } /** Creates the activity restart button window. */ - void createSizeCompatButton(boolean isImeShowing) { - if (isImeShowing || mButton != null) { - // When ime is showing, wait until ime is dismiss to create UI. + void createSizeCompatButton(boolean show) { + if (!show || mButton != null) { + // Wait until button should be visible. return; } mButton = mButtonWindowManager.createSizeCompatButton(); @@ -154,7 +154,7 @@ class SizeCompatUILayout { /** Called when size compat info changed. */ void updateSizeCompatInfo(Configuration taskConfig, - ShellTaskOrganizer.TaskListener taskListener, boolean isImeShowing) { + ShellTaskOrganizer.TaskListener taskListener, boolean show) { final Configuration prevTaskConfig = mTaskConfig; final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener; mTaskConfig = taskConfig; @@ -170,7 +170,7 @@ class SizeCompatUILayout { if (mButton == null || prevTaskListener != taskListener) { // TaskListener changed, recreate the button for new surface parent. release(); - createSizeCompatButton(isImeShowing); + createSizeCompatButton(show); return; } @@ -204,16 +204,16 @@ class SizeCompatUILayout { } } - /** Called when IME visibility changed. */ - void updateImeVisibility(boolean isImeShowing) { + /** Called when the visibility of the UI should change. */ + void updateVisibility(boolean show) { if (mButton == null) { - // Button may not be created because ime is previous showing. - createSizeCompatButton(isImeShowing); + // Button may not have been created because it was hidden previously. + createSizeCompatButton(show); return; } // Hide size compat UIs when IME is showing. - final int newVisibility = isImeShowing ? View.GONE : View.VISIBLE; + final int newVisibility = show ? View.VISIBLE : View.GONE; if (mButton.getVisibility() != newVisibility) { mButton.setVisibility(newVisibility); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 7457be2d0871..8af72a89a75f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -130,19 +130,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private StageCoordinator mStageCoordinator; - // TODO(b/205019015): Remove after we clean up downstream modules - public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer, - SyncTransactionQueue syncQueue, Context context, - RootTaskDisplayAreaOrganizer rootTDAOrganizer, - ShellExecutor mainExecutor, DisplayImeController displayImeController, - DisplayInsetsController displayInsetsController, - Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, - Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { - this(shellTaskOrganizer, syncQueue, context, rootTDAOrganizer, mainExecutor, - displayImeController, displayInsetsController, transitions, transactionPool, - iconProvider, Optional.empty(), unfoldControllerProvider); - } - public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, Context context, RootTaskDisplayAreaOrganizer rootTDAOrganizer, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 050d255bfd2d..d4941916850d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -78,7 +78,6 @@ import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; -import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.protolog.common.ProtoLog; @@ -157,10 +156,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private boolean mExitSplitScreenOnHide; private boolean mKeyguardOccluded; - // TODO(b/187041611): remove this flag after totally deprecated legacy split - /** Whether the device is supporting legacy split or not. */ - private boolean mUseLegacySplit; - @SplitScreen.StageType private int mDismissTop = NO_DISMISS; @@ -735,17 +730,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void onStageRootTaskAppeared(StageListenerImpl stageListener) { if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { - mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit); final WindowContainerTransaction wct = new WindowContainerTransaction(); // Make the stages adjacent to each other so they occlude what's behind them. wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); - - // Only sets side stage as launch-adjacent-flag-root when the device is not using legacy - // split to prevent new split behavior confusing users. - if (!mUseLegacySplit) { - wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - } - mTaskOrganizer.applyTransaction(wct); } } @@ -755,11 +742,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); // Deactivate the main stage if it no longer has a root task. mMainStage.deactivate(wct); - - if (!mUseLegacySplit) { - wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - } - mTaskOrganizer.applyTransaction(wct); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 190006ec0d9b..62b8638a2582 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -37,6 +37,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; +import com.android.internal.R; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SurfaceUtils; @@ -102,7 +103,12 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { mSurfaceSession = surfaceSession; mIconProvider = iconProvider; mStageTaskUnfoldController = stageTaskUnfoldController; - taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); + + // No need to create root task if the device is using legacy split screen. + // TODO(b/199236198): Remove this check after totally deprecated legacy split. + if (!context.getResources().getBoolean(R.bool.config_useLegacySplit)) { + taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); + } } int getChildCount() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java index 74e48120bf1a..367676f54aba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java @@ -26,10 +26,16 @@ import java.util.concurrent.Executor; */ public interface ShellUnfoldProgressProvider { + // This is a temporary workaround until we move the progress providers into the Shell or + // refactor the dependencies. TLDR, the base module depends on this provider to determine if the + // FullscreenUnfoldController is available, but this check can't rely on an optional component. + public static final ShellUnfoldProgressProvider NO_PROVIDER = + new ShellUnfoldProgressProvider() {}; + /** * Adds a transition listener */ - void addListener(Executor executor, UnfoldListener listener); + default void addListener(Executor executor, UnfoldListener listener) {} /** * Listener for receiving unfold updates diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index c2f58b8b6266..935f6695538d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -191,7 +191,7 @@ public class PipControllerTest extends ShellTestCase { mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged( displayId, new Configuration()); - verify(mMockPipMotionHelper).animateResizedBounds(any(Rect.class)); + verify(mMockPipMotionHelper).movePip(any(Rect.class)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java index d12cf5c3518a..877b19223bf6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java @@ -26,6 +26,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; @@ -43,6 +44,7 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import org.junit.Before; @@ -72,6 +74,7 @@ public class SizeCompatUIControllerTest extends ShellTestCase { private @Mock DisplayImeController mMockImeController; private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener; private @Mock SyncTransactionQueue mMockSyncQueue; + private @Mock ShellExecutor mMockExecutor; private @Mock SizeCompatUILayout mMockLayout; @Captor @@ -85,7 +88,7 @@ public class SizeCompatUIControllerTest extends ShellTestCase { doReturn(DISPLAY_ID).when(mMockLayout).getDisplayId(); doReturn(TASK_ID).when(mMockLayout).getTaskId(); mController = new SizeCompatUIController(mContext, mMockDisplayController, - mMockDisplayInsetsController, mMockImeController, mMockSyncQueue) { + mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor) { @Override SizeCompatUILayout createLayout(Context context, int displayId, int taskId, Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) { @@ -106,19 +109,17 @@ public class SizeCompatUIControllerTest extends ShellTestCase { final Configuration taskConfig = new Configuration(); // Verify that the restart button is added with non-null size compat info. - mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, - mMockTaskListener); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); verify(mController).createLayout(any(), eq(DISPLAY_ID), eq(TASK_ID), eq(taskConfig), eq(mMockTaskListener)); // Verify that the restart button is updated with non-null new size compat info. final Configuration newTaskConfig = new Configuration(); - mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, newTaskConfig, - mMockTaskListener); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, newTaskConfig, mMockTaskListener); verify(mMockLayout).updateSizeCompatInfo(taskConfig, mMockTaskListener, - false /* isImeShowing */); + true /* show */); // Verify that the restart button is removed with null size compat info. mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, null, mMockTaskListener); @@ -196,15 +197,90 @@ public class SizeCompatUIControllerTest extends ShellTestCase { @Test public void testChangeButtonVisibilityOnImeShowHide() { final Configuration taskConfig = new Configuration(); - mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, - mMockTaskListener); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + // Verify that the restart button is hidden after IME is showing. mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); - verify(mMockLayout).updateImeVisibility(true); + verify(mMockLayout).updateVisibility(false); + + // Verify button remains hidden while IME is showing. + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + verify(mMockLayout).updateSizeCompatInfo(taskConfig, mMockTaskListener, + false /* show */); + + // Verify button is shown after IME is hidden. mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); - verify(mMockLayout).updateImeVisibility(false); + verify(mMockLayout).updateVisibility(true); + } + + @Test + public void testChangeButtonVisibilityOnKeyguardOccludedChanged() { + final Configuration taskConfig = new Configuration(); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + // Verify that the restart button is hidden after keyguard becomes occluded. + mController.onKeyguardOccludedChanged(true); + + verify(mMockLayout).updateVisibility(false); + + // Verify button remains hidden while keyguard is occluded. + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + verify(mMockLayout).updateSizeCompatInfo(taskConfig, mMockTaskListener, + false /* show */); + + // Verify button is shown after keyguard becomes not occluded. + mController.onKeyguardOccludedChanged(false); + + verify(mMockLayout).updateVisibility(true); + } + + @Test + public void testButtonRemainsHiddenOnKeyguardOccludedFalseWhenImeIsShowing() { + final Configuration taskConfig = new Configuration(); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); + mController.onKeyguardOccludedChanged(true); + + verify(mMockLayout, times(2)).updateVisibility(false); + + clearInvocations(mMockLayout); + + // Verify button remains hidden after keyguard becomes not occluded since IME is showing. + mController.onKeyguardOccludedChanged(false); + + verify(mMockLayout).updateVisibility(false); + + // Verify button is shown after IME is not showing. + mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + + verify(mMockLayout).updateVisibility(true); + } + + @Test + public void testButtonRemainsHiddenOnImeHideWhenKeyguardIsOccluded() { + final Configuration taskConfig = new Configuration(); + mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); + mController.onKeyguardOccludedChanged(true); + + verify(mMockLayout, times(2)).updateVisibility(false); + + clearInvocations(mMockLayout); + + // Verify button remains hidden after IME is hidden since keyguard is occluded. + mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + + verify(mMockLayout).updateVisibility(false); + + // Verify button is shown after keyguard becomes not occluded. + mController.onKeyguardOccludedChanged(false); + + verify(mMockLayout).updateVisibility(true); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java index 2ba603c8eda8..eb9305b2e995 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java @@ -96,8 +96,8 @@ public class SizeCompatUILayoutTest extends ShellTestCase { @Test public void testCreateSizeCompatButton() { - // Not create button if IME is showing. - mLayout.createSizeCompatButton(true /* isImeShowing */); + // Not create button if show is false. + mLayout.createSizeCompatButton(false /* show */); verify(mLayout.mButtonWindowManager, never()).createSizeCompatButton(); assertNull(mLayout.mButton); @@ -106,7 +106,7 @@ public class SizeCompatUILayoutTest extends ShellTestCase { // Not create hint popup. mLayout.mShouldShowHint = false; - mLayout.createSizeCompatButton(false /* isImeShowing */); + mLayout.createSizeCompatButton(true /* show */); verify(mLayout.mButtonWindowManager).createSizeCompatButton(); assertNotNull(mLayout.mButton); @@ -116,7 +116,7 @@ public class SizeCompatUILayoutTest extends ShellTestCase { // Create hint popup. mLayout.release(); mLayout.mShouldShowHint = true; - mLayout.createSizeCompatButton(false /* isImeShowing */); + mLayout.createSizeCompatButton(true /* show */); verify(mLayout.mButtonWindowManager, times(2)).createSizeCompatButton(); assertNotNull(mLayout.mButton); @@ -128,7 +128,7 @@ public class SizeCompatUILayoutTest extends ShellTestCase { @Test public void testRelease() { - mLayout.createSizeCompatButton(false /* isImeShowing */); + mLayout.createSizeCompatButton(true /* show */); final SizeCompatUIWindowManager hintWindowManager = mLayout.mHintWindowManager; mLayout.release(); @@ -142,12 +142,11 @@ public class SizeCompatUILayoutTest extends ShellTestCase { @Test public void testUpdateSizeCompatInfo() { - mLayout.createSizeCompatButton(false /* isImeShowing */); + mLayout.createSizeCompatButton(true /* show */); // No diff clearInvocations(mLayout); - mLayout.updateSizeCompatInfo(mTaskConfig, mTaskListener, - false /* isImeShowing */); + mLayout.updateSizeCompatInfo(mTaskConfig, mTaskListener, true /* show */); verify(mLayout, never()).updateButtonSurfacePosition(); verify(mLayout, never()).release(); @@ -158,7 +157,7 @@ public class SizeCompatUILayoutTest extends ShellTestCase { final ShellTaskOrganizer.TaskListener newTaskListener = mock( ShellTaskOrganizer.TaskListener.class); mLayout.updateSizeCompatInfo(mTaskConfig, newTaskListener, - false /* isImeShowing */); + true /* show */); verify(mLayout).release(); verify(mLayout).createSizeCompatButton(anyBoolean()); @@ -168,7 +167,7 @@ public class SizeCompatUILayoutTest extends ShellTestCase { final Configuration newTaskConfiguration = new Configuration(); newTaskConfiguration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000)); mLayout.updateSizeCompatInfo(newTaskConfiguration, newTaskListener, - false /* isImeShowing */); + true /* show */); verify(mLayout).updateButtonSurfacePosition(); verify(mLayout).updateHintSurfacePosition(); @@ -220,24 +219,24 @@ public class SizeCompatUILayoutTest extends ShellTestCase { } @Test - public void testUpdateImeVisibility() { + public void testUpdateVisibility() { // Create button if it is not created. mLayout.mButton = null; - mLayout.updateImeVisibility(false /* isImeShowing */); + mLayout.updateVisibility(true /* show */); - verify(mLayout).createSizeCompatButton(false /* isImeShowing */); + verify(mLayout).createSizeCompatButton(true /* show */); - // Hide button if ime is shown. + // Hide button. clearInvocations(mLayout); doReturn(View.VISIBLE).when(mButton).getVisibility(); - mLayout.updateImeVisibility(true /* isImeShowing */); + mLayout.updateVisibility(false /* show */); verify(mLayout, never()).createSizeCompatButton(anyBoolean()); verify(mButton).setVisibility(View.GONE); - // Show button if ime is not shown. + // Show button. doReturn(View.GONE).when(mButton).getVisibility(); - mLayout.updateImeVisibility(false /* isImeShowing */); + mLayout.updateVisibility(true /* show */); verify(mLayout, never()).createSizeCompatButton(anyBoolean()); verify(mButton).setVisibility(View.VISIBLE); diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 96f127b6a611..77fff0f08d4c 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -191,5 +191,6 @@ public class SecureSettings { Settings.Secure.LOCATION_TIME_ZONE_DETECTION_ENABLED, Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, Settings.Secure.LOCKSCREEN_SHOW_WALLET, + Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 7aeacdc0cf71..9f883960981b 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -150,6 +150,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.POWER_MENU_LOCKED_SHOW_CONTENT, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LOCKSCREEN_SHOW_CONTROLS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LOCKSCREEN_SHOW_WALLET, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.DOZE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.DOZE_ALWAYS_ON, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.DOZE_PICK_UP_GESTURE, BOOLEAN_VALIDATOR); diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml index ef60a248f79a..c4cb89fecccb 100644 --- a/packages/SystemUI/animation/res/values/ids.xml +++ b/packages/SystemUI/animation/res/values/ids.xml @@ -16,4 +16,5 @@ --> <resources> <item type="id" name="launch_animation_running"/> + <item type="id" name="dialog_content_parent" /> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index 413612ff9a76..9aad2783ba2d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -40,6 +40,7 @@ import android.widget.FrameLayout import kotlin.math.roundToInt private const val TAG = "DialogLaunchAnimator" +private val DIALOG_CONTENT_PARENT_ID = R.id.dialog_content_parent /** * A class that allows dialogs to be started in a seamless way from a view that is transforming @@ -86,10 +87,10 @@ class DialogLaunchAnimator( // If the parent of the view we are launching from is the background of some other animated // dialog, then this means the caller intent is to launch a dialog from another dialog. In // this case, we also animate the parent (which is the dialog background). - val dialogContentParent = openedDialogs + val animatedParent = openedDialogs .firstOrNull { it.dialogContentParent == view.parent } - ?.dialogContentParent - val animateFrom = dialogContentParent ?: view + val parentHostDialog = animatedParent?.hostDialog + val animateFrom = animatedParent?.dialogContentParent ?: view // Make sure we don't run the launch animation from the same view twice at the same time. if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) { @@ -100,12 +101,18 @@ class DialogLaunchAnimator( animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) - val launchAnimation = AnimatedDialog( - context, launchAnimator, hostDialogProvider, animateFrom, - onDialogDismissed = { openedDialogs.remove(it) }, originalDialog = dialog, - animateBackgroundBoundsChange) - val hostDialog = launchAnimation.hostDialog - openedDialogs.add(launchAnimation) + val animatedDialog = AnimatedDialog( + context, + launchAnimator, + hostDialogProvider, + animateFrom, + onDialogDismissed = { openedDialogs.remove(it) }, + originalDialog = dialog, + animateBackgroundBoundsChange, + openedDialogs.firstOrNull { it.hostDialog == parentHostDialog } + ) + val hostDialog = animatedDialog.hostDialog + openedDialogs.add(animatedDialog) // If the dialog is dismissed/hidden/shown, then we should actually dismiss/hide/show the // host dialog. @@ -119,15 +126,15 @@ class DialogLaunchAnimator( // If AOD is disabled the screen will directly becomes black and we won't see // the animation anyways. if (reason == DialogListener.DismissReason.DEVICE_LOCKED) { - launchAnimation.exitAnimationDisabled = true + animatedDialog.exitAnimationDisabled = true } hostDialog.dismiss() } override fun onHide() { - if (launchAnimation.ignoreNextCallToHide) { - launchAnimation.ignoreNextCallToHide = false + if (animatedDialog.ignoreNextCallToHide) { + animatedDialog.ignoreNextCallToHide = false return } @@ -138,21 +145,44 @@ class DialogLaunchAnimator( hostDialog.show() // We don't actually want to show the original dialog, so hide it. - launchAnimation.ignoreNextCallToHide = true + animatedDialog.ignoreNextCallToHide = true dialog.hide() } override fun onSizeChanged() { - launchAnimation.onOriginalDialogSizeChanged() + animatedDialog.onOriginalDialogSizeChanged() + } + + override fun prepareForStackDismiss() { + animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss() } }) } - launchAnimation.start() + animatedDialog.start() return hostDialog } /** + * Launch [dialog] from a [parentHostDialog] as returned by [showFromView]. This will allow + * for dismissing the whole stack. + * + * This will return a new host dialog, with the same caveat as [showFromView]. + * + * @see DialogListener.prepareForStackDismiss + */ + fun showFromDialog( + dialog: Dialog, + parentHostDialog: Dialog, + animateBackgroundBoundsChange: Boolean = false + ): Dialog { + val view = parentHostDialog.findViewById<ViewGroup>(DIALOG_CONTENT_PARENT_ID) + ?.getChildAt(0) + ?: throw IllegalStateException("No dialog content parent found in host dialog") + return showFromView(dialog, view, animateBackgroundBoundsChange) + } + + /** * Ensure that all dialogs currently shown won't animate into their touch surface when * dismissed. * @@ -214,6 +244,12 @@ interface DialogListener { /** Called when this dialog show() is called. */ fun onShow() + /** + * Call before dismissing a stack of dialogs (dialogs launched from dialogs), so the topmost + * can animate directly into the original `touchSurface`. + */ + fun prepareForStackDismiss() + /** Called when this dialog size might have changed, e.g. because of configuration changes. */ fun onSizeChanged() } @@ -224,7 +260,7 @@ private class AnimatedDialog( hostDialogProvider: HostDialogProvider, /** The view that triggered the dialog after being tapped. */ - private val touchSurface: View, + var touchSurface: View, /** * A callback that will be called with this [AnimatedDialog] after the dialog was @@ -236,7 +272,10 @@ private class AnimatedDialog( private val originalDialog: Dialog, /** Whether we should animate the dialog background when its bounds change. */ - private val animateBackgroundBoundsChange: Boolean + private val animateBackgroundBoundsChange: Boolean, + + /** Launch animation corresponding to the parent [hostDialog]. */ + private val parentAnimatedDialog: AnimatedDialog? = null ) { /** * The fullscreen dialog to which we will add the content view [originalDialogView] of @@ -253,7 +292,9 @@ private class AnimatedDialog( * the same size as the original dialog window and to which we will set the original dialog * window background. */ - val dialogContentParent = FrameLayout(context) + val dialogContentParent = FrameLayout(context).apply { + id = DIALOG_CONTENT_PARENT_ID + } /** * The background color of [originalDialogView], taking into consideration the [originalDialog] @@ -359,9 +400,7 @@ private class AnimatedDialog( // Make the touch surface invisible and make sure that it stays invisible as long as the // dialog is shown or animating. touchSurface.visibility = View.INVISIBLE - if (touchSurface is LaunchableView) { - touchSurface.setShouldBlockVisibilityChanges(true) - } + (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true) // Add a pre draw listener to (maybe) start the animation once the touch surface is // actually invisible. @@ -576,9 +615,7 @@ private class AnimatedDialog( Log.i(TAG, "Skipping animation of dialog into the touch surface") // Make sure we allow the touch surface to change its visibility again. - if (touchSurface is LaunchableView) { - touchSurface.setShouldBlockVisibilityChanges(false) - } + (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false) // If the view is invisible it's probably because of us, so we make it visible again. if (touchSurface.visibility == View.INVISIBLE) { @@ -598,9 +635,7 @@ private class AnimatedDialog( }, onLaunchAnimationEnd = { // Make sure we allow the touch surface to change its visibility again. - if (touchSurface is LaunchableView) { - touchSurface.setShouldBlockVisibilityChanges(false) - } + (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false) touchSurface.visibility = View.VISIBLE dialogContentParent.visibility = View.INVISIBLE @@ -796,4 +831,18 @@ private class AnimatedDialog( animator.start() } } + + fun prepareForStackDismiss(): View { + if (parentAnimatedDialog == null) { + return touchSurface + } + parentAnimatedDialog.exitAnimationDisabled = true + parentAnimatedDialog.originalDialog.hide() + val view = parentAnimatedDialog.prepareForStackDismiss() + parentAnimatedDialog.originalDialog.dismiss() + // Make the touch surface invisible, so we end up animating to it when we actually + // dismiss the stack + view.visibility = View.INVISIBLE + return view + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 3d4e896178f6..9238b8226bbc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -11,7 +11,6 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver.OnPreDrawListener; import android.widget.FrameLayout; import android.widget.RelativeLayout; @@ -89,7 +88,6 @@ public class KeyguardClockSwitch extends RelativeLayout { private int mClockSwitchYAmount; @VisibleForTesting boolean mChildrenAreLaidOut = false; - private OnPreDrawListener mPreDrawListener; public KeyguardClockSwitch(Context context, AttributeSet attrs) { super(context, attrs); @@ -284,30 +282,21 @@ public class KeyguardClockSwitch extends RelativeLayout { // translate them properly if (mChildrenAreLaidOut) { animateClockChange(clockSize == LARGE); - mDisplayedClockSize = clockSize; - } else if (mPreDrawListener == null) { - mPreDrawListener = () -> { - switchToClock(clockSize); - getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); - mPreDrawListener = null; - return true; - }; - getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); } + + mDisplayedClockSize = clockSize; return true; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); - mChildrenAreLaidOut = true; - } - void onViewDetached() { - if (mPreDrawListener != null) { - getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); - mPreDrawListener = null; + if (mDisplayedClockSize != null && !mChildrenAreLaidOut) { + animateClockChange(mDisplayedClockSize == LARGE); } + + mChildrenAreLaidOut = true; } public Paint getPaint() { @@ -368,5 +357,6 @@ public class KeyguardClockSwitch extends RelativeLayout { pw.println(" mDarkAmount: " + mDarkAmount); pw.println(" mSupportsDarkText: " + mSupportsDarkText); pw.println(" mColorPalette: " + Arrays.toString(mColorPalette)); + pw.println(" mDisplayedClockSize: " + mDisplayedClockSize); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 905495d369a0..c628d4401bb1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -23,6 +23,8 @@ import static com.android.keyguard.KeyguardClockSwitch.LARGE; import android.app.WallpaperManager; import android.content.res.Resources; +import android.database.ContentObserver; +import android.provider.Settings; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; @@ -49,11 +51,13 @@ import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.ViewController; +import com.android.systemui.util.settings.SecureSettings; import java.util.HashSet; import java.util.Locale; import java.util.Set; import java.util.TimeZone; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -72,6 +76,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final BatteryController mBatteryController; private final LockscreenSmartspaceController mSmartspaceController; private final Resources mResources; + private final SecureSettings mSecureSettings; /** * Clock for both small and large sizes @@ -109,6 +114,14 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private SmartspaceTransitionController mSmartspaceTransitionController; private boolean mOnlyClock = false; + private Executor mUiExecutor; + private boolean mCanShowDoubleLineClock = true; + private ContentObserver mDoubleLineClockObserver = new ContentObserver(null) { + @Override + public void onChange(boolean change) { + updateDoubleLineClock(); + } + }; @Inject public KeyguardClockSwitchController( @@ -125,6 +138,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS LockscreenSmartspaceController smartspaceController, KeyguardUnlockAnimationController keyguardUnlockAnimationController, SmartspaceTransitionController smartspaceTransitionController, + SecureSettings secureSettings, + @Main Executor uiExecutor, @Main Resources resources) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; @@ -138,7 +153,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mBypassController = bypassController; mSmartspaceController = smartspaceController; mResources = resources; - + mSecureSettings = secureSettings; + mUiExecutor = uiExecutor; mKeyguardUnlockAnimationController = keyguardUnlockAnimationController; mSmartspaceTransitionController = smartspaceTransitionController; } @@ -223,6 +239,14 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS updateClockLayout(); mSmartspaceTransitionController.setLockscreenSmartspace(mSmartspaceView); } + + mSecureSettings.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK), + false, /* notifyForDescendants */ + mDoubleLineClockObserver + ); + + updateDoubleLineClock(); } int getNotificationIconAreaHeight() { @@ -236,7 +260,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } mColorExtractor.removeOnColorsChangedListener(mColorsListener); mView.setClockPlugin(null, mStatusBarStateController.getState()); - mView.onViewDetached(); + + mSecureSettings.unregisterContentObserver(mDoubleLineClockObserver); } /** @@ -268,6 +293,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS * hidden. */ public void displayClock(@KeyguardClockSwitch.ClockSize int clockSize) { + if (!mCanShowDoubleLineClock && clockSize == KeyguardClockSwitch.LARGE) { + return; + } + boolean appeared = mView.switchToClock(clockSize); if (appeared && clockSize == LARGE) { mLargeClockViewController.animateAppear(); @@ -410,4 +439,13 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private int getCurrentLayoutDirection() { return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()); } + + private void updateDoubleLineClock() { + mCanShowDoubleLineClock = mSecureSettings.getInt( + Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1) != 0; + + if (!mCanShowDoubleLineClock) { + mUiExecutor.execute(() -> displayClock(KeyguardClockSwitch.SMALL)); + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt index 9286175cc2ea..471bac16642b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt @@ -43,8 +43,7 @@ data class KeyguardFingerprintListenModel( val shouldListenForFingerprintAssistant: Boolean, val switchingUser: Boolean, val udfps: Boolean, - val userDoesNotHaveTrust: Boolean, - val userNeedsStrongAuth: Boolean + val userDoesNotHaveTrust: Boolean ) : KeyguardListenModel() { override val modality: Int = TYPE_FACE } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index d27bc675ecb8..e24f07c21076 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2222,11 +2222,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab !(mFingerprintLockedOut && mBouncer && mCredentialAttempted); final boolean isEncryptedOrLockdownForUser = isEncryptedOrLockdown(user); - final boolean userNeedsStrongAuth = userNeedsStrongAuth(); final boolean shouldListenUdfpsState = !isUdfps || (!userCanSkipBouncer && !isEncryptedOrLockdownForUser - && !userNeedsStrongAuth && userDoesNotHaveTrust && !mFingerprintLockedOut); @@ -2257,8 +2255,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab shouldListenForFingerprintAssistant, mSwitchingUser, isUdfps, - userDoesNotHaveTrust, - userNeedsStrongAuth)); + userDoesNotHaveTrust)); } return shouldListen; @@ -2362,7 +2359,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || (DEBUG_FINGERPRINT && model instanceof KeyguardFingerprintListenModel && mFingerprintRunningState != BIOMETRIC_STATE_RUNNING); - if (notYetRunning && model.getListening()) { + final boolean running = + (DEBUG_FACE + && model instanceof KeyguardFaceListenModel + && mFaceRunningState == BIOMETRIC_STATE_RUNNING) + || (DEBUG_FINGERPRINT + && model instanceof KeyguardFingerprintListenModel + && mFingerprintRunningState == BIOMETRIC_STATE_RUNNING); + if (notYetRunning && model.getListening() + || running && !model.getListening()) { mListenModels.add(model); } } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index e96e924d557f..d37dcafde92a 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -31,8 +31,6 @@ import com.android.systemui.dagger.WMComponent; import com.android.systemui.navigationbar.gestural.BackGestureTfClassifierProvider; import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider; import com.android.wm.shell.transition.ShellTransitions; -import com.android.wm.shell.transition.Transitions; -import com.android.wm.shell.recents.RecentTasks; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -122,7 +120,8 @@ public class SystemUIFactory { .setStartingSurface(mWMComponent.getStartingSurface()) .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper()) .setTaskSurfaceHelper(mWMComponent.getTaskSurfaceHelper()) - .setRecentTasks(mWMComponent.getRecentTasks()); + .setRecentTasks(mWMComponent.getRecentTasks()) + .setSizeCompatUI(Optional.of(mWMComponent.getSizeCompatUI())); } else { // TODO: Call on prepareSysUIComponentBuilder but not with real components. Other option // is separating this logic into newly creating SystemUITestsFactory. @@ -140,7 +139,8 @@ public class SystemUIFactory { .setDisplayAreaHelper(Optional.ofNullable(null)) .setStartingSurface(Optional.ofNullable(null)) .setTaskSurfaceHelper(Optional.ofNullable(null)) - .setRecentTasks(Optional.ofNullable(null)); + .setRecentTasks(Optional.ofNullable(null)) + .setSizeCompatUI(Optional.ofNullable(null)); } mSysUIComponent = builder.build(); if (mInitializeComponents) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index a9fb743bff8d..5fdf026b86f3 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -37,6 +37,7 @@ import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.recents.RecentTasks; +import com.android.wm.shell.sizecompatui.SizeCompatUI; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.startingsurface.StartingSurface; import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper; @@ -107,6 +108,9 @@ public interface SysUIComponent { @BindsInstance Builder setRecentTasks(Optional<RecentTasks> r); + @BindsInstance + Builder setSizeCompatUI(Optional<SizeCompatUI> s); + SysUIComponent build(); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java index d8b77426703e..543ba8f9b854 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java @@ -20,13 +20,13 @@ import android.content.Context; import com.android.systemui.SystemUIFactory; import com.android.systemui.tv.TvWMComponent; -import com.android.wm.shell.dagger.TvWMShellModule; -import com.android.wm.shell.dagger.WMShellModule; import com.android.wm.shell.ShellCommandHandler; import com.android.wm.shell.ShellInit; import com.android.wm.shell.TaskViewFactory; import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.dagger.TvWMShellModule; +import com.android.wm.shell.dagger.WMShellModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; @@ -34,6 +34,7 @@ import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.recents.RecentTasks; +import com.android.wm.shell.sizecompatui.SizeCompatUI; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.startingsurface.StartingSurface; import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper; @@ -115,4 +116,7 @@ public interface WMComponent { @WMSingleton Optional<RecentTasks> getRecentTasks(); + + @WMSingleton + SizeCompatUI getSizeCompatUI(); } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index 5c3e07fbaea1..dd94a75c55c6 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -16,6 +16,8 @@ package com.android.systemui.doze; +import static com.android.systemui.doze.DozeLog.REASON_SENSOR_QUICK_PICKUP; +import static com.android.systemui.doze.DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS; import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_DISPLAY; import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN; @@ -98,6 +100,7 @@ public class DozeSensors { private final DozeLog mDozeLog; private final SecureSettings mSecureSettings; private final DevicePostureController mDevicePostureController; + private final AuthController mAuthController; private final boolean mScreenOffUdfpsEnabled; // Sensors @@ -115,6 +118,7 @@ public class DozeSensors { private boolean mListening; private boolean mListeningTouchScreenSensors; private boolean mListeningProxSensors; + private boolean mUdfpsEnrolled; @DevicePostureController.DevicePostureInt private int mDevicePosture; @@ -169,10 +173,11 @@ public class DozeSensors { config.screenOffUdfpsEnabled(KeyguardUpdateMonitor.getCurrentUser()); mDevicePostureController = devicePostureController; mDevicePosture = mDevicePostureController.getDevicePosture(); + mAuthController = authController; - boolean udfpsEnrolled = - authController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()); - boolean alwaysOn = mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT); + mUdfpsEnrolled = + mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()); + mAuthController.addCallback(mAuthControllerCallback); mTriggerSensors = new TriggerSensor[] { new TriggerSensor( mSensorManager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION), @@ -221,7 +226,7 @@ public class DozeSensors { findSensor(config.udfpsLongPressSensorType()), "doze_pulse_on_auth", true /* settingDef */, - udfpsEnrolled && (alwaysOn || mScreenOffUdfpsEnabled), + udfpsLongPressConfigured(), DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS, true /* reports touch coordinates */, true /* touchscreen */, @@ -230,7 +235,8 @@ public class DozeSensors { new PluginSensor( new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY), Settings.Secure.DOZE_WAKE_DISPLAY_GESTURE, - mConfig.wakeScreenGestureAvailable() && alwaysOn, + mConfig.wakeScreenGestureAvailable() + && mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT), DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE, false /* reports touch coordinates */, false /* touchscreen */), @@ -246,8 +252,7 @@ public class DozeSensors { findSensor(config.quickPickupSensorType()), Settings.Secure.DOZE_QUICK_PICKUP_GESTURE, true /* setting default */, - config.quickPickupSensorEnabled(KeyguardUpdateMonitor.getCurrentUser()) - && udfpsEnrolled, + quickPickUpConfigured(), DozeLog.REASON_SENSOR_QUICK_PICKUP, false /* requiresTouchCoordinates */, false /* requiresTouchscreen */, @@ -265,6 +270,16 @@ public class DozeSensors { mDevicePostureController.addCallback(mDevicePostureCallback); } + private boolean udfpsLongPressConfigured() { + return mUdfpsEnrolled + && (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) || mScreenOffUdfpsEnabled); + } + + private boolean quickPickUpConfigured() { + return mUdfpsEnrolled + && mConfig.quickPickupSensorEnabled(KeyguardUpdateMonitor.getCurrentUser()); + } + /** * Unregister all sensors and callbacks. */ @@ -276,6 +291,7 @@ public class DozeSensors { mProximitySensor.pause(); mDevicePostureController.removeCallback(mDevicePostureCallback); + mAuthController.removeCallback(mAuthControllerCallback); } /** @@ -450,6 +466,7 @@ public class DozeSensors { pw.println("mSelectivelyRegisterProxSensors=" + mSelectivelyRegisterProxSensors); pw.println("mListeningProxSensors=" + mListeningProxSensors); pw.println("mScreenOffUdfpsEnabled=" + mScreenOffUdfpsEnabled); + pw.println("mUdfpsEnrolled=" + mUdfpsEnrolled); IndentingPrintWriter idpw = new IndentingPrintWriter(pw); idpw.increaseIndent(); for (TriggerSensor s : mTriggerSensors) { @@ -468,7 +485,7 @@ public class DozeSensors { @VisibleForTesting class TriggerSensor extends TriggerEventListener { @NonNull final Sensor[] mSensors; // index = posture, value = sensor - final boolean mConfigured; + boolean mConfigured; final int mPulseReason; private final String mSetting; private final boolean mReportsTouchCoordinates; @@ -606,8 +623,18 @@ public class DozeSensors { updateListening(); } + /** + * Update configured state. + */ + public void setConfigured(boolean configured) { + if (mConfigured == configured) return; + mConfigured = configured; + updateListening(); + } + public void updateListening() { final Sensor sensor = mSensors[mPosture]; + if (!mConfigured || sensor == null) return; if (mRequested && !mDisabled && (enabledBySetting() || mIgnoresSetting)) { if (!mRegistered) { @@ -791,6 +818,30 @@ public class DozeSensors { } }; + private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() { + @Override + public void onAllAuthenticatorsRegistered() { + updateUdfpsEnrolled(); + } + + @Override + public void onEnrollmentsChanged() { + updateUdfpsEnrolled(); + } + + private void updateUdfpsEnrolled() { + mUdfpsEnrolled = mAuthController.isUdfpsEnrolled( + KeyguardUpdateMonitor.getCurrentUser()); + for (TriggerSensor sensor : mTriggerSensors) { + if (REASON_SENSOR_QUICK_PICKUP == sensor.mPulseReason) { + sensor.setConfigured(quickPickUpConfigured()); + } else if (REASON_SENSOR_UDFPS_LONG_PRESS == sensor.mPulseReason) { + sensor.setConfigured(udfpsLongPressConfigured()); + } + } + } + }; + public interface Callback { /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 821bd5117d18..be9aa0e98876 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -28,6 +28,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.Nullable; + import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -37,10 +39,10 @@ import com.android.systemui.R; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.qs.PseudoGridView; import com.android.systemui.qs.QSUserSwitcherEvent; +import com.android.systemui.qs.user.UserSwitchDialogController; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.UserSwitcherController; -import java.util.function.Consumer; - import javax.inject.Inject; /** @@ -77,7 +79,7 @@ public class UserDetailView extends PseudoGridView { private View mCurrentUserView; private final UiEventLogger mUiEventLogger; private final FalsingManager mFalsingManager; - private Consumer<UserSwitcherController.UserRecord> mClickCallback; + private @Nullable UserSwitchDialogController.DialogShower mDialogShower; @Inject public Adapter(Context context, UserSwitcherController controller, @@ -95,8 +97,17 @@ public class UserDetailView extends PseudoGridView { return createUserDetailItemView(convertView, parent, item); } - public void injectCallback(Consumer<UserSwitcherController.UserRecord> clickCallback) { - mClickCallback = clickCallback; + /** + * If this adapter is inside a dialog, passing a + * {@link UserSwitchDialogController.DialogShower} will help animate to and from the parent + * dialog. This will also allow for dismissing the whole stack of dialogs in a single + * animation. + * + * @param shower + * @see SystemUIDialog#dismissStack() + */ + public void injectDialogShower(UserSwitchDialogController.DialogShower shower) { + mDialogShower = shower; } public UserDetailItemView createUserDetailItemView(View convertView, ViewGroup parent, @@ -172,10 +183,7 @@ public class UserDetailView extends PseudoGridView { } view.setActivated(true); } - onUserListItemClicked(tag); - } - if (mClickCallback != null) { - mClickCallback.accept(tag); + onUserListItemClicked(tag, mDialogShower); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java index 563c4cd628d2..77b9cc14fa6d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java @@ -306,12 +306,8 @@ public class InternetDialog extends SystemUIDialog implements final boolean isWifiScanEnabled = mInternetDialogController.isWifiScanEnabled(); updateWifiToggle(isWifiEnabled, isDeviceLocked); updateConnectedWifi(isWifiEnabled, isDeviceLocked); + updateWifiListAndSeeAll(isWifiEnabled, isDeviceLocked); updateWifiScanNotify(isWifiEnabled, isWifiScanEnabled, isDeviceLocked); - - final int visibility = (isDeviceLocked || !isWifiEnabled || mWifiEntriesCount <= 0) - ? View.GONE : View.VISIBLE; - mWifiRecyclerView.setVisibility(visibility); - mSeeAllLayout.setVisibility(visibility); } private void setOnClickListener() { @@ -414,6 +410,18 @@ public class InternetDialog extends SystemUIDialog implements } @MainThread + private void updateWifiListAndSeeAll(boolean isWifiEnabled, boolean isDeviceLocked) { + if (!isWifiEnabled || isDeviceLocked) { + mWifiRecyclerView.setVisibility(View.GONE); + mSeeAllLayout.setVisibility(View.GONE); + return; + } + mWifiRecyclerView.setVisibility(mWifiEntriesCount > 0 ? View.VISIBLE : View.GONE); + mSeeAllLayout.setVisibility( + (mConnectedWifiEntry != null || mWifiEntriesCount > 0) ? View.VISIBLE : View.GONE); + } + + @MainThread private void updateWifiScanNotify(boolean isWifiEnabled, boolean isWifiScanEnabled, boolean isDeviceLocked) { if (isWifiEnabled || !isWifiScanEnabled || isDeviceLocked) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index bae7996517c5..d74a50e24ed3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -16,7 +16,9 @@ package com.android.systemui.qs.user +import android.app.Dialog import android.content.Context +import android.content.DialogInterface import android.content.Intent import android.provider.Settings import android.view.View @@ -84,12 +86,26 @@ class UserSwitchDialogController @VisibleForTesting constructor( doneButton.setOnClickListener { dismiss() } val adapter = userDetailViewAdapterProvider.get() - adapter.injectCallback { - dismiss() - } adapter.linkToViewGroup(grid) - dialogLaunchAnimator.showFromView(this, view) + val hostDialog = dialogLaunchAnimator.showFromView(this, view) + adapter.injectDialogShower(DialogShowerImpl(hostDialog, dialogLaunchAnimator)) } } + + private class DialogShowerImpl( + private val hostDialog: Dialog, + private val dialogLaunchAnimator: DialogLaunchAnimator + ) : DialogInterface by hostDialog, DialogShower { + override fun showDialog(dialog: Dialog): Dialog { + return dialogLaunchAnimator.showFromDialog( + dialog, + parentHostDialog = hostDialog + ) + } + } + + interface DialogShower : DialogInterface { + fun showDialog(dialog: Dialog): Dialog + } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 1bbd45192bea..4479aa0468b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -3345,6 +3345,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } ipw.decreaseIndent(); ipw.println("}"); + } else if (mPrivateLayout != null) { + mPrivateLayout.dumpSmartReplies(ipw); } }); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index df484dd8ed77..0ffca30ed409 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -27,6 +27,7 @@ import android.os.Build; import android.provider.Settings; import android.util.ArrayMap; import android.util.AttributeSet; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Pair; import android.view.LayoutInflater; @@ -1955,6 +1956,22 @@ public class NotificationContentView extends FrameLayout { pw.println(); } + /** Add any existing SmartReplyView to the dump */ + public void dumpSmartReplies(IndentingPrintWriter pw) { + if (mHeadsUpSmartReplyView != null) { + pw.println("HeadsUp SmartReplyView:"); + pw.increaseIndent(); + mHeadsUpSmartReplyView.dump(pw); + pw.decreaseIndent(); + } + if (mExpandedSmartReplyView != null) { + pw.println("Expanded SmartReplyView:"); + pw.increaseIndent(); + mExpandedSmartReplyView.dump(pw); + pw.decreaseIndent(); + } + } + public RemoteInputView getExpandedRemoteInput() { return mExpandedRemoteInput; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index cf4aaba107cf..1130ec24108a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -218,6 +218,19 @@ public class SystemUIDialog extends AlertDialog implements ListenableDialog, } } + /** + * Dismiss this dialog. If it was launched from another dialog using + * {@link com.android.systemui.animation.DialogLaunchAnimator#showFromView} with a + * non-{@code null} {@code parentHostDialog} parameter, also dismisses the stack of dialogs, + * animating back to the original touchSurface. + */ + public void dismissStack() { + for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) { + listener.prepareForStackDismiss(); + } + dismiss(); + } + @Override public void hide() { super.hide(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java index 4e33529f3c36..85add6c21b72 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java @@ -1,7 +1,8 @@ package com.android.systemui.statusbar.policy; +import static java.lang.Float.NaN; + import android.annotation.ColorInt; -import android.annotation.NonNull; import android.app.Notification; import android.app.PendingIntent; import android.app.RemoteInput; @@ -14,10 +15,12 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.RippleDrawable; +import android.os.SystemClock; import android.text.Layout; import android.text.TextPaint; import android.text.method.TransformationMethod; import android.util.AttributeSet; +import android.util.IndentingPrintWriter; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -25,6 +28,8 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; +import androidx.annotation.NonNull; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.R; @@ -89,6 +94,13 @@ public class SmartReplyView extends ViewGroup { private int mMaxNumActions; private int mMinNumSystemGeneratedReplies; + // DEBUG variables tracked for the dump() + private long mLastDrawChildTime; + private long mLastDispatchDrawTime; + private long mLastMeasureTime; + private int mTotalSqueezeRemeasureAttempts; + private boolean mDidHideSystemReplies; + public SmartReplyView(Context context, AttributeSet attrs) { super(context, attrs); @@ -217,6 +229,7 @@ public class SmartReplyView extends ViewGroup { // Mark all buttons as hidden and un-squeezed. resetButtonsLayoutParams(); + mTotalSqueezeRemeasureAttempts = 0; if (!mCandidateButtonQueueForSqueezing.isEmpty()) { Log.wtf(TAG, "Single line button queue leaked between onMeasure calls"); @@ -329,6 +342,7 @@ public class SmartReplyView extends ViewGroup { } } + mDidHideSystemReplies = false; if (mSmartRepliesGeneratedByAssistant) { if (!gotEnoughSmartReplies(smartReplies)) { // We don't have enough smart replies - hide all of them. @@ -339,6 +353,7 @@ public class SmartReplyView extends ViewGroup { // Reset our measures back to when we had only added actions (before adding // replies). accumulatedMeasures = actionsMeasures; + mDidHideSystemReplies = true; } } @@ -356,6 +371,7 @@ public class SmartReplyView extends ViewGroup { accumulatedMeasures.mMeasuredWidth), widthMeasureSpec), resolveSize(buttonHeight, heightMeasureSpec)); + mLastMeasureTime = SystemClock.elapsedRealtime(); } // TODO: this should be replaced, and instead, setMinSystemGenerated... should be invoked @@ -371,6 +387,53 @@ public class SmartReplyView extends ViewGroup { } } + /** Dump internal state for debugging */ + public void dump(IndentingPrintWriter pw) { + pw.println(this); + pw.increaseIndent(); + pw.print("mMaxSqueezeRemeasureAttempts="); + pw.println(mMaxSqueezeRemeasureAttempts); + pw.print("mTotalSqueezeRemeasureAttempts="); + pw.println(mTotalSqueezeRemeasureAttempts); + pw.print("mMaxNumActions="); + pw.println(mMaxNumActions); + pw.print("mSmartRepliesGeneratedByAssistant="); + pw.println(mSmartRepliesGeneratedByAssistant); + pw.print("mMinNumSystemGeneratedReplies="); + pw.println(mMinNumSystemGeneratedReplies); + pw.print("mHeightUpperLimit="); + pw.println(mHeightUpperLimit); + pw.print("mDidHideSystemReplies="); + pw.println(mDidHideSystemReplies); + long now = SystemClock.elapsedRealtime(); + pw.print("lastMeasureAge (s)="); + pw.println(mLastMeasureTime == 0 ? NaN : (now - mLastMeasureTime) / 1000.0f); + pw.print("lastDrawChildAge (s)="); + pw.println(mLastDrawChildTime == 0 ? NaN : (now - mLastDrawChildTime) / 1000.0f); + pw.print("lastDispatchDrawAge (s)="); + pw.println(mLastDispatchDrawTime == 0 ? NaN : (now - mLastDispatchDrawTime) / 1000.0f); + int numChildren = getChildCount(); + pw.print("children: num="); + pw.println(numChildren); + pw.increaseIndent(); + for (int i = 0; i < numChildren; i++) { + View child = getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + pw.print("["); + pw.print(i); + pw.print("] type="); + pw.print(lp.mButtonType); + pw.print(" squeezeStatus="); + pw.print(lp.squeezeStatus); + pw.print(" show="); + pw.print(lp.show); + pw.print(" view="); + pw.println(child); + } + pw.decreaseIndent(); + pw.decreaseIndent(); + } + /** * Fields we keep track of inside onMeasure() to correctly measure the SmartReplyView depending * on which suggestions are added. @@ -393,8 +456,11 @@ public class SmartReplyView extends ViewGroup { * Returns whether our notification contains at least N smart replies (or 0) where N is * determined by {@link SmartReplyConstants}. */ - // TODO: we probably sholdn't make this deliberation in the View private boolean gotEnoughSmartReplies(List<View> smartReplies) { + if (mMinNumSystemGeneratedReplies <= 1) { + // Count is irrelevant, do not bother. + return true; + } int numShownReplies = 0; for (View smartReplyButton : smartReplies) { final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams(); @@ -474,6 +540,7 @@ public class SmartReplyView extends ViewGroup { final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth; final int maxSqueezeRemeasureAttempts = mMaxSqueezeRemeasureAttempts; for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) { + mTotalSqueezeRemeasureAttempts++; final int newPosition = moveLeft ? mBreakIterator.previous() : mBreakIterator.next(); if (newPosition == BreakIterator.DONE) { @@ -613,7 +680,17 @@ public class SmartReplyView extends ViewGroup { @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - return lp.show && super.drawChild(canvas, child, drawingTime); + if (!lp.show) { + return false; + } + mLastDrawChildTime = SystemClock.elapsedRealtime(); + return super.drawChild(canvas, child, drawingTime); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + mLastDispatchDrawTime = SystemClock.elapsedRealtime(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index b630689567ce..fd387ae0a82e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -55,6 +55,8 @@ import android.view.ViewGroup; import android.view.WindowManagerGlobal; import android.widget.BaseAdapter; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; @@ -77,6 +79,7 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.tiles.UserDetailView; +import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.NotificationShadeWindowView; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -460,7 +463,7 @@ public class UserSwitcherController implements Dumpable { } @VisibleForTesting - void onUserListItemClicked(UserRecord record) { + void onUserListItemClicked(UserRecord record, DialogShower dialogShower) { int id; if (record.isGuest && record.info == null) { // No guest user. Create one. @@ -472,7 +475,7 @@ public class UserSwitcherController implements Dumpable { mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD); id = guestId; } else if (record.isAddUser) { - showAddUserDialog(); + showAddUserDialog(dialogShower); return; } else { id = record.info.id; @@ -481,7 +484,7 @@ public class UserSwitcherController implements Dumpable { int currUserId = mUserTracker.getUserId(); if (currUserId == id) { if (record.isGuest) { - showExitGuestDialog(id); + showExitGuestDialog(id, dialogShower); } return; } @@ -490,11 +493,15 @@ public class UserSwitcherController implements Dumpable { // If switching from guest, we want to bring up the guest exit dialog instead of switching UserInfo currUserInfo = mUserManager.getUserInfo(currUserId); if (currUserInfo != null && currUserInfo.isGuest()) { - showExitGuestDialog(currUserId, record.resolveId()); + showExitGuestDialog(currUserId, record.resolveId(), dialogShower); return; } } - + if (dialogShower != null) { + // If we haven't morphed into another dialog, it means we have just switched users. + // Then, dismiss the dialog. + dialogShower.dismiss(); + } switchToUserId(id); } @@ -511,7 +518,7 @@ public class UserSwitcherController implements Dumpable { } } - protected void showExitGuestDialog(int id) { + private void showExitGuestDialog(int id, DialogShower dialogShower) { int newId = UserHandle.USER_SYSTEM; if (mResumeUserOnGuestLogout && mLastNonGuestUser != UserHandle.USER_SYSTEM) { UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser); @@ -519,23 +526,31 @@ public class UserSwitcherController implements Dumpable { newId = info.id; } } - showExitGuestDialog(id, newId); + showExitGuestDialog(id, newId, dialogShower); } - protected void showExitGuestDialog(int id, int targetId) { + private void showExitGuestDialog(int id, int targetId, DialogShower dialogShower) { if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { mExitGuestDialog.cancel(); } mExitGuestDialog = new ExitGuestDialog(mContext, id, targetId); - mExitGuestDialog.show(); + if (dialogShower != null) { + dialogShower.showDialog(mExitGuestDialog); + } else { + mExitGuestDialog.show(); + } } - public void showAddUserDialog() { + private void showAddUserDialog(DialogShower dialogShower) { if (mAddUserDialog != null && mAddUserDialog.isShowing()) { mAddUserDialog.cancel(); } mAddUserDialog = new AddUserDialog(mContext); - mAddUserDialog.show(); + if (dialogShower != null) { + dialogShower.showDialog(mAddUserDialog); + } else { + mAddUserDialog.show(); + } } private void listenForCallState() { @@ -868,9 +883,17 @@ public class UserSwitcherController implements Dumpable { /** * It handles click events on user list items. + * + * If the user switcher is hosted in a dialog, passing a non-null {@link DialogShower} + * will allow animation to and from the parent dialog. + * */ + public void onUserListItemClicked(UserRecord record, @Nullable DialogShower dialogShower) { + mController.onUserListItemClicked(record, dialogShower); + } + public void onUserListItemClicked(UserRecord record) { - mController.onUserListItemClicked(record); + onUserListItemClicked(record, null); } public String getName(Context context, UserRecord item) { @@ -1156,7 +1179,7 @@ public class UserSwitcherController implements Dumpable { cancel(); } else { mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE); - dismiss(); + dismissStack(); removeGuestUser(mGuestId, mTargetId); } } @@ -1187,7 +1210,7 @@ public class UserSwitcherController implements Dumpable { if (which == BUTTON_NEGATIVE) { cancel(); } else { - dismiss(); + dismissStack(); if (ActivityManager.isUserAMonkey()) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 426bc91a606d..592fa152b406 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -176,6 +176,34 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { ? WallpaperManager.FLAG_LOCK : WallpaperManager.FLAG_SYSTEM; } + private boolean isSeedColorSet(JSONObject jsonObject, WallpaperColors newWallpaperColors) { + if (newWallpaperColors == null) { + return false; + } + // Gets the color that was overridden in the theme setting if any. + String sysPaletteColor = (String) jsonObject.opt(OVERLAY_CATEGORY_SYSTEM_PALETTE); + if (sysPaletteColor == null) { + return false; + } + if (!sysPaletteColor.startsWith("#")) { + sysPaletteColor = "#" + sysPaletteColor; + } + final int systemPaletteColorArgb = Color.parseColor(sysPaletteColor); + // Gets seed colors from incoming {@link WallpaperColors} instance. + List<Integer> seedColors = ColorScheme.getSeedColors(newWallpaperColors); + for (int seedColor : seedColors) { + // The seed color from incoming {@link WallpaperColors} instance + // was set as color override. + if (seedColor == systemPaletteColorArgb) { + if (DEBUG) { + Log.d(TAG, "Same as previous set system palette: " + sysPaletteColor); + } + return true; + } + } + return false; + } + private void handleWallpaperColors(WallpaperColors wallpaperColors, int flags) { final boolean hadWallpaperColors = mCurrentColors != null; int latestWallpaperType = getLatestWallpaperType(); @@ -213,8 +241,11 @@ public class ThemeOverlayController extends SystemUI implements Dumpable { try { JSONObject jsonObject = (overlayPackageJson == null) ? new JSONObject() : new JSONObject(overlayPackageJson); + // The latest applied wallpaper should be the source of system colors when: + // There is not preset color applied and the incoming wallpaper color is not applied if (!COLOR_SOURCE_PRESET.equals(jsonObject.optString(OVERLAY_COLOR_SOURCE)) - && ((flags & latestWallpaperType) != 0)) { + && ((flags & latestWallpaperType) != 0 && !isSeedColorSet(jsonObject, + wallpaperColors))) { mSkipSettingChange = true; if (jsonObject.has(OVERLAY_CATEGORY_ACCENT_COLOR) || jsonObject.has( OVERLAY_CATEGORY_SYSTEM_PALETTE)) { diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt index cebc93182f1e..cd3e2d335f0b 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt @@ -100,11 +100,11 @@ class UnfoldTransitionModule { fun provideShellProgressProvider( config: UnfoldTransitionConfig, provider: Optional<UnfoldTransitionProgressProvider> - ): Optional<ShellUnfoldProgressProvider> = + ): ShellUnfoldProgressProvider = if (config.isEnabled && provider.isPresent()) { - Optional.of(UnfoldProgressProvider(provider.get())) + UnfoldProgressProvider(provider.get()) } else { - Optional.empty() + ShellUnfoldProgressProvider.NO_PROVIDER } } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 74611cce6f87..c3f9c9f7aacd 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -65,6 +65,7 @@ import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.onehanded.OneHandedUiEventLogger; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.protolog.ShellProtoLogImpl; +import com.android.wm.shell.sizecompatui.SizeCompatUI; import com.android.wm.shell.splitscreen.SplitScreen; import java.io.FileDescriptor; @@ -112,6 +113,7 @@ public final class WMShell extends SystemUI private final Optional<OneHanded> mOneHandedOptional; private final Optional<HideDisplayCutout> mHideDisplayCutoutOptional; private final Optional<ShellCommandHandler> mShellCommandHandler; + private final Optional<SizeCompatUI> mSizeCompatUIOptional; private final CommandQueue mCommandQueue; private final ConfigurationController mConfigurationController; @@ -128,6 +130,7 @@ public final class WMShell extends SystemUI private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback; private KeyguardUpdateMonitorCallback mPipKeyguardCallback; private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback; + private KeyguardUpdateMonitorCallback mSizeCompatUIKeyguardCallback; private WakefulnessLifecycle.Observer mWakefulnessObserver; @Inject @@ -138,6 +141,7 @@ public final class WMShell extends SystemUI Optional<OneHanded> oneHandedOptional, Optional<HideDisplayCutout> hideDisplayCutoutOptional, Optional<ShellCommandHandler> shellCommandHandler, + Optional<SizeCompatUI> sizeCompatUIOptional, CommandQueue commandQueue, ConfigurationController configurationController, KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -162,6 +166,7 @@ public final class WMShell extends SystemUI mWakefulnessLifecycle = wakefulnessLifecycle; mProtoTracer = protoTracer; mShellCommandHandler = shellCommandHandler; + mSizeCompatUIOptional = sizeCompatUIOptional; mSysUiMainExecutor = sysUiMainExecutor; } @@ -176,6 +181,7 @@ public final class WMShell extends SystemUI mSplitScreenOptional.ifPresent(this::initSplitScreen); mOneHandedOptional.ifPresent(this::initOneHanded); mHideDisplayCutoutOptional.ifPresent(this::initHideDisplayCutout); + mSizeCompatUIOptional.ifPresent(this::initSizeCompatUi); } @VisibleForTesting @@ -367,6 +373,17 @@ public final class WMShell extends SystemUI }); } + @VisibleForTesting + void initSizeCompatUi(SizeCompatUI sizeCompatUI) { + mSizeCompatUIKeyguardCallback = new KeyguardUpdateMonitorCallback() { + @Override + public void onKeyguardOccludedChanged(boolean occluded) { + sizeCompatUI.onKeyguardOccludedChanged(occluded); + } + }; + mKeyguardUpdateMonitor.registerCallback(mSizeCompatUIKeyguardCallback); + } + @Override public void writeToProto(SystemUiTraceProto proto) { if (proto.wmShell == null) { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 5e0f427800fc..e967033b69a2 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -18,14 +18,19 @@ package com.android.keyguard; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.res.Resources; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.view.View; import android.widget.FrameLayout; @@ -50,6 +55,9 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -104,11 +112,14 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private AnimatableClockView mLargeClockView; @Mock private FrameLayout mLargeClockFrame; + @Mock + private SecureSettings mSecureSettings; private final View mFakeSmartspaceView = new View(mContext); private KeyguardClockSwitchController mController; private View mSliceView; + private FakeExecutor mExecutor; @Before public void setup() { @@ -129,6 +140,7 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { when(mView.isAttachedToWindow()).thenReturn(true); when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView); + mExecutor = new FakeExecutor(new FakeSystemClock()); mController = new KeyguardClockSwitchController( mView, mStatusBarStateController, @@ -143,6 +155,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mSmartspaceController, mKeyguardUnlockAnimationController, mSmartSpaceTransitionController, + mSecureSettings, + mExecutor, mResources ); @@ -194,7 +208,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { verifyAttachment(times(1)); listenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView); - verify(mView).onViewDetached(); verify(mColorExtractor).removeOnColorsChangedListener( any(ColorExtractor.OnColorsChangedListener.class)); } @@ -235,6 +248,25 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { verify(mSmartspaceController).requestSmartspaceUpdate(); } + @Test + public void testChangeToDoubleLineClockSetsSmallClock() { + when(mSecureSettings.getInt(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1)) + .thenReturn(0); + ArgumentCaptor<ContentObserver> observerCaptor = + ArgumentCaptor.forClass(ContentObserver.class); + mController.init(); + verify(mSecureSettings).registerContentObserver(any(Uri.class), + anyBoolean(), observerCaptor.capture()); + ContentObserver observer = observerCaptor.getValue(); + mExecutor.runAllReady(); + + // When a settings change has occurred to the small clock, make sure the view is adjusted + reset(mView); + observer.onChange(true); + mExecutor.runAllReady(); + verify(mView).switchToClock(KeyguardClockSwitch.SMALL); + } + private void verifyAttachment(VerificationMode times) { verify(mClockManager, times).addOnClockChangedListener( any(ClockManager.ClockChangedListener.class)); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt index db87c5df16e1..4bdab7658a06 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt @@ -75,8 +75,7 @@ private fun fingerprintModel(user: Int) = KeyguardFingerprintListenModel( shouldListenForFingerprintAssistant = false, switchingUser = false, udfps = false, - userDoesNotHaveTrust = false, - userNeedsStrongAuth = false + userDoesNotHaveTrust = false ) private fun faceModel(user: Int) = KeyguardFaceListenModel( diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index ff5960bc33ce..de8cc8992da0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -19,6 +19,7 @@ package com.android.keyguard; import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE; import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT; import static com.google.common.truth.Truth.assertThat; @@ -961,6 +962,19 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testStartUdfpsServiceStrongAuthRequiredAfterTimeout() { + // GIVEN status bar state is on the keyguard + mStatusBarStateListener.onStateChanged(StatusBarState.KEYGUARD); + + // WHEN user loses smart unlock trust + when(mStrongAuthTracker.getStrongAuthForUser(KeyguardUpdateMonitor.getCurrentUser())) + .thenReturn(SOME_AUTH_REQUIRED_AFTER_USER_REQUEST); + + // THEN we should still listen for udfps + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isEqualTo(true); + } + + @Test public void testShouldNotListenForUdfps_whenTrustEnabled() { // GIVEN a "we should listen for udfps" state mStatusBarStateListener.onStateChanged(StatusBarState.KEYGUARD); diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt index d4c3840356d1..9bd33eb8db6b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt @@ -16,6 +16,7 @@ import com.android.systemui.animation.DialogListener.DismissReason import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue +import org.junit.After import org.junit.Test import org.junit.runner.RunWith @@ -28,24 +29,22 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { private val dialogLaunchAnimator = DialogLaunchAnimator(context, launchAnimator, hostDialogprovider) + private val attachedViews = mutableSetOf<View>() + + @After + fun tearDown() { + runOnMainThreadAndWaitForIdleSync { + attachedViews.forEach { + ViewUtils.detachView(it) + } + } + } + @Test fun testShowDialogFromView() { // Show the dialog. showFromView() must be called on the main thread with a dialog created // on the main thread too. - val (dialog, hostDialog) = runOnMainThreadAndWaitForIdleSync { - val touchSurfaceRoot = LinearLayout(context) - val touchSurface = View(context) - touchSurfaceRoot.addView(touchSurface) - - // We need to attach the root to the window manager otherwise the exit animation will - // be skipped - ViewUtils.attachView(touchSurfaceRoot) - - val dialog = TestDialog(context) - val hostDialog = - dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog - dialog to hostDialog - } + val (dialog, hostDialog) = createDialogAndHostDialog() // Only the host dialog is actually showing. assertTrue(hostDialog.isShowing) @@ -100,6 +99,51 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { assertTrue(dialog.onStopCalled) } + @Test + fun testStackedDialogsDismissesAll() { + val (_, hostDialogFirst) = createDialogAndHostDialog() + val (dialogSecond, hostDialogSecond) = createDialogAndHostDialogFromDialog(hostDialogFirst) + + runOnMainThreadAndWaitForIdleSync { + dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations() + dialogSecond.dismissStack() + } + + assertTrue(hostDialogSecond.wasDismissed) + assertTrue(hostDialogFirst.wasDismissed) + } + + private fun createDialogAndHostDialog(): Pair<TestDialog, TestHostDialog> { + return runOnMainThreadAndWaitForIdleSync { + val touchSurfaceRoot = LinearLayout(context) + val touchSurface = View(context) + touchSurfaceRoot.addView(touchSurface) + + // We need to attach the root to the window manager otherwise the exit animation will + // be skipped + ViewUtils.attachView(touchSurfaceRoot) + attachedViews.add(touchSurfaceRoot) + + val dialog = TestDialog(context) + val hostDialog = + dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog + dialog to hostDialog + } + } + + private fun createDialogAndHostDialogFromDialog( + hostParent: Dialog + ): Pair<TestDialog, TestHostDialog> { + return runOnMainThreadAndWaitForIdleSync { + val dialog = TestDialog(context) + val hostDialog = dialogLaunchAnimator.showFromDialog( + dialog, + hostParent + ) as TestHostDialog + dialog to hostDialog + } + } + private fun <T : Any> runOnMainThreadAndWaitForIdleSync(f: () -> T): T { lateinit var result: T context.mainExecutor.execute { @@ -198,6 +242,11 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { notifyListeners { onShow() } } + fun dismissStack() { + notifyListeners { prepareForStackDismiss() } + dismiss() + } + private fun notifyListeners(notify: DialogListener.() -> Unit) { for (listener in HashSet(listeners)) { listener.notify() diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java index f525fee27e20..4ccb92609c77 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java @@ -17,6 +17,7 @@ package com.android.systemui.doze; import static com.android.systemui.doze.DozeLog.REASON_SENSOR_TAP; +import static com.android.systemui.doze.DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS; import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN; import static org.junit.Assert.assertEquals; @@ -37,6 +38,7 @@ import static org.mockito.Mockito.when; import android.database.ContentObserver; import android.hardware.Sensor; import android.hardware.display.AmbientDisplayConfiguration; +import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -57,6 +59,8 @@ import com.android.systemui.util.wakelock.WakeLock; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -94,6 +98,13 @@ public class DozeSensorsTest extends SysuiTestCase { private DevicePostureController mDevicePostureController; @Mock private ProximitySensor mProximitySensor; + + // Capture listeners so that they can be used to send events + @Captor + private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor = + ArgumentCaptor.forClass(AuthController.Callback.class); + private AuthController.Callback mAuthControllerCallback; + private FakeSettings mFakeSettings = new FakeSettings(); private SensorManagerPlugin.SensorEventListener mWakeLockScreenListener; private TestableLooper mTestableLooper; @@ -105,14 +116,18 @@ public class DozeSensorsTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mTestableLooper = TestableLooper.get(this); when(mAmbientDisplayConfiguration.tapSensorTypeMapping()) - .thenReturn(new String[]{"tapSEnsor"}); + .thenReturn(new String[]{"tapSensor"}); when(mAmbientDisplayConfiguration.getWakeLockScreenDebounce()).thenReturn(5000L); when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfiguration.enabled(UserHandle.USER_CURRENT)).thenReturn(true); doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(mWakeLock).wrap(any(Runnable.class)); mDozeSensors = new TestableDozeSensors(); + + verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture()); + mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); } @Test @@ -375,6 +390,35 @@ public class DozeSensorsTest extends SysuiTestCase { "some other name")); } + @Test + public void testUdfpsEnrollmentChanged() throws Exception { + // GIVEN a UDFPS_LONG_PRESS trigger sensor that's not configured + Sensor mockSensor = mock(Sensor.class); + TriggerSensor triggerSensor = mDozeSensors.createDozeSensor( + mockSensor, + REASON_SENSOR_UDFPS_LONG_PRESS, + /* configured */ false); + mDozeSensors.addSensor(triggerSensor); + when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor))) + .thenReturn(true); + + // WHEN listening state is set to TRUE + mDozeSensors.setListening(true, true); + + // THEN mRegistered is still false b/c !mConfigured + assertFalse(triggerSensor.mConfigured); + assertFalse(triggerSensor.mRegistered); + + // WHEN enrollment changes to TRUE + when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true); + mAuthControllerCallback.onEnrollmentsChanged(); + + // THEN mConfigured = TRUE + assertTrue(triggerSensor.mConfigured); + + // THEN mRegistered = TRUE + assertTrue(triggerSensor.mRegistered); + } private class TestableDozeSensors extends DozeSensors { TestableDozeSensors() { @@ -407,6 +451,22 @@ public class DozeSensorsTest extends SysuiTestCase { requiresTouchScreen); } + public TriggerSensor createDozeSensor( + Sensor sensor, + int pulseReason, + boolean configured + ) { + return new TriggerSensor(/* sensor */ sensor, + /* setting name */ "test_setting", + /* settingDefault */ true, + /* configured */ configured, + /* pulseReason*/ pulseReason, + /* reportsTouchCoordinate*/ false, + /* requiresTouchscreen */ false, + /* ignoresSetting */ false, + /* requiresTouchScreen */false); + } + /** * create a doze sensor that supports postures and is enabled */ @@ -422,6 +482,15 @@ public class DozeSensorsTest extends SysuiTestCase { /* requiresProx */false, posture); } + + public void addSensor(TriggerSensor sensor) { + TriggerSensor[] newArray = new TriggerSensor[mTriggerSensors.length + 1]; + for (int i = 0; i < mTriggerSensors.length; i++) { + newArray[i] = mTriggerSensors[i]; + } + newArray[mTriggerSensors.length] = sensor; + mTriggerSensors = newArray; + } } public static void setSensorType(Sensor sensor, int type, String strType) throws Exception { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java index b6e8979db189..b32b4d4f3810 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java @@ -219,61 +219,89 @@ public class InternetDialogTest extends SysuiTestCase { } @Test - public void updateDialog_wifiOnAndNoWifiList_hideWifiListAndSeeAll() { + public void updateDialog_wifiOnAndNoWifiEntry_hideWifiEntryAndSeeAll() { // The precondition WiFi ON is already in setUp() + mInternetDialog.mConnectedWifiEntry = null; mInternetDialog.mWifiEntriesCount = 0; mInternetDialog.updateDialog(false); + assertThat(mConnectedWifi.getVisibility()).isEqualTo(View.GONE); assertThat(mWifiList.getVisibility()).isEqualTo(View.GONE); assertThat(mSeeAll.getVisibility()).isEqualTo(View.GONE); } @Test + public void updateDialog_wifiOnAndHasConnectedWifi_showConnectedWifiAndSeeAll() { + // The preconditions WiFi ON and WiFi entries are already in setUp() + mInternetDialog.mWifiEntriesCount = 0; + + mInternetDialog.updateDialog(false); + + assertThat(mConnectedWifi.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mWifiList.getVisibility()).isEqualTo(View.GONE); + assertThat(mSeeAll.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test public void updateDialog_wifiOnAndHasWifiList_showWifiListAndSeeAll() { // The preconditions WiFi ON and WiFi entries are already in setUp() + mInternetDialog.mConnectedWifiEntry = null; mInternetDialog.updateDialog(false); + assertThat(mConnectedWifi.getVisibility()).isEqualTo(View.GONE); assertThat(mWifiList.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mSeeAll.getVisibility()).isEqualTo(View.VISIBLE); } @Test - public void updateDialog_deviceLockedAndHasInternetWifi_showHighlightWifiToggle() { - // The preconditions WiFi ON and Internet WiFi are already in setUp() - when(mInternetDialogController.isDeviceLocked()).thenReturn(true); + public void updateDialog_wifiOnAndHasBothWifiEntry_showBothWifiEntryAndSeeAll() { + // The preconditions WiFi ON and WiFi entries are already in setUp() mInternetDialog.updateDialog(false); - assertThat(mWifiToggle.getVisibility()).isEqualTo(View.VISIBLE); - assertThat(mWifiToggle.getBackground()).isNotNull(); + assertThat(mConnectedWifi.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mWifiList.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mSeeAll.getVisibility()).isEqualTo(View.VISIBLE); } @Test - public void updateDialog_deviceLockedAndHasInternetWifi_hideConnectedWifi() { - // The preconditions WiFi ON and Internet WiFi are already in setUp() + public void updateDialog_deviceLockedAndNoConnectedWifi_showWifiToggle() { + // The preconditions WiFi entries are already in setUp() when(mInternetDialogController.isDeviceLocked()).thenReturn(true); + mInternetDialog.mConnectedWifiEntry = null; mInternetDialog.updateDialog(false); + // Show WiFi Toggle without background + assertThat(mWifiToggle.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mWifiToggle.getBackground()).isNull(); + // Hide Wi-Fi networks and See all assertThat(mConnectedWifi.getVisibility()).isEqualTo(View.GONE); + assertThat(mWifiList.getVisibility()).isEqualTo(View.GONE); + assertThat(mSeeAll.getVisibility()).isEqualTo(View.GONE); } @Test - public void updateDialog_deviceLockedAndHasWifiList_hideWifiListAndSeeAll() { - // The preconditions WiFi entries are already in setUp() + public void updateDialog_deviceLockedAndHasConnectedWifi_showWifiToggleWithBackground() { + // The preconditions WiFi ON and WiFi entries are already in setUp() when(mInternetDialogController.isDeviceLocked()).thenReturn(true); mInternetDialog.updateDialog(false); + // Show WiFi Toggle with highlight background + assertThat(mWifiToggle.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mWifiToggle.getBackground()).isNotNull(); + // Hide Wi-Fi networks and See all + assertThat(mConnectedWifi.getVisibility()).isEqualTo(View.GONE); assertThat(mWifiList.getVisibility()).isEqualTo(View.GONE); assertThat(mSeeAll.getVisibility()).isEqualTo(View.GONE); } @Test public void updateDialog_wifiOn_hideWifiScanNotify() { - // The preconditions WiFi ON and Internet WiFi are already in setUp() + // The preconditions WiFi ON and WiFi entries are already in setUp() mInternetDialog.updateDialog(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt index 7e900c843cc3..ea3a42ce501c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.user +import android.app.Dialog import android.content.Intent import android.provider.Settings import android.testing.AndroidTestingRunner @@ -27,7 +28,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PseudoGridView import com.android.systemui.qs.tiles.UserDetailView -import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq @@ -39,15 +40,13 @@ import org.mockito.ArgumentMatcher import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.`when` -import org.mockito.Mockito.any +import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.anyInt import org.mockito.Mockito.argThat import org.mockito.Mockito.inOrder -import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import java.util.function.Consumer @SmallTest @RunWith(AndroidTestingRunner::class) @@ -71,6 +70,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { private lateinit var gridView: PseudoGridView @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator + @Mock + private lateinit var hostDialog: Dialog @Captor private lateinit var clickCaptor: ArgumentCaptor<View.OnClickListener> @@ -85,6 +86,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { `when`(dialog.grid).thenReturn(gridView) `when`(launchView.context).thenReturn(mContext) + `when`(dialogLaunchAnimator.showFromView(any(), any(), anyBoolean())) + .thenReturn(hostDialog) controller = UserSwitchDialogController( { userDetailViewAdapter }, @@ -188,15 +191,15 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { } @Test - fun callbackFromDetailView_dismissesDialog() { - val captor = argumentCaptor<Consumer<UserSwitcherController.UserRecord>>() + fun callbackFromDialogShower_dismissesDialog() { + val captor = argumentCaptor<UserSwitchDialogController.DialogShower>() controller.showDialog(launchView) - verify(userDetailViewAdapter).injectCallback(capture(captor)) + verify(userDetailViewAdapter).injectDialogShower(capture(captor)) - captor.value.accept(mock(UserSwitcherController.UserRecord::class.java)) + captor.value.dismiss() - verify(dialog).dismiss() + verify(hostDialog).dismiss() } private class IntentMatcher(private val action: String) : ArgumentMatcher<Intent> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt index 69ab9c51db81..bdd189a9215b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt @@ -44,13 +44,16 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.QSUserSwitcherEvent +import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.phone.NotificationShadeWindowView import com.android.systemui.telephony.TelephonyListenerManager import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.time.FakeSystemClock import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull +import org.junit.Assert.assertFalse import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -85,6 +88,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor @Mock private lateinit var latencyTracker: LatencyTracker + @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower + @Mock private lateinit var notificationShadeWindowView: NotificationShadeWindowView private lateinit var testableLooper: TestableLooper private lateinit var uiBgExecutor: FakeExecutor private lateinit var uiEventLogger: UiEventLoggerFake @@ -98,6 +103,8 @@ class UserSwitcherControllerTest : SysuiTestCase() { private val guestId = 1234 private val guestInfo = UserInfo(guestId, "Guest", null, UserInfo.FLAG_FULL or UserInfo.FLAG_GUEST, UserManager.USER_TYPE_FULL_GUEST) + private val secondaryUser = + UserInfo(10, "Secondary", null, 0, UserManager.USER_TYPE_FULL_SECONDARY) @Before fun setUp() { @@ -114,6 +121,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { mock(FingerprintManager::class.java)) `when`(userManager.canAddMoreUsers()).thenReturn(true) + `when`(notificationShadeWindowView.context).thenReturn(context) userSwitcherController = UserSwitcherController( context, @@ -139,6 +147,26 @@ class UserSwitcherControllerTest : SysuiTestCase() { userSwitcherController.mPauseRefreshUsers = true picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user)) + userSwitcherController.init(notificationShadeWindowView) + } + + @Test + fun testSwitchUser_parentDialogDismissed() { + val otherUserRecord = UserSwitcherController.UserRecord( + secondaryUser, + picture, + false /* guest */, + false /* current */, + false /* isAddUser */, + false /* isRestricted */, + true /* isSwitchToEnabled */) + `when`(userTracker.userId).thenReturn(ownerId) + `when`(userTracker.userInfo).thenReturn(ownerInfo) + + userSwitcherController.onUserListItemClicked(otherUserRecord, dialogShower) + testableLooper.processAllMessages() + + verify(dialogShower).dismiss() } @Test @@ -156,7 +184,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { `when`(userManager.createGuest(any(), anyString())).thenReturn(guestInfo) - userSwitcherController.onUserListItemClicked(emptyGuestUserRecord) + userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, null) testableLooper.processAllMessages() verify(interactionJankMonitor).begin(any()) verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH) @@ -166,6 +194,26 @@ class UserSwitcherControllerTest : SysuiTestCase() { } @Test + fun testAddGuest_parentDialogDismissed() { + val emptyGuestUserRecord = UserSwitcherController.UserRecord( + null, + null, + true /* guest */, + false /* current */, + false /* isAddUser */, + false /* isRestricted */, + true /* isSwitchToEnabled */) + `when`(userTracker.userId).thenReturn(ownerId) + `when`(userTracker.userInfo).thenReturn(ownerInfo) + + `when`(userManager.createGuest(any(), anyString())).thenReturn(guestInfo) + + userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, dialogShower) + testableLooper.processAllMessages() + verify(dialogShower).dismiss() + } + + @Test fun testRemoveGuest_removeButtonPressed_isLogged() { val currentGuestUserRecord = UserSwitcherController.UserRecord( guestInfo, @@ -178,7 +226,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { `when`(userTracker.userId).thenReturn(guestInfo.id) `when`(userTracker.userInfo).thenReturn(guestInfo) - userSwitcherController.onUserListItemClicked(currentGuestUserRecord) + userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) assertNotNull(userSwitcherController.mExitGuestDialog) userSwitcherController.mExitGuestDialog .getButton(DialogInterface.BUTTON_POSITIVE).performClick() @@ -188,6 +236,46 @@ class UserSwitcherControllerTest : SysuiTestCase() { } @Test + fun testRemoveGuest_removeButtonPressed_dialogDismissed() { + val currentGuestUserRecord = UserSwitcherController.UserRecord( + guestInfo, + picture, + true /* guest */, + true /* current */, + false /* isAddUser */, + false /* isRestricted */, + true /* isSwitchToEnabled */) + `when`(userTracker.userId).thenReturn(guestInfo.id) + `when`(userTracker.userInfo).thenReturn(guestInfo) + + userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) + assertNotNull(userSwitcherController.mExitGuestDialog) + userSwitcherController.mExitGuestDialog + .getButton(DialogInterface.BUTTON_POSITIVE).performClick() + testableLooper.processAllMessages() + assertFalse(userSwitcherController.mExitGuestDialog.isShowing) + } + + @Test + fun testRemoveGuest_dialogShowerUsed() { + val currentGuestUserRecord = UserSwitcherController.UserRecord( + guestInfo, + picture, + true /* guest */, + true /* current */, + false /* isAddUser */, + false /* isRestricted */, + true /* isSwitchToEnabled */) + `when`(userTracker.userId).thenReturn(guestInfo.id) + `when`(userTracker.userInfo).thenReturn(guestInfo) + + userSwitcherController.onUserListItemClicked(currentGuestUserRecord, dialogShower) + assertNotNull(userSwitcherController.mExitGuestDialog) + testableLooper.processAllMessages() + verify(dialogShower).showDialog(userSwitcherController.mExitGuestDialog) + } + + @Test fun testRemoveGuest_cancelButtonPressed_isNotLogged() { val currentGuestUserRecord = UserSwitcherController.UserRecord( guestInfo, @@ -200,7 +288,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { `when`(userTracker.userId).thenReturn(guestId) `when`(userTracker.userInfo).thenReturn(guestInfo) - userSwitcherController.onUserListItemClicked(currentGuestUserRecord) + userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) assertNotNull(userSwitcherController.mExitGuestDialog) userSwitcherController.mExitGuestDialog .getButton(DialogInterface.BUTTON_NEGATIVE).performClick() @@ -226,7 +314,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt())) .thenReturn(1) - userSwitcherController.onUserListItemClicked(currentGuestUserRecord) + userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) // Simulate a user switch event val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) @@ -260,7 +348,7 @@ class UserSwitcherControllerTest : SysuiTestCase() { eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt())) .thenReturn(1) - userSwitcherController.onUserListItemClicked(currentGuestUserRecord) + userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) // Simulate a user switch event val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 766471b9a695..3ff5666271bf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -248,8 +248,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { Color.valueOf(Color.BLUE), null); String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"home_wallpaper\"," + "{\"android.theme.customization.color_source\":\"home_wallpaper\"," + + "\"android.theme.customization.system_palette\":\"A16B00\"," + + "\"android.theme.customization.accent_color\":\"A16B00\"," + "\"android.theme.customization.color_index\":\"2\"}"; when(mSecureSettings.getStringForUser( @@ -274,14 +275,15 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { } @Test - public void onWallpaperColorsChanged_ResetThemeWithDifferentWallpapers() { + public void onWallpaperColorsChanged_ResetThemeWithNewHomeWallpapers() { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"home_wallpaper\"," + "{\"android.theme.customization.color_source\":\"home_wallpaper\"," + + "\"android.theme.customization.system_palette\":\"A16B00\"," + + "\"android.theme.customization.accent_color\":\"A16B00\"," + "\"android.theme.customization.color_index\":\"2\"}"; when(mSecureSettings.getStringForUser( @@ -304,14 +306,15 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { } @Test - public void onWallpaperColorsChanged_ResetThemeWithSameWallpaper() { + public void onWallpaperColorsChanged_ResetThemeWithNewHomeAndLockWallpaper() { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"home_wallpaper\"," + "{\"android.theme.customization.color_source\":\"home_wallpaper\"," + + "\"android.theme.customization.system_palette\":\"A16B00\"," + + "\"android.theme.customization.accent_color\":\"A16B00\"," + "\"android.theme.customization.color_index\":\"2\"}"; when(mSecureSettings.getStringForUser( @@ -339,8 +342,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"home_wallpaper\"," + "{\"android.theme.customization.color_source\":\"home_wallpaper\"," + + "\"android.theme.customization.system_palette\":\"A16B00\"," + + "\"android.theme.customization.accent_color\":\"A16B00\"," + "\"android.theme.customization.color_index\":\"2\"}"; when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -366,8 +370,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"lock_wallpaper\"," + "{\"android.theme.customization.color_source\":\"lock_wallpaper\"," + + "\"android.theme.customization.system_palette\":\"A16B00\"," + + "\"android.theme.customization.accent_color\":\"A16B00\"," + "\"android.theme.customization.color_index\":\"2\"}"; when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -394,8 +399,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { Color.valueOf(Color.BLUE), null); String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"home_wallpaper\"," + "{\"android.theme.customization.color_source\":\"home_wallpaper\"," + + "\"android.theme.customization.system_palette\":\"A16B00\"," + + "\"android.theme.customization.accent_color\":\"A16B00\"," + "\"android.theme.customization.color_index\":\"2\"}"; when(mSecureSettings.getStringForUser( @@ -416,6 +422,36 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { } @Test + public void onWallpaperColorsChanged_keepThemeWhenFromLatestWallpaperAndSpecifiedColor() { + // Shouldn't ask for a new theme when the colors of the last applied wallpaper change + // with the same specified system palette one. + WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), + Color.valueOf(0xffa16b00), null); + + String jsonString = + "{\"android.theme.customization.color_source\":\"home_wallpaper\"," + + "\"android.theme.customization.system_palette\":\"A16B00\"," + + "\"android.theme.customization.accent_color\":\"A16B00\"," + + "\"android.theme.customization.color_index\":\"2\"}"; + + when(mSecureSettings.getStringForUser( + eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) + .thenReturn(jsonString); + when(mWallpaperManager.getWallpaperId(WallpaperManager.FLAG_LOCK)).thenReturn(1); + // SYSTEM wallpaper is the last applied one + when(mWallpaperManager.getWallpaperId(WallpaperManager.FLAG_SYSTEM)).thenReturn(2); + + mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM); + + ArgumentCaptor<String> updatedSetting = ArgumentCaptor.forClass(String.class); + verify(mSecureSettings, never()).putString( + eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), updatedSetting.capture()); + + // Apply overlay by existing theme from secure setting + verify(mThemeOverlayApplier).applyCurrentUserOverlays(any(), any(), anyInt(), any()); + } + + @Test public void onWallpaperColorsChanged_keepThemeIfNotLatestWallpaper() { // Shouldn't ask for a new theme when the colors of the wallpaper that is not the last // applied one change @@ -423,8 +459,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { Color.valueOf(Color.BLUE), null); String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"home_wallpaper\"," + "{\"android.theme.customization.color_source\":\"home_wallpaper\"," + + "\"android.theme.customization.system_palette\":\"A16B00\"," + + "\"android.theme.customization.accent_color\":\"A16B00\"," + "\"android.theme.customization.color_index\":\"2\"}"; when(mSecureSettings.getStringForUser( diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java index 8480702c57c0..ae7afcef57a6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -41,6 +41,7 @@ import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEventCallback; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.sizecompatui.SizeCompatUI; import com.android.wm.shell.splitscreen.SplitScreen; import org.junit.Before; @@ -76,6 +77,7 @@ public class WMShellTest extends SysuiTestCase { @Mock WakefulnessLifecycle mWakefulnessLifecycle; @Mock ProtoTracer mProtoTracer; @Mock ShellCommandHandler mShellCommandHandler; + @Mock SizeCompatUI mSizeCompatUI; @Mock ShellExecutor mSysUiMainExecutor; @Before @@ -84,10 +86,10 @@ public class WMShellTest extends SysuiTestCase { mWMShell = new WMShell(mContext, Optional.of(mPip), Optional.of(mLegacySplitScreen), Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mHideDisplayCutout), - Optional.of(mShellCommandHandler), mCommandQueue, mConfigurationController, - mKeyguardUpdateMonitor, mNavigationModeController, - mScreenLifecycle, mSysUiState, mProtoTracer, mWakefulnessLifecycle, - mSysUiMainExecutor); + Optional.of(mShellCommandHandler), Optional.of(mSizeCompatUI), + mCommandQueue, mConfigurationController, mKeyguardUpdateMonitor, + mNavigationModeController, mScreenLifecycle, mSysUiState, mProtoTracer, + mWakefulnessLifecycle, mSysUiMainExecutor); } @Test @@ -129,4 +131,11 @@ public class WMShellTest extends SysuiTestCase { verify(mConfigurationController).addCallback( any(ConfigurationController.ConfigurationListener.class)); } + + @Test + public void initSizeCompatUI_registersCallbacks() { + mWMShell.initSizeCompatUi(mSizeCompatUI); + + verify(mKeyguardUpdateMonitor).registerCallback(any(KeyguardUpdateMonitorCallback.class)); + } } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index a2fec2753340..df2ae987f8cb 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -87,13 +87,13 @@ import android.app.ActivityManagerInternal.ServiceNotificationPolicy; import android.app.ActivityThread; import android.app.AppGlobals; import android.app.AppOpsManager; -import android.app.ForegroundServiceDidNotStartInTimeException; import android.app.ForegroundServiceStartNotAllowedException; import android.app.IApplicationThread; import android.app.IServiceConnection; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.RemoteServiceException.ForegroundServiceDidNotStartInTimeException; import android.app.Service; import android.app.ServiceStartArgs; import android.app.admin.DevicePolicyEventLogger; @@ -1243,7 +1243,7 @@ public final class ActiveServices { } void killMisbehavingService(ServiceRecord r, - int appUid, int appPid, String localPackageName) { + int appUid, int appPid, String localPackageName, int exceptionTypeId) { synchronized (mAm) { if (!r.destroying) { // This service is still alive, stop it. @@ -1257,8 +1257,8 @@ public final class ActiveServices { stopServiceLocked(found, false); } } - mAm.crashApplication(appUid, appPid, localPackageName, -1, - "Bad notification for startForeground", true /*force*/); + mAm.crashApplicationWithType(appUid, appPid, localPackageName, -1, + "Bad notification for startForeground", true /*force*/, exceptionTypeId); } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b5f3389c50e0..5cd66aabbf2e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -183,7 +183,6 @@ import android.app.PendingIntent; import android.app.ProcessMemoryState; import android.app.ProfilerInfo; import android.app.PropertyInvalidatedCache; -import android.app.RemoteServiceException; import android.app.SyncNotedAppOp; import android.app.WaitResult; import android.app.backup.BackupManager.OperationType; @@ -3000,13 +2999,6 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void crashApplication(int uid, int initialPid, String packageName, int userId, - String message, boolean force) { - crashApplicationWithType(uid, initialPid, packageName, userId, message, force, - RemoteServiceException.TYPE_ID); - } - - @Override public void crashApplicationWithType(int uid, int initialPid, String packageName, int userId, String message, boolean force, int exceptionTypeId) { if (checkCallingPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 31e48fb0837f..ea28117a6a3d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -46,6 +46,7 @@ import android.app.IStopUserCallback; import android.app.IUidObserver; import android.app.KeyguardManager; import android.app.ProfilerInfo; +import android.app.RemoteServiceException.CrashedByAdbException; import android.app.UserSwitchObserver; import android.app.WaitResult; import android.app.usage.AppStandbyInfo; @@ -1154,7 +1155,8 @@ final class ActivityManagerShellCommand extends ShellCommand { } catch (NumberFormatException e) { packageName = arg; } - mInterface.crashApplication(-1, pid, packageName, userId, "shell-induced crash", false); + mInterface.crashApplicationWithType(-1, pid, packageName, userId, "shell-induced crash", + false, CrashedByAdbException.TYPE_ID); return 0; } diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index 94bf62f8b9b7..487101b7eac4 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -20,7 +20,13 @@ import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY; import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE; import static android.text.TextUtils.formatSimple; -import static com.android.server.am.ActivityManagerDebugConfig.*; +import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST; +import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_DEFERRAL; +import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_LIGHT; +import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU; +import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; +import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_BROADCAST; +import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_MU; import android.annotation.Nullable; import android.app.ActivityManager; @@ -29,6 +35,7 @@ import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.IApplicationThread; import android.app.PendingIntent; +import android.app.RemoteServiceException.CannotDeliverBroadcastException; import android.app.usage.UsageEvents.Event; import android.content.ComponentName; import android.content.ContentResolver; @@ -603,7 +610,8 @@ public final class BroadcastQueue { synchronized (mService) { Slog.w(TAG, "Can't deliver broadcast to " + app.processName + " (pid " + app.getPid() + "). Crashing it."); - app.scheduleCrashLocked("can't deliver broadcast"); + app.scheduleCrashLocked("can't deliver broadcast", + CannotDeliverBroadcastException.TYPE_ID); } throw ex; } diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 9e94d4aa2c0f..14ba7167f529 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -26,7 +26,6 @@ import android.app.ApplicationExitInfo; import android.app.ApplicationExitInfo.Reason; import android.app.ApplicationExitInfo.SubReason; import android.app.IApplicationThread; -import android.app.RemoteServiceException; import android.content.pm.ApplicationInfo; import android.content.pm.ProcessInfo; import android.content.pm.VersionedPackage; @@ -948,11 +947,6 @@ class ProcessRecord implements WindowProcessListener { return mServices.hasForegroundServices(); } - @GuardedBy("mService") - void scheduleCrashLocked(String message) { - scheduleCrashLocked(message, RemoteServiceException.TYPE_ID); - } - /** * Let an app process throw an exception on a binder thread, which typically crashes the * process, unless it has an unhandled exception handler. diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 17930ea9c93c..e36898fee387 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -28,6 +28,7 @@ import android.annotation.Nullable; import android.app.IApplicationThread; import android.app.Notification; import android.app.PendingIntent; +import android.app.RemoteServiceException.CannotPostForegroundServiceNotificationException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -1039,7 +1040,8 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN // If it gave us a garbage notification, it doesn't // get to be foreground. ams.mServices.killMisbehavingService(record, - appUid, appPid, localPackageName); + appUid, appPid, localPackageName, + CannotPostForegroundServiceNotificationException.TYPE_ID); } } }); diff --git a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java index aa4fa7c6f470..4a41cb6d81e2 100644 --- a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java @@ -917,7 +917,7 @@ public final class MultiClientInputMethodManagerService { .putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( context, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), - PendingIntent.FLAG_MUTABLE)); + PendingIntent.FLAG_IMMUTABLE)); // Note: Instead of re-dispatching callback from the main thread to the worker thread // where OnWorkerThreadCallback is running, we pass the Handler object here so that diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 47d8022a5acc..7ed897dfdbf9 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -149,6 +149,7 @@ import android.app.NotificationHistory.HistoricalNotification; import android.app.NotificationManager; import android.app.NotificationManager.Policy; import android.app.PendingIntent; +import android.app.RemoteServiceException.BadForegroundServiceNotificationException; import android.app.StatsManager; import android.app.StatusBarManager; import android.app.UriGrantsManager; @@ -1256,10 +1257,11 @@ public class NotificationManagerService extends SystemService { // Still crash for foreground services, preventing the not-crash behaviour abused // by apps to give us a garbage notification and silently start a fg service. Binder.withCleanCallingIdentity( - () -> mAm.crashApplication(uid, initialPid, pkg, -1, + () -> mAm.crashApplicationWithType(uid, initialPid, pkg, -1, "Bad notification(tag=" + tag + ", id=" + id + ") posted from package " + pkg + ", crashing app(uid=" + uid + ", pid=" + initialPid + "): " - + message, true /* force */)); + + message, true /* force */, + BadForegroundServiceNotificationException.TYPE_ID)); } } @@ -7065,7 +7067,9 @@ public class NotificationManagerService extends SystemService { if (index < 0) { mNotificationList.add(r); mUsageStats.registerPostedByApp(r); - r.setInterruptive(isVisuallyInterruptive(null, r)); + final boolean isInterruptive = isVisuallyInterruptive(null, r); + r.setInterruptive(isInterruptive); + r.setTextChanged(isInterruptive); } else { old = mNotificationList.get(index); // Potentially *changes* old mNotificationList.set(index, r); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index c493639fc6b1..0ad119df6b55 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -3872,6 +3872,19 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testTextChangedSet_forNewNotifs() throws Exception { + NotificationRecord original = generateNotificationRecord(mTestNotificationChannel); + mService.addEnqueuedNotification(original); + + NotificationManagerService.PostNotificationRunnable runnable = + mService.new PostNotificationRunnable(original.getKey()); + runnable.run(); + waitForIdle(); + + assertTrue(original.isTextChanged()); + } + + @Test public void testVisuallyInterruptive_notSeen() throws Exception { NotificationRecord original = generateNotificationRecord(mTestNotificationChannel); mService.addNotification(original); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index f05dd636aa22..e19ea47df3bb 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -841,7 +841,7 @@ final class HotwordDetectionConnection { try { return mContext.bindIsolatedService( mIntent, - Context.BIND_AUTO_CREATE | mBindingFlags, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE | mBindingFlags, "hotword_detector_" + mInstanceNumber, mExecutor, serviceConnection); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 4d2e00785d49..9c9e41b64892 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -5865,7 +5865,7 @@ public class CarrierConfigManager { sDefaults.putString(KEY_CARRIER_PROVISIONING_APP_STRING, ""); sDefaults.putBoolean(KEY_DISPLAY_NO_DATA_NOTIFICATION_ON_PERMANENT_FAILURE_BOOL, false); sDefaults.putBoolean(KEY_UNTHROTTLE_DATA_RETRY_WHEN_TAC_CHANGES_BOOL, false); - sDefaults.putBoolean(KEY_VONR_SETTING_VISIBILITY_BOOL, false); + sDefaults.putBoolean(KEY_VONR_SETTING_VISIBILITY_BOOL, true); sDefaults.putBoolean(KEY_VONR_ENABLED_BOOL, false); } |