diff options
242 files changed, 6638 insertions, 2424 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 3ffab902f18d..664dfe980b49 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4919,6 +4919,7 @@ package android.app { method public int getPendingIntentBackgroundActivityStartMode(); method public int getPendingIntentCreatorBackgroundActivityStartMode(); method public int getSplashScreenStyle(); + method @FlaggedApi("com.android.window.flags.touch_pass_through_opt_in") public boolean isAllowPassThroughOnTouchOutside(); method @Deprecated public boolean isPendingIntentBackgroundActivityLaunchAllowed(); method public boolean isShareIdentityEnabled(); method public static android.app.ActivityOptions makeBasic(); @@ -4932,6 +4933,7 @@ package android.app { method public static android.app.ActivityOptions makeTaskLaunchBehind(); method public static android.app.ActivityOptions makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int); method public void requestUsageTimeReport(android.app.PendingIntent); + method @FlaggedApi("com.android.window.flags.touch_pass_through_opt_in") public void setAllowPassThroughOnTouchOutside(boolean); method public android.app.ActivityOptions setAppVerificationBundle(android.os.Bundle); method public android.app.ActivityOptions setLaunchBounds(@Nullable android.graphics.Rect); method public android.app.ActivityOptions setLaunchDisplayId(int); @@ -8775,6 +8777,11 @@ package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager { method @Deprecated @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); + field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 + field public static final int APP_FUNCTION_STATE_DISABLED = 2; // 0x2 + field public static final int APP_FUNCTION_STATE_ENABLED = 1; // 0x1 } @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service { @@ -8816,6 +8823,7 @@ package android.app.appfunctions { field public static final String PROPERTY_RETURN_VALUE = "returnValue"; field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2 field public static final int RESULT_DENIED = 1; // 0x1 + field public static final int RESULT_DISABLED = 6; // 0x6 field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3 field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4 field public static final int RESULT_OK = 0; // 0x0 diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 91aa225039a4..0d183c7c37aa 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -26,6 +26,7 @@ import static android.content.Intent.FLAG_RECEIVER_FOREGROUND; import static android.view.Display.INVALID_DISPLAY; import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -453,6 +454,10 @@ public class ActivityOptions extends ComponentOptions { private static final String KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE = "android.activity.pendingIntentCreatorBackgroundActivityStartMode"; + /** See {@link #setAllowPassThroughOnTouchOutside(boolean)}. */ + private static final String KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE = + "android.activity.allowPassThroughOnTouchOutside"; + /** * @see #setLaunchCookie * @hide @@ -554,6 +559,7 @@ public class ActivityOptions extends ComponentOptions { private int mPendingIntentCreatorBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; private boolean mDisableStartingWindow; + private boolean mAllowPassThroughOnTouchOutside; /** * Create an ActivityOptions specifying a custom animation to run when @@ -1416,6 +1422,7 @@ public class ActivityOptions extends ComponentOptions { KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE, MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); mDisableStartingWindow = opts.getBoolean(KEY_DISABLE_STARTING_WINDOW); + mAllowPassThroughOnTouchOutside = opts.getBoolean(KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE); mAnimationAbortListener = IRemoteCallback.Stub.asInterface( opts.getBinder(KEY_ANIM_ABORT_LISTENER)); } @@ -1839,6 +1846,39 @@ public class ActivityOptions extends ComponentOptions { && mLaunchIntoPipParams.isLaunchIntoPip(); } + /** + * Returns whether the source activity allows the overlaying activities from the to-be-launched + * app to pass through touch events to it when touches fall outside the content window. + * + * @see #setAllowPassThroughOnTouchOutside(boolean) + */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_TOUCH_PASS_THROUGH_OPT_IN) + public boolean isAllowPassThroughOnTouchOutside() { + return mAllowPassThroughOnTouchOutside; + } + + /** + * Sets whether the source activity allows the overlaying activities from the to-be-launched + * app to pass through touch events to it when touches fall outside the content window. + * + * <p> By default, touches that fall on a translucent non-touchable area of an overlaying + * activity window are blocked from passing through to the activity below (source activity), + * unless the overlaying activity is from the same UID as the source activity. The source + * activity may use this method to opt in and allow the overlaying activities from the + * to-be-launched app to pass through touches to itself. The source activity needs to ensure + * that it trusts the overlaying activity and its content is not vulnerable to UI redressing + * attacks. The flag is ignored if the context calling + * {@link Context#startActivity(Intent, Bundle)} is not an activity. + * + * <p> For backward compatibility, apps with target SDK 35 and below may still receive + * pass-through touches without opt-in if the cross-uid activity is launched by the source + * activity. + */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_TOUCH_PASS_THROUGH_OPT_IN) + public void setAllowPassThroughOnTouchOutside(boolean allowed) { + mAllowPassThroughOnTouchOutside = allowed; + } + /** @hide */ public int getLaunchActivityType() { return mLaunchActivityType; @@ -2520,6 +2560,10 @@ public class ActivityOptions extends ComponentOptions { if (mDisableStartingWindow) { b.putBoolean(KEY_DISABLE_STARTING_WINDOW, mDisableStartingWindow); } + if (mAllowPassThroughOnTouchOutside) { + b.putBoolean(KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE, + mAllowPassThroughOnTouchOutside); + } b.putBinder(KEY_ANIM_ABORT_LISTENER, mAnimationAbortListener != null ? mAnimationAbortListener.asBinder() : null); return b; diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index b781ce50c4db..f21c3e8d44d6 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -493,6 +493,9 @@ public class DownloadManager { * {@link Environment#getExternalStoragePublicDirectory(String)} with * {@link Environment#DIRECTORY_DOWNLOADS}). * + * All non-visible downloads that are not modified in the last 7 days will be deleted during + * idle runs. + * * @param uri a file {@link Uri} indicating the destination for the downloaded file. * @return this object */ @@ -796,7 +799,9 @@ public class DownloadManager { * public Downloads directory (as returned by * {@link Environment#getExternalStoragePublicDirectory(String)} with * {@link Environment#DIRECTORY_DOWNLOADS}) will be visible in system's Downloads UI - * and the rest will not be visible. + * and the rest will not be visible. All non-visible downloads that are not modified + * in the last 7 days will be deleted during idle runs. + * * (e.g. {@link Context#getExternalFilesDir(String)}) will not be visible. */ @Deprecated diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index a39f216d033e..c21fe0e2d8b3 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1619,22 +1619,6 @@ public class Notification implements Parcelable public static final String EXTRA_DECLINE_COLOR = "android.declineColor"; /** - * {@link #extras} key: {@link Icon} of an image used as an overlay Icon on - * {@link Notification#mLargeIcon} for {@link EnRouteStyle} notifications. - * This extra is an {@code Icon}. - * @hide - */ - @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) - public static final String EXTRA_ENROUTE_OVERLAY_ICON = "android.enrouteOverlayIcon"; - - /** - * {@link #extras} key: text used as a sub-text for the largeIcon of - * {@link EnRouteStyle} notification. This extra is a {@code CharSequence}. - * @hide - */ - @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) - public static final String EXTRA_ENROUTE_LARGE_ICON_SUBTEXT = "android.enrouteLargeIconSubText"; - /** * {@link #extras} key: whether the notification should be colorized as * supplied to {@link Builder#setColorized(boolean)}. */ @@ -3152,7 +3136,6 @@ public class Notification implements Parcelable } if (Flags.apiRichOngoing()) { - visitIconUri(visitor, extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class)); visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class)); visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class)); visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class)); @@ -11173,145 +11156,6 @@ public class Notification implements Parcelable } /** - * TODO(b/360827871): Make EnRouteStyle public. - * A style used to represent the progress of a real-world journey with a known destination. - * For example: - * <ul> - * <li>Delivery tracking</li> - * <li>Ride progress</li> - * <li>Flight tracking</li> - * </ul> - * - * The exact fields from {@link Notification} that are shown with this style may vary by - * the surface where this update appears, but the following fields are recommended: - * <ul> - * <li>{@link Notification.Builder#setContentTitle}</li> - * <li>{@link Notification.Builder#setContentText}</li> - * <li>{@link Notification.Builder#setSubText}</li> - * <li>{@link Notification.Builder#setLargeIcon}</li> - * <li>{@link Notification.Builder#setProgress}</li> - * <li>{@link Notification.Builder#setWhen} - This should be the future time of the next, - * final, or most important stop on this journey.</li> - * </ul> - * @hide - */ - @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) - public static class EnRouteStyle extends Notification.Style { - - @Nullable - private Icon mOverlayIcon = null; - - @Nullable - private CharSequence mLargeIconSubText = null; - - public EnRouteStyle() { - } - - /** - * Returns the overlay icon to be displayed on {@link Notification#mLargeIcon}. - * @see EnRouteStyle#setOverlayIcon - */ - @Nullable - public Icon getOverlayIcon() { - return mOverlayIcon; - } - - /** - * Optional icon to be displayed on {@link Notification#mLargeIcon}. - * - * This image will be cropped to a circle and will obscure - * a semicircle of the right side of the large icon. - */ - @NonNull - public EnRouteStyle setOverlayIcon(@Nullable Icon overlayIcon) { - mOverlayIcon = overlayIcon; - return this; - } - - /** - * Returns the sub-text for {@link Notification#mLargeIcon}. - * @see EnRouteStyle#setLargeIconSubText - */ - @Nullable - public CharSequence getLargeIconSubText() { - return mLargeIconSubText; - } - - /** - * Optional text which generally related to - * the {@link Notification.Builder#setLargeIcon} or {@link #setOverlayIcon} or both. - */ - @NonNull - public EnRouteStyle setLargeIconSubText(@Nullable CharSequence largeIconSubText) { - mLargeIconSubText = stripStyling(largeIconSubText); - return this; - } - - /** - * @hide - */ - @Override - public boolean areNotificationsVisiblyDifferent(Style other) { - if (other == null || getClass() != other.getClass()) { - return true; - } - - final EnRouteStyle enRouteStyle = (EnRouteStyle) other; - return !Objects.equals(mOverlayIcon, enRouteStyle.mOverlayIcon) - || !Objects.equals(mLargeIconSubText, enRouteStyle.mLargeIconSubText); - } - - /** - * @hide - */ - @Override - public void addExtras(Bundle extras) { - super.addExtras(extras); - extras.putParcelable(EXTRA_ENROUTE_OVERLAY_ICON, mOverlayIcon); - extras.putCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT, mLargeIconSubText); - } - - /** - * @hide - */ - @Override - protected void restoreFromExtras(Bundle extras) { - super.restoreFromExtras(extras); - mOverlayIcon = extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class); - mLargeIconSubText = extras.getCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT); - } - - /** - * @hide - */ - @Override - public void purgeResources() { - super.purgeResources(); - if (mOverlayIcon != null) { - mOverlayIcon.convertToAshmem(); - } - } - - /** - * @hide - */ - @Override - public void reduceImageSizes(Context context) { - super.reduceImageSizes(context); - if (mOverlayIcon != null) { - final Resources resources = context.getResources(); - final boolean isLowRam = ActivityManager.isLowRamDeviceStatic(); - - int rightIconSize = resources.getDimensionPixelSize(isLowRam - ? R.dimen.notification_right_icon_size_low_ram - : R.dimen.notification_right_icon_size); - mOverlayIcon.scaleDownIfNecessary(rightIconSize, rightIconSize); - } - } - } - - - /** * A Notification Style used to to define a notification whose expanded state includes * a highly customizable progress bar with segments, steps, a custom tracker icon, * and custom icons at the start and end of the progress bar. diff --git a/core/java/android/app/admin/PolicySizeVerifier.java b/core/java/android/app/admin/PolicySizeVerifier.java index 7f8e50ec4420..1e03e1fd206d 100644 --- a/core/java/android/app/admin/PolicySizeVerifier.java +++ b/core/java/android/app/admin/PolicySizeVerifier.java @@ -22,7 +22,9 @@ import android.os.Parcelable; import android.os.PersistableBundle; import com.android.internal.util.Preconditions; +import com.android.modules.utils.ModifiedUtf8; +import java.io.UTFDataFormatException; import java.util.ArrayDeque; import java.util.Queue; @@ -33,8 +35,6 @@ import java.util.Queue; */ public class PolicySizeVerifier { - // Binary XML serializer doesn't support longer strings - public static final int MAX_POLICY_STRING_LENGTH = 65535; // FrameworkParsingPackageUtils#MAX_FILE_NAME_SIZE, Android packages are used in dir names. public static final int MAX_PACKAGE_NAME_LENGTH = 223; @@ -47,8 +47,11 @@ public class PolicySizeVerifier { * Throw if string argument is too long to be serialized. */ public static void enforceMaxStringLength(String str, String argName) { - Preconditions.checkArgument( - str.length() <= MAX_POLICY_STRING_LENGTH, argName + " loo long"); + try { + long len = ModifiedUtf8.countBytes(str, /* throw error if too long */ true); + } catch (UTFDataFormatException e) { + throw new IllegalArgumentException(argName + " too long"); + } } /** diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 216ba5d994ec..6797a51e59f4 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -22,15 +22,21 @@ import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANA import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.UserHandleAware; +import android.app.appsearch.AppSearchManager; import android.content.Context; import android.os.CancellationSignal; import android.os.ICancellationSignal; +import android.os.OutcomeReceiver; +import android.os.ParcelableException; import android.os.RemoteException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -45,10 +51,44 @@ import java.util.function.Consumer; @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) @SystemService(Context.APP_FUNCTION_SERVICE) public final class AppFunctionManager { + + /** + * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset + * enabled state to the default value. + */ + public static final int APP_FUNCTION_STATE_DEFAULT = 0; + + /** + * The app function is enabled. To enable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_ENABLED = 1; + + /** + * The app function is disabled. To disable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_DISABLED = 2; + private final IAppFunctionManager mService; private final Context mContext; /** + * The enabled state of the app function. + * + * @hide + */ + @IntDef( + prefix = {"APP_FUNCTION_STATE_"}, + value = { + APP_FUNCTION_STATE_DEFAULT, + APP_FUNCTION_STATE_ENABLED, + APP_FUNCTION_STATE_DISABLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface EnabledState {} + + /** * Creates an instance. * * @param service An interface to the backing service. @@ -164,4 +204,118 @@ public final class AppFunctionManager { throw e.rethrowFromSystemServer(); } } + + /** + * Returns a boolean through a callback, indicating whether the app function is enabled. + * + * <p>* This method can only check app functions owned by the caller, or those where the caller + * has visibility to the owner package and holds either the {@link + * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link + * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission. + * + * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: + * + * <ul> + * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not + * have access to it. + * </ul> + * + * @param functionIdentifier the identifier of the app function to check (unique within the + * target package) and in most cases, these are automatically generated by the AppFunctions + * SDK + * @param targetPackage the package name of the app function's owner + * @param executor the executor to run the request + * @param callback the callback to receive the function enabled check result + */ + public void isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull String targetPackage, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Boolean, Exception> callback) { + Objects.requireNonNull(functionIdentifier); + Objects.requireNonNull(targetPackage); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + AppSearchManager appSearchManager = mContext.getSystemService(AppSearchManager.class); + if (appSearchManager == null) { + callback.onError(new IllegalStateException("Failed to get AppSearchManager.")); + return; + } + + AppFunctionManagerHelper.isAppFunctionEnabled( + functionIdentifier, targetPackage, appSearchManager, executor, callback); + } + + /** + * Sets the enabled state of the app function owned by the calling package. + * + * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: + * + * <ul> + * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not + * have access to it. + * </ul> + * + * @param functionIdentifier the identifier of the app function to enable (unique within the + * calling package). In most cases, identifiers are automatically generated by the + * AppFunctions SDK + * @param newEnabledState the new state of the app function + * @param executor the executor to run the callback + * @param callback the callback to receive the result of the function enablement. The call was + * successful if no exception was thrown. + */ + @UserHandleAware + public void setAppFunctionEnabled( + @NonNull String functionIdentifier, + @EnabledState int newEnabledState, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Void, Exception> callback) { + Objects.requireNonNull(functionIdentifier); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + CallbackWrapper callbackWrapper = new CallbackWrapper(executor, callback); + try { + mService.setAppFunctionEnabled( + mContext.getPackageName(), + functionIdentifier, + mContext.getUser(), + newEnabledState, + callbackWrapper); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private static class CallbackWrapper extends IAppFunctionEnabledCallback.Stub { + + private final OutcomeReceiver<Void, Exception> mCallback; + private final Executor mExecutor; + + CallbackWrapper( + @NonNull Executor callbackExecutor, + @NonNull OutcomeReceiver<Void, Exception> callback) { + mCallback = callback; + mExecutor = callbackExecutor; + } + + @Override + public void onSuccess() { + mExecutor.execute(() -> mCallback.onResult(null)); + } + + @Override + public void onError(@NonNull ParcelableException exception) { + mExecutor.execute(() -> { + if (IllegalArgumentException.class.isAssignableFrom( + exception.getCause().getClass())) { + mCallback.onError((IllegalArgumentException) exception.getCause()); + } else if (SecurityException.class.isAssignableFrom( + exception.getCause().getClass())) { + mCallback.onError((SecurityException) exception.getCause()); + } else { + mCallback.onError(exception); + } + }); + } + } } diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java index d6f45e4c9f6a..fe2db49684fd 100644 --- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java +++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java @@ -22,7 +22,7 @@ import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCT import static android.app.appfunctions.AppFunctionStaticMetadataHelper.STATIC_PROPERTY_ENABLED_BY_DEFAULT; import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; -import android.annotation.CallbackExecutor; +import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.app.appsearch.AppSearchManager; @@ -33,6 +33,7 @@ import android.app.appsearch.SearchResult; import android.app.appsearch.SearchResults; import android.app.appsearch.SearchSpec; import android.os.OutcomeReceiver; +import android.text.TextUtils; import java.io.IOException; import java.util.List; @@ -50,73 +51,69 @@ public class AppFunctionManagerHelper { /** * Returns (through a callback) a boolean indicating whether the app function is enabled. * - * <p>This method can only check app functions that are owned by the caller owned by packages - * visible to the caller. + * This method can only check app functions owned by the caller, or those where the caller + * has visibility to the owner package and holds either the {@link + * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link + * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission. * * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: * * <ul> - * <li>{@link IllegalArgumentException}, if the function is not found - * <li>{@link SecurityException}, if the caller does not have permission to query the target - * package + * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not + * have access to it. * </ul> * * @param functionIdentifier the identifier of the app function to check (unique within the - * target package) and in most cases, these are automatically generated by the AppFunctions - * SDK - * @param targetPackage the package name of the app function's owner - * @param appSearchExecutor the executor to run the metadata search mechanism through AppSearch - * @param callbackExecutor the executor to run the callback - * @param callback the callback to receive the function enabled check result + * target package) and in most cases, these are automatically + * generated by the AppFunctions + * SDK + * @param targetPackage the package name of the app function's owner + * @param executor executor the executor to run the request + * @param callback the callback to receive the function enabled check result * @hide */ public static void isAppFunctionEnabled( @NonNull String functionIdentifier, @NonNull String targetPackage, @NonNull AppSearchManager appSearchManager, - @NonNull Executor appSearchExecutor, - @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull Executor executor, @NonNull OutcomeReceiver<Boolean, Exception> callback) { Objects.requireNonNull(functionIdentifier); Objects.requireNonNull(targetPackage); Objects.requireNonNull(appSearchManager); - Objects.requireNonNull(appSearchExecutor); - Objects.requireNonNull(callbackExecutor); + Objects.requireNonNull(executor); Objects.requireNonNull(callback); appSearchManager.createGlobalSearchSession( - appSearchExecutor, + executor, (searchSessionResult) -> { if (!searchSessionResult.isSuccess()) { - callbackExecutor.execute( - () -> - callback.onError( - failedResultToException(searchSessionResult))); + callback.onError(failedResultToException(searchSessionResult)); return; } try (GlobalSearchSession searchSession = searchSessionResult.getResultValue()) { SearchResults results = searchJoinedStaticWithRuntimeAppFunctions( - searchSession, targetPackage, functionIdentifier); + Objects.requireNonNull(searchSession), + targetPackage, + functionIdentifier); results.getNextPage( - appSearchExecutor, - listAppSearchResult -> - callbackExecutor.execute( - () -> { - if (listAppSearchResult.isSuccess()) { - callback.onResult( - getEnabledStateFromSearchResults( - Objects.requireNonNull( - listAppSearchResult + executor, + listAppSearchResult -> { + if (listAppSearchResult.isSuccess()) { + callback.onResult( + getEffectiveEnabledStateFromSearchResults( + Objects.requireNonNull( + listAppSearchResult .getResultValue()))); - } else { - callback.onError( - failedResultToException( - listAppSearchResult)); - } - })); + } else { + callback.onError( + failedResultToException(listAppSearchResult)); + } + }); + results.close(); } catch (Exception e) { - callbackExecutor.execute(() -> callback.onError(e)); + callback.onError(e); } }); } @@ -124,56 +121,58 @@ public class AppFunctionManagerHelper { /** * Searches joined app function static and runtime metadata using the function Id and the * package. - * - * @hide */ private static @NonNull SearchResults searchJoinedStaticWithRuntimeAppFunctions( @NonNull GlobalSearchSession session, @NonNull String targetPackage, @NonNull String functionIdentifier) { SearchSpec runtimeSearchSpec = - getAppFunctionRuntimeMetadataSearchSpecByFunctionId(targetPackage); + getAppFunctionRuntimeMetadataSearchSpecByPackageName(targetPackage); JoinSpec joinSpec = new JoinSpec.Builder(PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID) - .setNestedSearch(functionIdentifier, runtimeSearchSpec) + .setNestedSearch( + buildFilerRuntimeMetadataByFunctionIdQuery(functionIdentifier), + runtimeSearchSpec) .build(); SearchSpec joinedStaticWithRuntimeSearchSpec = new SearchSpec.Builder() - .setJoinSpec(joinSpec) .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE) .addFilterSchemas( AppFunctionStaticMetadataHelper.getStaticSchemaNameForPackage( targetPackage)) - .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) + .setJoinSpec(joinSpec) + .setVerbatimSearchEnabled(true) .build(); - return session.search(functionIdentifier, joinedStaticWithRuntimeSearchSpec); + return session.search( + buildFilerStaticMetadataByFunctionIdQuery(functionIdentifier), + joinedStaticWithRuntimeSearchSpec); } /** - * Finds whether the function is enabled or not from the search results returned by {@link - * #searchJoinedStaticWithRuntimeAppFunctions}. + * Returns whether the function is effectively enabled or not from the search results returned + * by {@link #searchJoinedStaticWithRuntimeAppFunctions}. * + * @param joinedStaticRuntimeResults search results joining AppFunctionStaticMetadata + * and AppFunctionRuntimeMetadata. * @throws IllegalArgumentException if the function is not found in the results - * @hide */ - private static boolean getEnabledStateFromSearchResults( + private static boolean getEffectiveEnabledStateFromSearchResults( @NonNull List<SearchResult> joinedStaticRuntimeResults) { if (joinedStaticRuntimeResults.isEmpty()) { - // Function not found. throw new IllegalArgumentException("App function not found."); } else { List<SearchResult> runtimeMetadataResults = joinedStaticRuntimeResults.getFirst().getJoinedResults(); - if (!runtimeMetadataResults.isEmpty()) { - Boolean result = - (Boolean) - runtimeMetadataResults - .getFirst() - .getGenericDocument() - .getProperty(PROPERTY_ENABLED); - if (result != null) { - return result; - } + if (runtimeMetadataResults.isEmpty()) { + throw new IllegalArgumentException("App function not found."); + } + boolean[] enabled = + runtimeMetadataResults + .getFirst() + .getGenericDocument() + .getPropertyBooleanArray(PROPERTY_ENABLED); + if (enabled != null && enabled.length != 0) { + return enabled[0]; } // Runtime metadata not found. Using the default value in the static metadata. return joinedStaticRuntimeResults @@ -186,36 +185,39 @@ public class AppFunctionManagerHelper { /** * Returns search spec that queries app function metadata for a specific package name by its * function identifier. - * - * @hide */ - public static @NonNull SearchSpec getAppFunctionRuntimeMetadataSearchSpecByFunctionId( + private static @NonNull SearchSpec getAppFunctionRuntimeMetadataSearchSpecByPackageName( @NonNull String targetPackage) { return new SearchSpec.Builder() .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE) .addFilterSchemas( AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(targetPackage)) - .addFilterProperties( - AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(targetPackage), - List.of(AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID)) - .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) + .setVerbatimSearchEnabled(true) .build(); } - /** - * Converts a failed app search result codes into an exception. - * - * @hide - */ - public static @NonNull Exception failedResultToException( + private static String buildFilerRuntimeMetadataByFunctionIdQuery(String functionIdentifier) { + return TextUtils.formatSimple("%s:\"%s\"", + AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID, + functionIdentifier); + } + + private static String buildFilerStaticMetadataByFunctionIdQuery(String functionIdentifier) { + return TextUtils.formatSimple("%s:\"%s\"", + AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID, + functionIdentifier); + } + + /** Converts a failed app search result codes into an exception. */ + private static @NonNull Exception failedResultToException( @NonNull AppSearchResult appSearchResult) { return switch (appSearchResult.getResultCode()) { - case AppSearchResult.RESULT_INVALID_ARGUMENT -> - new IllegalArgumentException(appSearchResult.getErrorMessage()); - case AppSearchResult.RESULT_IO_ERROR -> - new IOException(appSearchResult.getErrorMessage()); - case AppSearchResult.RESULT_SECURITY_ERROR -> - new SecurityException(appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException( + appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_IO_ERROR -> new IOException( + appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException( + appSearchResult.getErrorMessage()); default -> new IllegalStateException(appSearchResult.getErrorMessage()); }; } diff --git a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java index 83b5aa05c383..8b7f326ee816 100644 --- a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java +++ b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java @@ -204,11 +204,17 @@ public class AppFunctionRuntimeMetadata extends GenericDocument { packageName, functionId)); } + public Builder(AppFunctionRuntimeMetadata original) { + this(original.getPackageName(), original.getFunctionId()); + setEnabled(original.getEnabled()); + } + /** * Sets an indicator specifying if the function is enabled or not. This would override the * default enabled state in the static metadata ({@link - * AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT}). Sets this to - * null to clear the override. + * AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT}). Sets this to null + * to clear the override. + * TODO(369683073) Replace the tristate Boolean with IntDef EnabledState. */ @NonNull public Builder setEnabled(@Nullable Boolean enabled) { diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java index f6580e63d757..4ed0a1b50a08 100644 --- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java +++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java @@ -99,6 +99,9 @@ public final class ExecuteAppFunctionResponse implements Parcelable { /** The operation was timed out. */ public static final int RESULT_TIMED_OUT = 5; + /** The caller tried to execute a disabled app function. */ + public static final int RESULT_DISABLED = 6; + /** The result code of the app function execution. */ @ResultCode private final int mResultCode; @@ -274,6 +277,7 @@ public final class ExecuteAppFunctionResponse implements Parcelable { RESULT_INTERNAL_ERROR, RESULT_INVALID_ARGUMENT, RESULT_TIMED_OUT, + RESULT_DISABLED, }) @Retention(RetentionPolicy.SOURCE) public @interface ResultCode {} diff --git a/core/java/android/app/appfunctions/IAppFunctionEnabledCallback.aidl b/core/java/android/app/appfunctions/IAppFunctionEnabledCallback.aidl new file mode 100644 index 000000000000..ced415541e49 --- /dev/null +++ b/core/java/android/app/appfunctions/IAppFunctionEnabledCallback.aidl @@ -0,0 +1,27 @@ +/* + * 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 android.app.appfunctions; + +import android.os.ParcelableException; + +/** + * @hide + */ +oneway interface IAppFunctionEnabledCallback { + void onSuccess(); + void onError(in ParcelableException exception); +} diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl index c63217ffe850..72335e40c207 100644 --- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl +++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl @@ -17,9 +17,11 @@ package android.app.appfunctions; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; +import android.app.appfunctions.IAppFunctionEnabledCallback; import android.app.appfunctions.IExecuteAppFunctionCallback; import android.os.ICancellationSignal; +import android.os.UserHandle; /** * Defines the interface for apps to interact with the app function execution service * {@code AppFunctionManagerService} running in the system server process. @@ -37,4 +39,15 @@ interface IAppFunctionManager { in ExecuteAppFunctionAidlRequest request, in IExecuteAppFunctionCallback callback ); + + /** + * Sets an AppFunction's enabled state provided by {@link AppFunctionService} through the system. + */ + void setAppFunctionEnabled( + in String callingPackage, + in String functionIdentifier, + in UserHandle userHandle, + int enabledState, + in IAppFunctionEnabledCallback callback + ); } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 303197dfd82d..e1732559e262 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -1079,9 +1079,12 @@ public class ZenModeConfig implements Parcelable { // in ensureManualZenRule() and setManualZenMode(). rt.manualRule.pkg = PACKAGE_ANDROID; rt.manualRule.type = AutomaticZenRule.TYPE_OTHER; - rt.manualRule.condition = new Condition( - rt.manualRule.conditionId != null ? rt.manualRule.conditionId - : Uri.EMPTY, "", Condition.STATE_TRUE); + // conditionId in rule must match condition.id to pass isValidManualRule(). + if (rt.manualRule.conditionId == null) { + rt.manualRule.conditionId = Uri.EMPTY; + } + rt.manualRule.condition = new Condition(rt.manualRule.conditionId, "", + Condition.STATE_TRUE); } } return rt; diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS index 31a8dfaaf86b..1ea58bcbb76a 100644 --- a/core/java/android/view/OWNERS +++ b/core/java/android/view/OWNERS @@ -48,6 +48,7 @@ per-file KeyCharacterMap.java = file:/services/core/java/com/android/server/inpu per-file VelocityTracker.java = file:/services/core/java/com/android/server/input/OWNERS per-file VerifiedInputEvent.java = file:/services/core/java/com/android/server/input/OWNERS per-file VerifiedInputEvent.aidl = file:/services/core/java/com/android/server/input/OWNERS +per-file LetterboxScrollProcessor*.java = file:/services/core/java/com/android/server/input/OWNERS # InputWindowHandle per-file InputWindowHandle.java = file:/services/core/java/com/android/server/input/OWNERS diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 2f649c21fe08..1e5c6d8177e1 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -465,13 +465,6 @@ public final class InputMethodManager { private static final long USE_ASYNC_SHOW_HIDE_METHOD = 352594277L; // This is a bug id. /** - * Version-gating is guarded by bug-fix flag. - */ - private static final boolean ASYNC_SHOW_HIDE_METHOD_ENABLED = - !Flags.compatchangeForZerojankproxy() - || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD); - - /** * If {@code true}, avoid calling the * {@link com.android.server.inputmethod.InputMethodManagerService InputMethodManagerService} * by skipping the call to {@link IInputMethodManager#startInputOrWindowGainedFocus} @@ -614,6 +607,15 @@ public final class InputMethodManager { @UnsupportedAppUsage Rect mCursorRect = new Rect(); + /** + * Version-gating is guarded by bug-fix flag. + */ + // Note: this is non-static so that it only gets initialized once CompatChanges has + // access to the correct application context. + private final boolean mAsyncShowHideMethodEnabled = + !Flags.compatchangeForZerojankproxy() + || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD); + /** Cached value for {@link #isStylusHandwritingAvailable} for userId. */ @GuardedBy("mH") private PropertyInvalidatedCache<Integer, Boolean> mStylusHandwritingAvailableCache; @@ -2419,7 +2421,7 @@ public final class InputMethodManager { mCurRootView.getLastClickToolType(), resultReceiver, reason, - ASYNC_SHOW_HIDE_METHOD_ENABLED); + mAsyncShowHideMethodEnabled); } } } @@ -2463,7 +2465,7 @@ public final class InputMethodManager { mCurRootView.getLastClickToolType(), resultReceiver, reason, - ASYNC_SHOW_HIDE_METHOD_ENABLED); + mAsyncShowHideMethodEnabled); } } @@ -2572,7 +2574,7 @@ public final class InputMethodManager { return true; } else { return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, - statsToken, flags, resultReceiver, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED); + statsToken, flags, resultReceiver, reason, mAsyncShowHideMethodEnabled); } } } @@ -2615,7 +2617,7 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, view.getWindowToken(), - statsToken, flags, null, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED); + statsToken, flags, null, reason, mAsyncShowHideMethodEnabled); } } @@ -3392,7 +3394,7 @@ public final class InputMethodManager { servedInputConnection == null ? null : servedInputConnection.asIRemoteAccessibilityInputConnection(), view.getContext().getApplicationInfo().targetSdkVersion, targetUserId, - mImeDispatcher, ASYNC_SHOW_HIDE_METHOD_ENABLED); + mImeDispatcher, mAsyncShowHideMethodEnabled); } else { res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, startInputFlags, diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index b0e38e256430..cff42fbcfcc5 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -399,13 +399,10 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto return -1; } case "enable-text" -> { - if (mViewerConfigReader != null) { - mViewerConfigReader.loadViewerConfig(groups, logger); - } - return setTextLogging(true, logger, groups); + return startLoggingToLogcat(groups, logger); } case "disable-text" -> { - return setTextLogging(false, logger, groups); + return stopLoggingToLogcat(groups, logger); } default -> { return unknownCommand(pw); diff --git a/core/java/com/android/internal/protolog/Utils.java b/core/java/com/android/internal/protolog/Utils.java index 1e6ba309c046..00ef80ab2bdd 100644 --- a/core/java/com/android/internal/protolog/Utils.java +++ b/core/java/com/android/internal/protolog/Utils.java @@ -93,8 +93,7 @@ public class Utils { os.write(TAG, tag); break; default: - throw new RuntimeException( - "Unexpected field id " + pis.getFieldNumber()); + Log.e(LOG_TAG, "Unexpected field id " + pis.getFieldNumber()); } } @@ -126,8 +125,7 @@ public class Utils { os.write(LOCATION, pis.readString(LOCATION)); break; default: - throw new RuntimeException( - "Unexpected field id " + pis.getFieldNumber()); + Log.e(LOG_TAG, "Unexpected field id " + pis.getFieldNumber()); } } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 92c390656da5..5c0dca2104af 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6183,6 +6183,10 @@ is enabled and activity is connected to the camera in fullscreen. --> <bool name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled">false</bool> + <!-- Which aspect ratio to use when camera compat treatment is enabled and an activity eligible + for treatment is connected to the camera. --> + <item name="config_windowManagerCameraCompatAspectRatio" format="float" type="dimen">1.0</item> + <!-- Docking is a uiMode configuration change and will cause activities to relaunch if it's not handled. If true, the configuration change will be sent but activities will not be relaunched upon docking. Apps with desk resources will behave like normal, since they may diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 5f40a6c7eba4..807df1be7fb5 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4778,6 +4778,7 @@ <java-symbol type="bool" name="config_isCompatFakeFocusEnabled" /> <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" /> <java-symbol type="bool" name="config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled" /> + <java-symbol type="dimen" name="config_windowManagerCameraCompatAspectRatio" /> <java-symbol type="bool" name="config_skipActivityRelaunchWhenDocking" /> <java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" /> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java index bfccb29bc952..e3a1d8ac48e2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java @@ -142,6 +142,19 @@ class BackupHelper { } } + void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) { + // Clean-up the legacy states in the system + for (int i = mTaskFragmentInfos.size() - 1; i >= 0; i--) { + final TaskFragmentInfo info = mTaskFragmentInfos.valueAt(i); + mPresenter.deleteTaskFragment(wct, info.getFragmentToken()); + } + mPresenter.setSavedState(new Bundle()); + + mParcelableTaskContainerDataList.clear(); + mTaskFragmentInfos.clear(); + mTaskFragmentParentInfos.clear(); + } + boolean hasPendingStateToRestore() { return !mParcelableTaskContainerDataList.isEmpty(); } @@ -196,6 +209,7 @@ class BackupHelper { mController.onTaskFragmentParentRestored(wct, taskContainer.getTaskId(), mTaskFragmentParentInfos.get(taskContainer.getTaskId())); + mTaskFragmentParentInfos.remove(taskContainer.getTaskId()); restoredAny = true; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index db4bb0e5e75e..8345b409ae52 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -56,6 +56,7 @@ import static androidx.window.extensions.embedding.TaskFragmentContainer.Overlay import android.annotation.CallbackExecutor; import android.app.Activity; import android.app.ActivityClient; +import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.AppGlobals; @@ -280,7 +281,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mSplitRules.clear(); mSplitRules.addAll(rules); - if (!Flags.aeBackStackRestore() || !mPresenter.isRebuildTaskContainersNeeded()) { + if (!Flags.aeBackStackRestore() || !mPresenter.isWaitingToRebuildTaskContainers()) { return; } @@ -2893,6 +2894,36 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } synchronized (mLock) { + if (mPresenter.isWaitingToRebuildTaskContainers()) { + Log.w(TAG, "Rebuilding aborted, clean up and restart"); + + // Retrieve the Task intent. + final int taskId = getTaskId(activity); + Intent taskIntent = null; + final ActivityManager am = activity.getSystemService(ActivityManager.class); + final List<ActivityManager.AppTask> appTasks = am.getAppTasks(); + for (ActivityManager.AppTask appTask : appTasks) { + if (appTask.getTaskInfo().taskId == taskId) { + taskIntent = appTask.getTaskInfo().baseIntent.cloneFilter(); + break; + } + } + + // Clean up and abort the restoration + // TODO(b/369488857): also to remove the non-organized activities in the Task? + final TransactionRecord transactionRecord = + mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + mPresenter.abortTaskContainerRebuilding(wct); + transactionRecord.apply(false /* shouldApplyIndependently */); + + // Start the Task root activity. + if (taskIntent != null) { + activity.startActivity(taskIntent); + } + return; + } + final IBinder activityToken = activity.getActivityToken(); final IBinder initialTaskFragmentToken = getTaskFragmentTokenFromActivityClientRecord(activity); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 0c0ded9bad74..b498ee2ff438 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -187,10 +187,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mBackupHelper.scheduleBackup(); } - boolean isRebuildTaskContainersNeeded() { + boolean isWaitingToRebuildTaskContainers() { return mBackupHelper.hasPendingStateToRestore(); } + void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) { + mBackupHelper.abortTaskContainerRebuilding(wct); + } + boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct, @NonNull Set<EmbeddingRule> rules) { return mBackupHelper.rebuildTaskContainers(wct, rules); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index dcc2d93060c9..b453f1d4e936 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -156,7 +156,7 @@ class TaskContainer { mSplitController = splitController; for (ParcelableTaskFragmentContainerData tfData : data.getParcelableTaskFragmentContainerDataList()) { - final TaskFragmentInfo info = taskFragmentInfoMap.get(tfData.mToken); + final TaskFragmentInfo info = taskFragmentInfoMap.remove(tfData.mToken); if (info != null && !info.isEmpty()) { final TaskFragmentContainer container = new TaskFragmentContainer(tfData, splitController, this); 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 af6844262771..7f6118689dad 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 @@ -592,8 +592,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "onActivityRestartAttempt: %s", task.topActivity); - if (task.getWindowingMode() != WINDOWING_MODE_PINNED) { + "onActivityRestartAttempt: topActivity=%s, wasVisible=%b", + task.topActivity, wasVisible); + if (task.getWindowingMode() != WINDOWING_MODE_PINNED || !wasVisible) { return; } if (mPipTaskOrganizer.isLaunchToSplit(task)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index d3bed59f7994..a2439a937512 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -361,8 +361,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int anim = getRotationAnimationHint(change, info, mDisplayController); isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS; if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) { - startRotationAnimation(startTransaction, change, info, anim, animations, - onAnimFinish); + final int flags = wallpaperTransit != WALLPAPER_TRANSITION_NONE + && Flags.commonSurfaceAnimator() + ? ScreenRotationAnimation.FLAG_HAS_WALLPAPER : 0; + startRotationAnimation(startTransaction, change, info, anim, flags, + animations, onAnimFinish); isDisplayRotationAnimationStarted = true; continue; } @@ -414,7 +417,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if (change.getParent() == null && !change.hasFlags(FLAG_IS_DISPLAY) && change.getStartRotation() != change.getEndRotation()) { startRotationAnimation(startTransaction, change, info, - ROTATION_ANIMATION_ROTATE, animations, onAnimFinish); + ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish); continue; } } @@ -699,12 +702,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } private void startRotationAnimation(SurfaceControl.Transaction startTransaction, - TransitionInfo.Change change, TransitionInfo info, int animHint, + TransitionInfo.Change change, TransitionInfo info, int animHint, int flags, ArrayList<Animator> animations, Runnable onAnimFinish) { final int rootIdx = TransitionUtil.rootIndexFor(change, info); final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(), - animHint); + animHint, flags); // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real // content, and background color. The item of "animGroup" will be removed if the sub // animation is finished. Then if the list becomes empty, the rotation animation is done. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index 5802e2ca8133..1a04997fa384 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -25,12 +25,9 @@ import static com.android.wm.shell.transition.DefaultTransitionHandler.buildSurf import static com.android.wm.shell.transition.Transitions.TAG; import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; -import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.hardware.HardwareBuffer; @@ -38,6 +35,7 @@ import android.util.Slog; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; +import android.view.animation.AccelerateInterpolator; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.window.ScreenCapture; @@ -74,6 +72,7 @@ import java.util.ArrayList; */ class ScreenRotationAnimation { static final int MAX_ANIMATION_DURATION = 10 * 1000; + static final int FLAG_HAS_WALLPAPER = 1; private final Context mContext; private final TransactionPool mTransactionPool; @@ -98,6 +97,12 @@ class ScreenRotationAnimation { private SurfaceControl mBackColorSurface; /** The leash using to animate screenshot layer. */ private final SurfaceControl mAnimLeash; + /** + * The container with background color for {@link #mSurfaceControl}. It is only created if + * {@link #mSurfaceControl} may be translucent. E.g. visible wallpaper with alpha < 1 (dimmed). + * That prevents flickering of alpha blending. + */ + private SurfaceControl mBackEffectSurface; // The current active animation to move from the old to the new rotated // state. Which animation is run here will depend on the old and new @@ -111,8 +116,8 @@ class ScreenRotationAnimation { /** Intensity of light/whiteness of the layout after rotation occurs. */ private float mEndLuma; - ScreenRotationAnimation(Context context, TransactionPool pool, - Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) { + ScreenRotationAnimation(Context context, TransactionPool pool, Transaction t, + TransitionInfo.Change change, SurfaceControl rootLeash, int animHint, int flags) { mContext = context; mTransactionPool = pool; mAnimHint = animHint; @@ -170,11 +175,20 @@ class ScreenRotationAnimation { } hardwareBuffer.close(); } + if ((flags & FLAG_HAS_WALLPAPER) != 0) { + mBackEffectSurface = new SurfaceControl.Builder() + .setCallsite("ShellRotationAnimation").setParent(rootLeash) + .setEffectLayer().setOpaque(true).setName("BackEffect").build(); + t.reparent(mSurfaceControl, mBackEffectSurface) + .setColor(mBackEffectSurface, + new float[] {mStartLuma, mStartLuma, mStartLuma}) + .show(mBackEffectSurface); + } t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE); t.show(mAnimLeash); // Crop the real content in case it contains a larger child layer, e.g. wallpaper. - t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight)); + t.setCrop(getEnterSurface(), new Rect(0, 0, mEndWidth, mEndHeight)); if (!isCustomRotate()) { mBackColorSurface = new SurfaceControl.Builder() @@ -202,6 +216,11 @@ class ScreenRotationAnimation { return mAnimHint == ROTATION_ANIMATION_CROSSFADE || mAnimHint == ROTATION_ANIMATION_JUMPCUT; } + /** Returns the surface which contains the real content to animate enter. */ + private SurfaceControl getEnterSurface() { + return mBackEffectSurface != null ? mBackEffectSurface : mSurfaceControl; + } + private void setScreenshotTransform(SurfaceControl.Transaction t) { if (mScreenshotLayer == null) { return; @@ -314,7 +333,11 @@ class ScreenRotationAnimation { } else { startDisplayRotation(animations, finishCallback, mainExecutor); startScreenshotRotationAnimation(animations, finishCallback, mainExecutor); - //startColorAnimation(mTransaction, animationScale); + if (mBackEffectSurface != null && mStartLuma > 0.1f) { + // Animate from the color of background to black for smooth alpha blending. + buildLumaAnimation(animations, mStartLuma, 0f /* endLuma */, mBackEffectSurface, + animationScale, finishCallback, mainExecutor); + } } return true; @@ -322,7 +345,7 @@ class ScreenRotationAnimation { private void startDisplayRotation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { - buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, + buildSurfaceAnimation(animations, mRotateEnterAnimation, getEnterSurface(), finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, null /* clipRect */, false /* isActivity */); } @@ -341,40 +364,17 @@ class ScreenRotationAnimation { null /* clipRect */, false /* isActivity */); } - private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { - int colorTransitionMs = mContext.getResources().getInteger( - R.integer.config_screen_rotation_color_transition); - final float[] rgbTmpFloat = new float[3]; - final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma); - final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma); - final long duration = colorTransitionMs * (long) animationScale; - final Transaction t = mTransactionPool.acquire(); - - final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); - // Animation length is already expected to be scaled. - va.overrideDurationScale(1.0f); - va.setDuration(duration); - va.addUpdateListener(animation -> { - final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); - final float fraction = currentPlayTime / va.getDuration(); - applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t); - }); - va.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, - t); - mTransactionPool.release(t); - } - - @Override - public void onAnimationEnd(Animator animation) { - applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, - t); - mTransactionPool.release(t); - } - }); - animExecutor.execute(va::start); + private void buildLumaAnimation(@NonNull ArrayList<Animator> animations, + float startLuma, float endLuma, SurfaceControl surface, float animationScale, + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { + final long durationMillis = (long) (mContext.getResources().getInteger( + R.integer.config_screen_rotation_color_transition) * animationScale); + final LumaAnimation animation = new LumaAnimation(durationMillis); + // Align the end with the enter animation. + animation.setStartOffset(mRotateEnterAnimation.getDuration() - durationMillis); + final LumaAnimationAdapter adapter = new LumaAnimationAdapter(surface, startLuma, endLuma); + DefaultSurfaceAnimator.buildSurfaceAnimation(animations, animation, finishCallback, + mTransactionPool, mainExecutor, adapter); } public void kill() { @@ -389,21 +389,47 @@ class ScreenRotationAnimation { if (mBackColorSurface != null && mBackColorSurface.isValid()) { t.remove(mBackColorSurface); } + if (mBackEffectSurface != null && mBackEffectSurface.isValid()) { + t.remove(mBackEffectSurface); + } t.apply(); mTransactionPool.release(t); } - private static void applyColor(int startColor, int endColor, float[] rgbFloat, - float fraction, SurfaceControl surface, SurfaceControl.Transaction t) { - final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor, - endColor); - Color middleColor = Color.valueOf(color); - rgbFloat[0] = middleColor.red(); - rgbFloat[1] = middleColor.green(); - rgbFloat[2] = middleColor.blue(); - if (surface.isValid()) { - t.setColor(surface, rgbFloat); + /** A no-op wrapper to provide animation duration. */ + private static class LumaAnimation extends Animation { + LumaAnimation(long durationMillis) { + setDuration(durationMillis); + } + } + + private static class LumaAnimationAdapter extends DefaultSurfaceAnimator.AnimationAdapter { + final float[] mColorArray = new float[3]; + final float mStartLuma; + final float mEndLuma; + final AccelerateInterpolator mInterpolation; + + LumaAnimationAdapter(@NonNull SurfaceControl leash, float startLuma, float endLuma) { + super(leash); + mStartLuma = startLuma; + mEndLuma = endLuma; + // Make the initial progress color lighter if the background is light. That avoids + // darker content when fading into the entering surface. + final float factor = Math.min(3f, (Math.max(0.5f, mStartLuma) - 0.5f) * 10); + Slog.d(TAG, "Luma=" + mStartLuma + " factor=" + factor); + mInterpolation = factor > 0.5f ? new AccelerateInterpolator(factor) : null; + } + + @Override + void applyTransformation(ValueAnimator animator, long currentPlayTime) { + final float fraction = mInterpolation != null + ? mInterpolation.getInterpolation(animator.getAnimatedFraction()) + : animator.getAnimatedFraction(); + final float luma = mStartLuma + fraction * (mEndLuma - mStartLuma); + mColorArray[0] = luma; + mColorArray[1] = luma; + mColorArray[2] = luma; + mTransaction.setColor(mLeash, mColorArray); } - t.apply(); } } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml index 40dbbac32c7f..c8df15d81345 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml index 85715db3d952..706c63244890 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml index 6c903a2e8c42..7df1675f541c 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml index 6c903a2e8c42..7df1675f541c 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml index f69a90cc793f..d87c1795cf7b 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index b76d06565700..99969e71238a 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml index 041978c371ff..19c3e4048d69 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml index bf040d2a95f4..7505860709e9 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="on"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="on"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index 3ed33db2222e..bb0fc41acd30 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -5,6 +5,11 @@ package com.google.android.appfunctions.sidecar { ctor public AppFunctionManager(android.content.Context); method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); + field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 + field public static final int APP_FUNCTION_STATE_DISABLED = 2; // 0x2 + field public static final int APP_FUNCTION_STATE_ENABLED = 1; // 0x1 } public abstract class AppFunctionService extends android.app.Service { @@ -41,6 +46,7 @@ package com.google.android.appfunctions.sidecar { field public static final String PROPERTY_RETURN_VALUE = "returnValue"; field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2 field public static final int RESULT_DENIED = 1; // 0x1 + field public static final int RESULT_DISABLED = 6; // 0x6 field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3 field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4 field public static final int RESULT_OK = 0; // 0x0 diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java index 815fe05cc3ab..d660926575d1 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -16,11 +16,18 @@ package com.google.android.appfunctions.sidecar; +import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.UserHandleAware; import android.content.Context; import android.os.CancellationSignal; +import android.os.OutcomeReceiver; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -37,6 +44,39 @@ import java.util.function.Consumer; // TODO(b/357551503): Implement get and set enabled app function APIs. // TODO(b/367329899): Add sidecar library to Android B builds. public final class AppFunctionManager { + /** + * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset + * enabled state to the default value. + */ + public static final int APP_FUNCTION_STATE_DEFAULT = 0; + + /** + * The app function is enabled. To enable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_ENABLED = 1; + + /** + * The app function is disabled. To disable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_DISABLED = 2; + + /** + * The enabled state of the app function. + * + * @hide + */ + @IntDef( + prefix = {"APP_FUNCTION_STATE_"}, + value = { + APP_FUNCTION_STATE_DEFAULT, + APP_FUNCTION_STATE_ENABLED, + APP_FUNCTION_STATE_DISABLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface EnabledState {} + private final android.app.appfunctions.AppFunctionManager mManager; private final Context mContext; @@ -111,4 +151,64 @@ public final class AppFunctionManager { new CancellationSignal(), callback); } + + /** + * Returns a boolean through a callback, indicating whether the app function is enabled. + * + * <p>* This method can only check app functions owned by the caller, or those where the caller + * has visibility to the owner package and holds either the {@link + * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link + * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission. + * + * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: + * + * <ul> + * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not + * have access to it. + * </ul> + * + * @param functionIdentifier the identifier of the app function to check (unique within the + * target package) and in most cases, these are automatically generated by the AppFunctions + * SDK + * @param targetPackage the package name of the app function's owner + * @param executor the executor to run the request + * @param callback the callback to receive the function enabled check result + */ + public void isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull String targetPackage, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Boolean, Exception> callback) { + mManager.isAppFunctionEnabled(functionIdentifier, targetPackage, executor, callback); + } + + /** + * Sets the enabled state of the app function owned by the calling package. + * + * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: + * + * <ul> + * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not + * have access to it. + * </ul> + * + * @param functionIdentifier the identifier of the app function to enable (unique within the + * calling package). In most cases, identifiers are automatically generated by the + * AppFunctions SDK + * @param newEnabledState the new state of the app function + * @param executor the executor to run the callback + * @param callback the callback to receive the result of the function enablement. The call was + * successful if no exception was thrown. + */ + // Constants in @EnabledState should always mirror those in + // android.app.appfunctions.AppFunctionManager. + @SuppressLint("WrongConstant") + @UserHandleAware + public void setAppFunctionEnabled( + @NonNull String functionIdentifier, + @EnabledState int newEnabledState, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Void, Exception> callback) { + mManager.setAppFunctionEnabled(functionIdentifier, newEnabledState, executor, callback); + } } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java index 60c25fae58d1..c7ce95bab7a5 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java @@ -76,6 +76,9 @@ public final class ExecuteAppFunctionResponse { /** The operation was timed out. */ public static final int RESULT_TIMED_OUT = 5; + /** The caller tried to execute a disabled app function. */ + public static final int RESULT_DISABLED = 6; + /** The result code of the app function execution. */ @ResultCode private final int mResultCode; @@ -234,6 +237,7 @@ public final class ExecuteAppFunctionResponse { RESULT_INTERNAL_ERROR, RESULT_INVALID_ARGUMENT, RESULT_TIMED_OUT, + RESULT_DISABLED }) @Retention(RetentionPolicy.SOURCE) public @interface ResultCode {} diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index bbb142014ed8..f0bcfe537998 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -36,7 +36,7 @@ namespace android { MinikinFontSkia::MinikinFontSkia(sk_sp<SkTypeface> typeface, int sourceId, const void* fontData, size_t fontSize, std::string_view filePath, int ttcIndex, - const std::vector<minikin::FontVariation>& axes) + const minikin::VariationSettings& axes) : mTypeface(std::move(typeface)) , mSourceId(sourceId) , mFontData(fontData) @@ -123,12 +123,12 @@ int MinikinFontSkia::GetFontIndex() const { return mTtcIndex; } -const std::vector<minikin::FontVariation>& MinikinFontSkia::GetAxes() const { +const minikin::VariationSettings& MinikinFontSkia::GetAxes() const { return mAxes; } std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( - const std::vector<minikin::FontVariation>& variations) const { + const minikin::VariationSettings& variations) const { SkFontArguments args; std::vector<SkFontArguments::VariationPosition::Coordinate> skVariation; diff --git a/libs/hwui/hwui/MinikinSkia.h b/libs/hwui/hwui/MinikinSkia.h index de9a5c2af0aa..7fe5978bfda3 100644 --- a/libs/hwui/hwui/MinikinSkia.h +++ b/libs/hwui/hwui/MinikinSkia.h @@ -32,7 +32,7 @@ class ANDROID_API MinikinFontSkia : public minikin::MinikinFont { public: MinikinFontSkia(sk_sp<SkTypeface> typeface, int sourceId, const void* fontData, size_t fontSize, std::string_view filePath, int ttcIndex, - const std::vector<minikin::FontVariation>& axes); + const minikin::VariationSettings& axes); float GetHorizontalAdvance(uint32_t glyph_id, const minikin::MinikinPaint& paint, const minikin::FontFakery& fakery) const override; @@ -59,9 +59,9 @@ public: size_t GetFontSize() const; int GetFontIndex() const; const std::string& getFilePath() const { return mFilePath; } - const std::vector<minikin::FontVariation>& GetAxes() const; + const minikin::VariationSettings& GetAxes() const; std::shared_ptr<minikin::MinikinFont> createFontWithVariation( - const std::vector<minikin::FontVariation>&) const; + const minikin::VariationSettings&) const; int GetSourceId() const override { return mSourceId; } static uint32_t packFontFlags(const SkFont&); @@ -80,7 +80,7 @@ private: const void* mFontData; size_t mFontSize; int mTtcIndex; - std::vector<minikin::FontVariation> mAxes; + minikin::VariationSettings mAxes; std::string mFilePath; }; diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index a9d1a2aed8cc..2d812d675fdc 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -92,8 +92,8 @@ Typeface* Typeface::createAbsolute(Typeface* base, int weight, bool italic) { return result; } -Typeface* Typeface::createFromTypefaceWithVariation( - Typeface* src, const std::vector<minikin::FontVariation>& variations) { +Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src, + const minikin::VariationSettings& variations) { const Typeface* resolvedFace = Typeface::resolveDefault(src); Typeface* result = new Typeface(); if (result != nullptr) { @@ -192,9 +192,8 @@ void Typeface::setRobotoTypefaceForTest() { sk_sp<SkTypeface> typeface = fm->makeFromStream(std::move(fontData)); LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", kRobotoFont); - std::shared_ptr<minikin::MinikinFont> font = - std::make_shared<MinikinFontSkia>(std::move(typeface), 0, data, st.st_size, kRobotoFont, - 0, std::vector<minikin::FontVariation>()); + std::shared_ptr<minikin::MinikinFont> font = std::make_shared<MinikinFontSkia>( + std::move(typeface), 0, data, st.st_size, kRobotoFont, 0, minikin::VariationSettings()); std::vector<std::shared_ptr<minikin::Font>> fonts; fonts.push_back(minikin::Font::Builder(font).build()); diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h index 565136e53676..2c96c1ad80fe 100644 --- a/libs/hwui/hwui/Typeface.h +++ b/libs/hwui/hwui/Typeface.h @@ -74,8 +74,8 @@ public: static Typeface* createRelative(Typeface* src, Style desiredStyle); static Typeface* createAbsolute(Typeface* base, int weight, bool italic); - static Typeface* createFromTypefaceWithVariation( - Typeface* src, const std::vector<minikin::FontVariation>& variations); + static Typeface* createFromTypefaceWithVariation(Typeface* src, + const minikin::VariationSettings& variations); static Typeface* createFromFamilies( std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic, diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp index e6d790f56d0f..9922ff393e55 100644 --- a/libs/hwui/jni/FontFamily.cpp +++ b/libs/hwui/jni/FontFamily.cpp @@ -133,9 +133,9 @@ static bool addSkTypeface(NativeFamilyBuilder* builder, sk_sp<SkData>&& data, in builder->axes.clear(); return false; } - std::shared_ptr<minikin::MinikinFont> minikinFont = - std::make_shared<MinikinFontSkia>(std::move(face), fonts::getNewSourceId(), fontPtr, - fontSize, "", ttcIndex, builder->axes); + std::shared_ptr<minikin::MinikinFont> minikinFont = std::make_shared<MinikinFontSkia>( + std::move(face), fonts::getNewSourceId(), fontPtr, fontSize, "", ttcIndex, + minikin::VariationSettings(builder->axes, false)); minikin::Font::Builder fontBuilder(minikinFont); if (weight != RESOLVE_BY_FONT_TABLE) { diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index 209b35c5537c..0f458dde8b07 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -80,7 +80,8 @@ static jlong Typeface_createFromTypefaceWithVariation(JNIEnv* env, jobject, jlon AxisHelper axis(env, axisObject); variations.push_back(minikin::FontVariation(axis.getTag(), axis.getStyleValue())); } - return toJLong(Typeface::createFromTypefaceWithVariation(toTypeface(familyHandle), variations)); + return toJLong(Typeface::createFromTypefaceWithVariation( + toTypeface(familyHandle), minikin::VariationSettings(variations, false /* sorted */))); } static jlong Typeface_createWeightAlias(JNIEnv* env, jobject, jlong familyHandle, jint weight) { @@ -273,7 +274,7 @@ void MinikinFontSkiaFactory::write(minikin::BufferWriter* writer, const std::string& path = typeface->GetFontPath(); writer->writeString(path); writer->write<int>(typeface->GetFontIndex()); - const std::vector<minikin::FontVariation>& axes = typeface->GetAxes(); + const minikin::VariationSettings& axes = typeface->GetAxes(); writer->writeArray<minikin::FontVariation>(axes.data(), axes.size()); bool hasVerity = getVerity(path); writer->write<int8_t>(static_cast<int8_t>(hasVerity)); diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index f405abaaf5b4..6a05b6c2626c 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -142,7 +142,7 @@ static jlong Font_Builder_clone(JNIEnv* env, jobject clazz, jlong fontPtr, jlong std::shared_ptr<minikin::MinikinFont> newMinikinFont = std::make_shared<MinikinFontSkia>( std::move(newTypeface), minikinSkia->GetSourceId(), minikinSkia->GetFontData(), minikinSkia->GetFontSize(), minikinSkia->getFilePath(), minikinSkia->GetFontIndex(), - builder->axes); + minikin::VariationSettings(builder->axes, false)); std::shared_ptr<minikin::Font> newFont = minikin::Font::Builder(newMinikinFont) .setWeight(weight) .setSlant(static_cast<minikin::FontStyle::Slant>(italic)) @@ -303,7 +303,7 @@ static jlong Font_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr, jint inde var = reader.readArray<minikin::FontVariation>().first[index]; } else { const std::shared_ptr<minikin::MinikinFont>& minikinFont = font->font->baseTypeface(); - var = minikinFont->GetAxes().at(index); + var = minikinFont->GetAxes()[index]; } uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value); return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary); diff --git a/native/android/system_fonts.cpp b/native/android/system_fonts.cpp index 91f78ce6f950..0c07b2acbb0c 100644 --- a/native/android/system_fonts.cpp +++ b/native/android/system_fonts.cpp @@ -327,7 +327,7 @@ AFont* _Nonnull AFontMatcher_match( result->mWeight = font->style().weight(); result->mItalic = font->style().slant() == minikin::FontStyle::Slant::ITALIC; result->mCollectionIndex = minikinFontSkia->GetFontIndex(); - const std::vector<minikin::FontVariation>& axes = minikinFontSkia->GetAxes(); + const minikin::VariationSettings& axes = minikinFontSkia->GetAxes(); result->mAxes.reserve(axes.size()); for (auto axis : axes) { result->mAxes.push_back(std::make_pair(axis.axisTag, axis.value)); diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index af07686f064c..06f471e91d1d 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -42,6 +42,7 @@ android_library { "SettingsLibFooterPreference", "SettingsLibHelpUtils", "SettingsLibIllustrationPreference", + "SettingsLibIntroPreference", "SettingsLibLayoutPreference", "SettingsLibMainSwitchPreference", "SettingsLibProfileSelector", diff --git a/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml b/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml index 47ce58735048..b06052ad6a00 100644 --- a/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml +++ b/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml @@ -78,15 +78,6 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?android:attr/textColorSecondary" android:visibility="gone"/> - - <ProgressBar - android:id="@android:id/progress" - style="?android:attr/progressBarStyleHorizontal" - android:layout_width="match_parent" - android:layout_height="4dp" - android:layout_marginTop="4dp" - android:max="100" - android:visibility="gone"/> </LinearLayout> <LinearLayout diff --git a/packages/SettingsLib/AppPreference/res/layout/preference_app.xml b/packages/SettingsLib/AppPreference/res/layout/preference_app.xml index e65f7de2466a..ac572280c5ad 100644 --- a/packages/SettingsLib/AppPreference/res/layout/preference_app.xml +++ b/packages/SettingsLib/AppPreference/res/layout/preference_app.xml @@ -74,15 +74,6 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?android:attr/textColorSecondary" android:visibility="gone"/> - - <ProgressBar - android:id="@android:id/progress" - style="?android:attr/progressBarStyleHorizontal" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="4dp" - android:max="100" - android:visibility="gone"/> </LinearLayout> <LinearLayout diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java index f1d162e116b5..3b52df7e5fbb 100644 --- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java +++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java @@ -18,11 +18,8 @@ package com.android.settingslib.widget; import android.content.Context; import android.util.AttributeSet; -import android.view.View; -import android.widget.ProgressBar; import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import com.android.settingslib.widget.preference.app.R; @@ -31,9 +28,6 @@ import com.android.settingslib.widget.preference.app.R; */ public class AppPreference extends Preference { - private int mProgress; - private boolean mProgressVisible; - public AppPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setLayoutResource(R.layout.preference_app); @@ -53,29 +47,4 @@ public class AppPreference extends Preference { super(context, attrs); setLayoutResource(R.layout.preference_app); } - - /** - * Sets the current progress. - * @param amount the current progress - * - * @see ProgressBar#setProgress(int) - */ - public void setProgress(int amount) { - mProgress = amount; - mProgressVisible = true; - notifyChanged(); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - - final ProgressBar progress = (ProgressBar) view.findViewById(android.R.id.progress); - if (mProgressVisible) { - progress.setProgress(mProgress); - progress.setVisibility(View.VISIBLE); - } else { - progress.setVisibility(View.GONE); - } - } } diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml new file mode 100644 index 000000000000..f55b320269a8 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Filled" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml new file mode 100644 index 000000000000..b663b6ccc5bf --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Filled.Extra" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml new file mode 100644 index 000000000000..784e6ad6a9f8 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Filled.Large" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml new file mode 100644 index 000000000000..8b44a6539801 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Outline" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml new file mode 100644 index 000000000000..f8a2d8fbd975 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Outline.Extra" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml new file mode 100644 index 000000000000..781a5a136164 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Outline.Large" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml new file mode 100644 index 000000000000..5b568f870ea4 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Tonal" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml new file mode 100644 index 000000000000..1e7a08b714f1 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Tonal.Extra" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml new file mode 100644 index 000000000000..42116be07041 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Tonal.Large" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml b/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml new file mode 100644 index 000000000000..a1761e55f1e0 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <declare-styleable name="ButtonPreference"> + <attr name="buttonType" format="enum"> + <enum name="filled" value="0"/> + <enum name="tonal" value="1"/> + <enum name="outline" value="2"/> + </attr> + <attr name="buttonSize" format="enum"> + <enum name="normal" value="0"/> + <enum name="large" value="1"/> + <enum name="extra" value="2"/> + </attr> + </declare-styleable> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java index 16ba96265751..0041eb2c7072 100644 --- a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java +++ b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java @@ -32,11 +32,44 @@ import androidx.preference.PreferenceViewHolder; import com.android.settingslib.widget.preference.button.R; +import com.google.android.material.button.MaterialButton; + /** * A preference handled a button */ public class ButtonPreference extends Preference { + enum ButtonStyle { + FILLED_NORMAL(0, 0, R.layout.settingslib_expressive_button_filled), + FILLED_LARGE(0, 1, R.layout.settingslib_expressive_button_filled_large), + FILLED_EXTRA(0, 2, R.layout.settingslib_expressive_button_filled_extra), + TONAL_NORMAL(1, 0, R.layout.settingslib_expressive_button_tonal), + TONAL_LARGE(1, 1, R.layout.settingslib_expressive_button_tonal_large), + TONAL_EXTRA(1, 2, R.layout.settingslib_expressive_button_tonal_extra), + OUTLINE_NORMAL(2, 0, R.layout.settingslib_expressive_button_outline), + OUTLINE_LARGE(2, 1, R.layout.settingslib_expressive_button_outline_large), + OUTLINE_EXTRA(2, 2, R.layout.settingslib_expressive_button_outline_extra); + + private final int mType; + private final int mSize; + private final int mLayoutId; + + ButtonStyle(int type, int size, int layoutId) { + this.mType = type; + this.mSize = size; + this.mLayoutId = layoutId; + } + + static int getLayoutId(int type, int size) { + for (ButtonStyle style : values()) { + if (style.mType == type && style.mSize == size) { + return style.mLayoutId; + } + } + throw new IllegalArgumentException(); + } + } + private static final int ICON_SIZE = 24; private View.OnClickListener mClickListener; @@ -86,7 +119,7 @@ public class ButtonPreference extends Preference { } private void init(Context context, AttributeSet attrs, int defStyleAttr) { - setLayoutResource(R.layout.settingslib_button_layout); + int resId = R.layout.settingslib_button_layout; if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, @@ -102,8 +135,16 @@ public class ButtonPreference extends Preference { R.styleable.ButtonPreference, defStyleAttr, 0 /*defStyleRes*/); mGravity = a.getInt(R.styleable.ButtonPreference_android_gravity, Gravity.START); + + if (SettingsThemeHelper.isExpressiveTheme(context)) { + int type = a.getInt(R.styleable.ButtonPreference_buttonType, 0); + int size = a.getInt(R.styleable.ButtonPreference_buttonSize, 0); + resId = ButtonStyle.getLayoutId(type, size); + } a.recycle(); } + + setLayoutResource(resId); } @Override @@ -144,14 +185,20 @@ public class ButtonPreference extends Preference { if (mButton == null || icon == null) { return; } - //get pixel from dp - int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, ICON_SIZE, - getContext().getResources().getDisplayMetrics()); - icon.setBounds(0, 0, size, size); - - //set drawableStart - mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null/* top */, null/* end */, - null/* bottom */); + + if (mButton instanceof MaterialButton) { + ((MaterialButton) mButton).setIcon(icon); + } else { + //get pixel from dp + int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, ICON_SIZE, + getContext().getResources().getDisplayMetrics()); + icon.setBounds(0, 0, size, size); + + //set drawableStart + mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null/* top */, + null/* end */, + null/* bottom */); + } } @Override diff --git a/packages/SettingsLib/IntroPreference/Android.bp b/packages/SettingsLib/IntroPreference/Android.bp new file mode 100644 index 000000000000..155db186c702 --- /dev/null +++ b/packages/SettingsLib/IntroPreference/Android.bp @@ -0,0 +1,33 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SettingsLibIntroPreference", + use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + resource_dirs: ["res"], + + static_libs: [ + "androidx.preference_preference", + "SettingsLibSettingsTheme", + ], + + sdk_version: "system_current", + min_sdk_version: "21", + apex_available: [ + "//apex_available:platform", + ], +} diff --git a/packages/SettingsLib/IntroPreference/AndroidManifest.xml b/packages/SettingsLib/IntroPreference/AndroidManifest.xml new file mode 100644 index 000000000000..f1bfee5524e7 --- /dev/null +++ b/packages/SettingsLib/IntroPreference/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.widget.preference.intro"> + + <uses-sdk android:minSdkVersion="21" /> + +</manifest> diff --git a/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml new file mode 100644 index 000000000000..203a395c3e98 --- /dev/null +++ b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/entity_header" + style="@style/SettingsLibEntityHeader"> + + <LinearLayout + android:id="@+id/entity_header_content" + style="@style/SettingsLibEntityHeaderContent"> + + <ImageView + android:id="@android:id/icon" + android:src="@drawable/settingslib_arrow_drop_down" + style="@style/SettingsLibEntityHeaderIcon"/> + + <TextView + android:id="@android:id/title" + android:text="Title" + style="@style/SettingsLibEntityHeaderTitle"/> + + <com.android.settingslib.widget.CollapsableTextView + android:id="@+id/collapsable_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"/> + + </LinearLayout> + +</RelativeLayout> diff --git a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt new file mode 100644 index 000000000000..c93ec2bd0492 --- /dev/null +++ b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import androidx.annotation.RequiresApi +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.android.settingslib.widget.preference.intro.R + +class IntroPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : Preference(context, attrs, defStyleAttr, defStyleRes) { + + private var isCollapsable: Boolean = false + private var minLines: Int = 2 + + init { + layoutResource = R.layout.settingslib_expressive_preference_intro + isSelectable = false + + initAttributes(context, attrs, defStyleAttr) + } + + private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + context.obtainStyledAttributes( + attrs, + COLLAPSABLE_TEXT_VIEW_ATTRS, defStyleAttr, 0 + ).apply { + isCollapsable = getBoolean(IS_COLLAPSABLE, false) + minLines = getInt( + MIN_LINES, + if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES + ).coerceIn(1, DEFAULT_MAX_LINES) + recycle() + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedBelow = false + holder.isDividerAllowedAbove = false + + (holder.findViewById(R.id.collapsable_summary) as? CollapsableTextView)?.apply { + setCollapsable(isCollapsable) + setMinLines(minLines) + setText(summary.toString()) + } + } + + /** + * Sets whether the summary is collapsable. + * @param collapsable True if the summary should be collapsable, false otherwise. + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + fun setCollapsable(collapsable: Boolean) { + isCollapsable = collapsable + minLines = if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES + notifyChanged() + } + + /** + * Sets the minimum number of lines to display when collapsed. + * @param lines The minimum number of lines. + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + fun setMinLines(lines: Int) { + minLines = lines.coerceIn(1, DEFAULT_MAX_LINES) + notifyChanged() + } + + companion object { + private const val DEFAULT_MAX_LINES = 10 + private const val DEFAULT_MIN_LINES = 2 + + private val COLLAPSABLE_TEXT_VIEW_ATTRS = + com.android.settingslib.widget.theme.R.styleable.CollapsableTextView + private val MIN_LINES = + com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_android_minLines + private val IS_COLLAPSABLE = + com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_isCollapsable + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp index 17852e8e7ece..e83e17cc8375 100644 --- a/packages/SettingsLib/Preference/Android.bp +++ b/packages/SettingsLib/Preference/Android.bp @@ -22,3 +22,16 @@ android_library { ], kotlincflags: ["-Xjvm-default=all"], } + +android_library { + name: "SettingsLibPreference-testutils", + srcs: ["testutils/**/*.kt"], + static_libs: [ + "SettingsLibPreference", + "androidx.fragment_fragment-testing", + "androidx.test.core", + "androidx.test.ext.junit", + "flag-junit", + "truth", + ], +} diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt index 5fcf4784f43b..5e6989546cb9 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt @@ -68,10 +68,13 @@ interface PreferenceBinding { preference.icon = null } val context = preference.context + val isPreferenceScreen = preference is PreferenceScreen preference.peekExtras()?.clear() extras(context)?.let { preference.extras.putAll(it) } preference.title = getPreferenceTitle(context) - preference.summary = getPreferenceSummary(context) + if (!isPreferenceScreen) { + preference.summary = getPreferenceSummary(context) + } preference.isEnabled = isEnabled(context) preference.isVisible = (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false @@ -81,7 +84,7 @@ interface PreferenceBinding { // dependency here. This simplifies dependency management and avoid the // IllegalStateException when call Preference.setDependency preference.dependency = null - if (preference !is PreferenceScreen) { // avoid recursive loop when build graph + if (!isPreferenceScreen) { // avoid recursive loop when build graph preference.fragment = (this as? PreferenceScreenCreator)?.fragmentClass()?.name preference.intent = intent(context) } diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt new file mode 100644 index 000000000000..4d5f85fa9020 --- /dev/null +++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import android.content.Context +import android.platform.test.flag.junit.SetFlagsRule +import android.util.Log +import androidx.fragment.app.testing.FragmentScenario +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceGroup +import androidx.preference.PreferenceScreen +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** Test case for catalyst screen. */ +@RunWith(AndroidJUnit4::class) +abstract class CatalystScreenTestCase { + @get:Rule val setFlagsRule = SetFlagsRule() + + protected val context: Context = ApplicationProvider.getApplicationContext() + + /** Catalyst screen. */ + protected abstract val preferenceScreenCreator: PreferenceScreenCreator + + /** Flag to control catalyst screen. */ + protected abstract val flagName: String + + /** + * Test to compare the preference screen hierarchy between legacy screen (flag is disabled) and + * catalyst screen (flag is enabled). + */ + @Test + fun migration() { + enableCatalystScreen() + assertThat(preferenceScreenCreator.isFlagEnabled(context)).isTrue() + val catalystScreen = stringifyPreferenceScreen() + Log.i("Catalyst", catalystScreen) + + disableCatalystScreen() + assertThat(preferenceScreenCreator.isFlagEnabled(context)).isFalse() + val legacyScreen = stringifyPreferenceScreen() + + assertThat(catalystScreen).isEqualTo(legacyScreen) + } + + /** + * Enables the catalyst screen. + * + * By default, enable the [flagName]. Override for more complex situation. + */ + @Suppress("DEPRECATION") + protected open fun enableCatalystScreen() { + setFlagsRule.enableFlags(flagName) + } + + /** + * Disables the catalyst screen (legacy screen is shown). + * + * By default, disable the [flagName]. Override for more complex situation. + */ + @Suppress("DEPRECATION") + protected open fun disableCatalystScreen() { + setFlagsRule.disableFlags(flagName) + } + + private fun stringifyPreferenceScreen(): String { + @Suppress("UNCHECKED_CAST") + val clazz = preferenceScreenCreator.fragmentClass() as Class<PreferenceFragmentCompat> + val builder = StringBuilder() + FragmentScenario.launch(clazz).use { + it.onFragment { fragment -> fragment.preferenceScreen.toString(builder) } + } + return builder.toString() + } + + private fun Preference.toString(builder: StringBuilder, indent: String = "") { + val clazz = javaClass + builder.append(indent).append(clazz).append(" {\n") + val indent2 = "$indent " + if (clazz != PreferenceScreen::class.java) { + key?.let { builder.append(indent2).append("key: \"$it\"\n") } + } + title?.let { builder.append(indent2).append("title: \"$it\"\n") } + summary?.let { builder.append(indent2).append("summary: \"$it\"\n") } + fragment?.let { builder.append(indent2).append("fragment: \"$it\"\n") } + builder.append(indent2).append("order: $order\n") + builder.append(indent2).append("isCopyingEnabled: $isCopyingEnabled\n") + builder.append(indent2).append("isEnabled: $isEnabled\n") + builder.append(indent2).append("isIconSpaceReserved: $isIconSpaceReserved\n") + if (clazz != Preference::class.java && clazz != PreferenceScreen::class.java) { + builder.append(indent2).append("isPersistent: $isPersistent\n") + } + builder.append(indent2).append("isSelectable: $isSelectable\n") + if (this is PreferenceGroup) { + val count = preferenceCount + builder.append(indent2).append("preferenceCount: $count\n") + val indent4 = "$indent2 " + for (index in 0..<count) { + getPreference(index).toString(builder, indent4) + } + } + builder.append(indent).append("}\n") + } +} diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml index 250c27e8b581..9c659050b15e 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml @@ -290,4 +290,43 @@ <item name="iconPadding">@dimen/settingslib_expressive_space_extrasmall4</item> <item name="rippleColor">?android:attr/colorControlHighlight</item> </style> + + <style name="EntityHeader"> + <item name="android:paddingTop">@dimen/settingslib_expressive_space_small4</item> + <item name="android:paddingBottom">@dimen/settingslib_expressive_space_small1</item> + <item name="android:paddingEnd">@dimen/settingslib_expressive_space_small1</item> + </style> + + <style name="SettingsLibEntityHeader" parent="EntityHeader"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:paddingStart">?android:attr/listPreferredItemPaddingStart</item> + <item name="android:paddingEnd">?android:attr/listPreferredItemPaddingEnd</item> + </style> + + <style name="SettingsLibEntityHeaderContent"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_centerHorizontal">true</item> + <item name="android:orientation">vertical</item> + <item name="android:gravity">center_horizontal</item> + </style> + + <style name="SettingsLibEntityHeaderIcon"> + <item name="android:layout_width">@dimen/settingslib_expressive_space_large3</item> + <item name="android:layout_height">@dimen/settingslib_expressive_space_large3</item> + <item name="android:scaleType">fitCenter</item> + <item name="android:antialias">true</item> + </style> + + <style name="SettingsLibEntityHeaderTitle"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginTop">@dimen/settingslib_expressive_space_small1</item> + <item name="android:singleLine">false</item> + <item name="android:gravity">center</item> + <item name="android:ellipsize">marquee</item> + <item name="android:textDirection">locale</item> + <item name="android:textAppearance">@style/TextAppearance.EntityHeaderTitle</item> + </style> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 7139f5b468ca..2a251a59e1d8 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -30,9 +30,6 @@ import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider import com.android.settingslib.spa.gallery.editor.SettingsDropdownBoxPageProvider import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider import com.android.settingslib.spa.gallery.home.HomePageProvider -import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider -import com.android.settingslib.spa.gallery.itemList.ItemOperatePageProvider -import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider import com.android.settingslib.spa.gallery.editor.SettingsOutlinedTextFieldPageProvider import com.android.settingslib.spa.gallery.editor.SettingsTextFieldPasswordPageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageProvider @@ -66,10 +63,6 @@ import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider */ enum class SettingsPageProviderEnum(val displayName: String) { HOME("home"), - PREFERENCE("preference"), - ARGUMENT("argument"), - ITEM_LIST("itemList"), - ITEM_OP_PAGE("itemOp"), // Add your SPPs } @@ -101,9 +94,6 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { ChartPageProvider, DialogMainPageProvider, NavDialogProvider, - ItemListPageProvider, - ItemOperatePageProvider, - OperateListPageProvider, EditorMainPageProvider, SettingsOutlinedTextFieldPageProvider, SettingsDropdownBoxPageProvider, diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt index 6edd9173d7e5..c16d8bfde23e 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt @@ -39,9 +39,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme @@ -161,14 +159,12 @@ object BannerPageProvider : SettingsPageProvider { } } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } private const val TITLE = "Sample Banner" diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt index b001caddd000..773d3d121a1d 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt @@ -23,9 +23,7 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.WarningAmber import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.button.ActionButton @@ -55,14 +53,12 @@ object ActionButtonPageProvider : SettingsPageProvider { } } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt index 7a6ae2cee6ad..6ceb395272c4 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt @@ -39,6 +39,7 @@ import java.text.NumberFormat private enum class WeekDay(val num: Int) { Sun(0), Mon(1), Tue(2), Wed(3), Thu(4), Fri(5), Sat(6), } + private const val TITLE = "Sample Chart" object ChartPageProvider : SettingsPageProvider { @@ -103,14 +104,12 @@ object ChartPageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt index 4e3fcee5383e..c9c81aac01c3 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt @@ -18,6 +18,7 @@ package com.android.settingslib.spa.gallery.dialog import android.os.Bundle import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider @@ -55,13 +56,13 @@ object DialogMainPageProvider : SettingsPageProvider { }.build(), ) - fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } override fun getTitle(arguments: Bundle?) = TITLE } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt index c511542f265a..f2b4091c7d85 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt @@ -17,8 +17,8 @@ package com.android.settingslib.spa.gallery.editor import android.os.Bundle +import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator @@ -44,14 +44,12 @@ object EditorMainPageProvider : SettingsPageProvider { ) } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt index b1558cce718a..4d77ea173a85 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt @@ -20,20 +20,16 @@ import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider import com.android.settingslib.spa.gallery.banner.BannerPageProvider +import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider import com.android.settingslib.spa.gallery.chart.ChartPageProvider import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider -import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider -import com.android.settingslib.spa.gallery.page.ArgumentPageModel import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider import com.android.settingslib.spa.gallery.page.IllustrationPageProvider @@ -48,35 +44,11 @@ import com.android.settingslib.spa.gallery.ui.CategoryPageProvider import com.android.settingslib.spa.gallery.ui.CopyablePageProvider import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider import com.android.settingslib.spa.widget.scaffold.HomeScaffold +import com.android.settingslib.spa.widget.ui.Category object HomePageProvider : SettingsPageProvider { override val name = SettingsPageProviderEnum.HOME.name override val displayName = SettingsPageProviderEnum.HOME.displayName - private val owner = createSettingsPage() - - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - return listOf( - PreferenceMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - OperateListPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ArgumentPageProvider.buildInjectEntry("foo")!!.setLink(fromPage = owner).build(), - SearchScaffoldPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - SuwScaffoldPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - SliderPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - SpinnerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - PagerMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - FooterPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - LoadingBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - DialogMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - BannerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - CopyablePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ) - } override fun getTitle(arguments: Bundle?): String { return SpaEnvironmentFactory.instance.appContext.getString(R.string.app_name) @@ -85,14 +57,30 @@ object HomePageProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { val title = remember { getTitle(arguments) } - val entries = remember { buildEntry(arguments) } HomeScaffold(title) { - for (entry in entries) { - if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) { - entry.UiLayout(ArgumentPageModel.buildArgument(intParam = 0)) - } else { - entry.UiLayout() - } + Category { + PreferenceMainPageProvider.Entry() + } + Category { + SearchScaffoldPageProvider.Entry() + SuwScaffoldPageProvider.Entry() + ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0) + } + Category { + SliderPageProvider.Entry() + SpinnerPageProvider.Entry() + PagerMainPageProvider.Entry() + FooterPageProvider.Entry() + IllustrationPageProvider.Entry() + CategoryPageProvider.Entry() + ActionButtonPageProvider.Entry() + ProgressBarPageProvider.Entry() + LoadingBarPageProvider.Entry() + ChartPageProvider.Entry() + DialogMainPageProvider.Entry() + EditorMainPageProvider.Entry() + BannerPageProvider.Entry() + CopyablePageProvider.Entry() } } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt deleted file mode 100644 index 5f251b1b14dd..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.gallery.itemList - -import android.os.Bundle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.core.os.bundleOf -import androidx.navigation.NavType -import androidx.navigation.navArgument -import com.android.settingslib.spa.framework.common.EntrySearchData -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder -import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.util.getStringArg -import com.android.settingslib.spa.framework.util.navLink -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.widget.preference.Preference -import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.scaffold.RegularScaffold - -private const val OPERATOR_PARAM_NAME = "opParam" - -object ItemListPageProvider : SettingsPageProvider { - override val name = SettingsPageProviderEnum.ITEM_LIST.name - override val displayName = SettingsPageProviderEnum.ITEM_LIST.displayName - override val parameter = listOf( - navArgument(OPERATOR_PARAM_NAME) { type = NavType.StringType }, - ) - - override fun getTitle(arguments: Bundle?): String { - val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) ?: "NULL" - return "Operation: $operation" - } - - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - if (!ItemOperatePageProvider.isValidArgs(arguments)) return emptyList() - val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)!! - val owner = createSettingsPage(arguments) - return listOf( - ItemOperatePageProvider.buildInjectEntry(operation)!!.setLink(fromPage = owner).build(), - ) - } - - fun buildInjectEntry(opParam: String): SettingsEntryBuilder? { - val arguments = bundleOf(OPERATOR_PARAM_NAME to opParam) - if (!ItemOperatePageProvider.isValidArgs(arguments)) return null - - return SettingsEntryBuilder.createInject( - owner = createSettingsPage(arguments), - label = "ItemList_$opParam", - ).setUiLayoutFn { - Preference( - object : PreferenceModel { - override val title = opParam - override val onClick = navigator( - SettingsPageProviderEnum.ITEM_LIST.name + parameter.navLink(it) - ) - } - ) - }.setSearchDataFn { - EntrySearchData(title = "Operation: $opParam") - } - } - - @Composable - override fun Page(arguments: Bundle?) { - val title = remember { getTitle(arguments) } - val entries = remember { buildEntry(arguments) } - val itemList = remember { - // Add logic to get item List during runtime. - listOf("itemFoo", "itemBar", "itemToy") - } - RegularScaffold(title) { - for (item in itemList) { - val rtArgs = ItemOperatePageProvider.genRuntimeArguments(item) - for (entry in entries) { - entry.UiLayout(rtArgs) - } - } - } - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt deleted file mode 100644 index 6caec07371f5..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.gallery.itemList - -import android.os.Bundle -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.core.os.bundleOf -import androidx.navigation.NavType -import androidx.navigation.navArgument -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder -import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.util.getStringArg -import com.android.settingslib.spa.framework.util.navLink -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.widget.preference.Preference -import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.preference.SwitchPreference -import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel - -private const val OPERATOR_PARAM_NAME = "opParam" -private const val ITEM_NAME_PARAM_NAME = "rt_nameParam" -private val ALLOWED_OPERATOR_LIST = listOf("opDnD", "opPiP", "opInstall", "opConnect") - -object ItemOperatePageProvider : SettingsPageProvider { - override val name = SettingsPageProviderEnum.ITEM_OP_PAGE.name - override val displayName = SettingsPageProviderEnum.ITEM_OP_PAGE.displayName - override val parameter = listOf( - navArgument(OPERATOR_PARAM_NAME) { type = NavType.StringType }, - navArgument(ITEM_NAME_PARAM_NAME) { type = NavType.StringType }, - ) - - override fun getTitle(arguments: Bundle?): String { - // Operation name is not a runtime parameter, which should always available - val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) ?: "opInValid" - // Item name is a runtime parameter, which could be missing - val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, arguments) ?: "[unset]" - return "$operation on $itemName" - } - - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - if (!isValidArgs(arguments)) return emptyList() - - val owner = createSettingsPage(arguments) - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - SettingsEntryBuilder.create("ItemName", owner) - .setUiLayoutFn { - // Item name is a runtime parameter, which needs to be read inside UiLayoutFn - val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, it) ?: "NULL" - Preference( - object : PreferenceModel { - override val title = "Item $itemName" - } - ) - }.build() - ) - - // Operation name is not a runtime parameter, which can be read outside. - val opName = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)!! - entryList.add( - SettingsEntryBuilder.create("ItemOp", owner) - .setUiLayoutFn { - var checked by rememberSaveable { mutableStateOf(false) } - SwitchPreference(remember { - object : SwitchPreferenceModel { - override val title = "Item operation: $opName" - override val checked = { checked } - override val onCheckedChange = - { newChecked: Boolean -> checked = newChecked } - } - }) - }.build(), - ) - return entryList - } - - fun buildInjectEntry(opParam: String): SettingsEntryBuilder? { - val arguments = bundleOf(OPERATOR_PARAM_NAME to opParam) - if (!isValidArgs(arguments)) return null - - return SettingsEntryBuilder.createInject( - owner = createSettingsPage(arguments), - label = "ItemOp_$opParam", - ).setUiLayoutFn { - // Item name is a runtime parameter, which needs to be read inside UiLayoutFn - val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, it) ?: "NULL" - Preference( - object : PreferenceModel { - override val title = "item: $itemName" - override val onClick = navigator( - SettingsPageProviderEnum.ITEM_OP_PAGE.name + parameter.navLink(it) - ) - } - ) - } - } - - fun isValidArgs(arguments: Bundle?): Boolean { - val opParam = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) - return (opParam != null && ALLOWED_OPERATOR_LIST.contains(opParam)) - } - - fun genRuntimeArguments(itemName: String): Bundle { - return bundleOf(ITEM_NAME_PARAM_NAME to itemName) - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt deleted file mode 100644 index e0baf86119a3..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.gallery.itemList - -import android.os.Bundle -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder -import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.widget.preference.Preference -import com.android.settingslib.spa.widget.preference.PreferenceModel - -private const val TITLE = "Operate List Main" - -object OperateListPageProvider : SettingsPageProvider { - override val name = "OpList" - private val owner = createSettingsPage() - - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - return listOf( - ItemListPageProvider.buildInjectEntry("opPiP")!!.setLink(fromPage = owner).build(), - ItemListPageProvider.buildInjectEntry("opInstall")!!.setLink(fromPage = owner).build(), - ItemListPageProvider.buildInjectEntry("opDnD")!!.setLink(fromPage = owner).build(), - ItemListPageProvider.buildInjectEntry("opConnect")!!.setLink(fromPage = owner).build(), - ) - } - - override fun getTitle(arguments: Bundle?): String { - return TITLE - } - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt index f01ff3849701..9ad1c22a4912 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt @@ -18,112 +18,69 @@ package com.android.settingslib.spa.gallery.page import android.os.Bundle import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder -import com.android.settingslib.spa.framework.common.SettingsPage +import androidx.navigation.NavType +import androidx.navigation.navArgument import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.scaffold.RegularScaffold -object ArgumentPageProvider : SettingsPageProvider { - // Defines all entry name in this page. - // Note that entry name would be used in log. DO NOT change it once it is set. - // One can still change the display name for better readability if necessary. - private enum class EntryEnum(val displayName: String) { - STRING_PARAM("string_param"), - INT_PARAM("int_param"), - } - - private fun createEntry(owner: SettingsPage, entry: EntryEnum): SettingsEntryBuilder { - return SettingsEntryBuilder.create(owner, entry.name, entry.displayName) - } - - override val name = SettingsPageProviderEnum.ARGUMENT.name - override val displayName = SettingsPageProviderEnum.ARGUMENT.displayName - override val parameter = ArgumentPageModel.parameter +private const val TITLE = "Sample page with arguments" +private const val STRING_PARAM_NAME = "stringParam" +private const val INT_PARAM_NAME = "intParam" - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - if (!ArgumentPageModel.isValidArgument(arguments)) return emptyList() +object ArgumentPageProvider : SettingsPageProvider { + override val name = "Argument" - val owner = createSettingsPage(arguments) - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - createEntry(owner, EntryEnum.STRING_PARAM) - // Set attributes - .setIsSearchDataDynamic(true) - .setSearchDataFn { ArgumentPageModel.genStringParamSearchData() } - .setUiLayoutFn { - // Set ui rendering - Preference(ArgumentPageModel.create(it).genStringParamPreferenceModel()) - }.build() - ) + override val parameter = listOf( + navArgument(STRING_PARAM_NAME) { type = NavType.StringType }, + navArgument(INT_PARAM_NAME) { type = NavType.IntType }, + ) - entryList.add( - createEntry(owner, EntryEnum.INT_PARAM) - // Set attributes - .setIsSearchDataDynamic(true) - .setSearchDataFn { ArgumentPageModel.genIntParamSearchData() } - .setUiLayoutFn { - // Set ui rendering - Preference(ArgumentPageModel.create(it).genIntParamPreferenceModel()) - }.build() + @Composable + override fun Page(arguments: Bundle?) { + ArgumentPage( + stringParam = arguments!!.getString(STRING_PARAM_NAME, "default"), + intParam = arguments.getInt(INT_PARAM_NAME), ) + } - entryList.add(buildInjectEntry("foo")!!.setLink(fromPage = owner).build()) - entryList.add(buildInjectEntry("bar")!!.setLink(fromPage = owner).build()) - - return entryList + @Composable + fun EntryItem(stringParam: String, intParam: Int) { + Preference(object : PreferenceModel { + override val title = TITLE + override val summary = { "$STRING_PARAM_NAME=$stringParam, $INT_PARAM_NAME=$intParam" } + override val onClick = navigator("$name/$stringParam/$intParam") + }) } +} - fun buildInjectEntry(stringParam: String): SettingsEntryBuilder? { - val arguments = ArgumentPageModel.buildArgument(stringParam) - if (!ArgumentPageModel.isValidArgument(arguments)) return null +@Composable +fun ArgumentPage(stringParam: String, intParam: Int) { + RegularScaffold(title = TITLE) { + Preference(object : PreferenceModel { + override val title = "String param value" + override val summary = { stringParam } + }) - return SettingsEntryBuilder.createInject( - owner = createSettingsPage(arguments), - label = "${name}_$stringParam", - ) - .setSearchDataFn { ArgumentPageModel.genInjectSearchData() } - .setUiLayoutFn { - // Set ui rendering - Preference(ArgumentPageModel.create(it).genInjectPreferenceModel()) - } - } + Preference(object : PreferenceModel { + override val title = "Int param value" + override val summary = { intParam.toString() } + }) - override fun getTitle(arguments: Bundle?): String { - return ArgumentPageModel.genPageTitle() - } + ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = intParam + 1) - @Composable - override fun Page(arguments: Bundle?) { - val title = remember { getTitle(arguments) } - val entries = remember { buildEntry(arguments) } - val rtArgNext = remember { ArgumentPageModel.buildNextArgument(arguments) } - RegularScaffold(title) { - for (entry in entries) { - if (entry.toPage != null) { - entry.UiLayout(rtArgNext) - } else { - entry.UiLayout() - } - } - } + ArgumentPageProvider.EntryItem(stringParam = "bar", intParam = intParam + 1) } } @Preview(showBackground = true) @Composable private fun ArgumentPagePreview() { - SpaEnvironmentFactory.resetForPreview() SettingsTheme { - ArgumentPageProvider.Page( - ArgumentPageModel.buildArgument(stringParam = "foo", intParam = 0) - ) + ArgumentPage(stringParam = "foo", intParam = 0) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt deleted file mode 100644 index d763f77d2644..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.gallery.page - -import android.os.Bundle -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavType -import androidx.navigation.navArgument -import com.android.settingslib.spa.framework.common.EntrySearchData -import com.android.settingslib.spa.framework.common.PageModel -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.util.getIntArg -import com.android.settingslib.spa.framework.util.getStringArg -import com.android.settingslib.spa.framework.util.navLink -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.widget.preference.PreferenceModel - -private const val TAG = "ArgumentPageModel" - -// Defines all the resources for this page. -// In real Settings App, resources data is defined in xml, rather than SPP. -private const val PAGE_TITLE = "Sample page with arguments" -private const val STRING_PARAM_TITLE = "String param value" -private const val INT_PARAM_TITLE = "Int param value" -private const val STRING_PARAM_NAME = "stringParam" -private const val INT_PARAM_NAME = "rt_intParam" -private val ARGUMENT_PAGE_KEYWORDS = listOf("argument keyword1", "argument keyword2") - -class ArgumentPageModel : PageModel() { - - companion object { - val parameter = listOf( - navArgument(STRING_PARAM_NAME) { type = NavType.StringType }, - navArgument(INT_PARAM_NAME) { type = NavType.IntType }, - ) - - fun buildArgument(stringParam: String? = null, intParam: Int? = null): Bundle { - val args = Bundle() - if (stringParam != null) args.putString(STRING_PARAM_NAME, stringParam) - if (intParam != null) args.putInt(INT_PARAM_NAME, intParam) - return args - } - - fun buildNextArgument(arguments: Bundle? = null): Bundle { - val intParam = parameter.getIntArg(INT_PARAM_NAME, arguments) - val nextIntParam = if (intParam != null) intParam + 1 else null - return buildArgument(intParam = nextIntParam) - } - - fun isValidArgument(arguments: Bundle?): Boolean { - val stringParam = parameter.getStringArg(STRING_PARAM_NAME, arguments) - return (stringParam != null && listOf("foo", "bar").contains(stringParam)) - } - - fun genStringParamSearchData(): EntrySearchData { - return EntrySearchData(title = STRING_PARAM_TITLE) - } - - fun genIntParamSearchData(): EntrySearchData { - return EntrySearchData(title = INT_PARAM_TITLE) - } - - fun genInjectSearchData(): EntrySearchData { - return EntrySearchData(title = PAGE_TITLE, keyword = ARGUMENT_PAGE_KEYWORDS) - } - - fun genPageTitle(): String { - return PAGE_TITLE - } - - @Composable - fun create(arguments: Bundle?): ArgumentPageModel { - val pageModel: ArgumentPageModel = viewModel(key = arguments.toString()) - pageModel.initOnce(arguments) - return pageModel - } - } - - private var arguments: Bundle? = null - private var stringParam: String? = null - private var intParam: Int? = null - - override fun initialize(arguments: Bundle?) { - SpaEnvironmentFactory.instance.logger.message( - TAG, "Initialize with args " + arguments.toString() - ) - this.arguments = arguments - stringParam = parameter.getStringArg(STRING_PARAM_NAME, arguments) - intParam = parameter.getIntArg(INT_PARAM_NAME, arguments) - } - - @Composable - fun genStringParamPreferenceModel(): PreferenceModel { - return object : PreferenceModel { - override val title = STRING_PARAM_TITLE - override val summary = { stringParam!! } - } - } - - @Composable - fun genIntParamPreferenceModel(): PreferenceModel { - return object : PreferenceModel { - override val title = INT_PARAM_TITLE - override val summary = { intParam!!.toString() } - } - } - - @Composable - fun genInjectPreferenceModel(): PreferenceModel { - val summaryArray = listOf( - "$STRING_PARAM_NAME=" + stringParam!!, - "$INT_PARAM_NAME=" + intParam!! - ) - return object : PreferenceModel { - override val title = PAGE_TITLE - override val summary = { summaryArray.joinToString(", ") } - override val onClick = navigator( - SettingsPageProviderEnum.ARGUMENT.name + parameter.navLink(arguments) - ) - } - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt index 345b47aff791..d31dab31669c 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt @@ -43,7 +43,7 @@ object FooterPageProvider : SettingsPageProvider { override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { val entryList = mutableListOf<SettingsEntry>() entryList.add( - SettingsEntryBuilder.create( "Some Preference", owner) + SettingsEntryBuilder.create("Some Preference", owner) .setSearchDataFn { EntrySearchData(title = "Some Preference") } .setUiLayoutFn { Preference(remember { @@ -58,14 +58,12 @@ object FooterPageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPageProvider.kt index ee22b96c937f..021e84f81808 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPageProvider.kt @@ -41,7 +41,7 @@ object IllustrationPageProvider : SettingsPageProvider { override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { val entryList = mutableListOf<SettingsEntry>() entryList.add( - SettingsEntryBuilder.create( "Lottie Illustration", owner) + SettingsEntryBuilder.create("Lottie Illustration", owner) .setUiLayoutFn { Preference(object : PreferenceModel { override val title = "Lottie Illustration" @@ -54,7 +54,7 @@ object IllustrationPageProvider : SettingsPageProvider { }.build() ) entryList.add( - SettingsEntryBuilder.create( "Image Illustration", owner) + SettingsEntryBuilder.create("Image Illustration", owner) .setUiLayoutFn { Preference(object : PreferenceModel { override val title = "Image Illustration" @@ -70,14 +70,12 @@ object IllustrationPageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt index f1cbc3729a78..4d474816082f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt @@ -30,9 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme @@ -47,14 +45,12 @@ private const val TITLE = "Sample LoadingBar" object LoadingBarPageProvider : SettingsPageProvider { override val name = "LoadingBar" - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPageProvider.kt index 9026a240a04a..47c49fea3e5a 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPageProvider.kt @@ -27,9 +27,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference @@ -46,14 +44,12 @@ private const val TITLE = "Sample ProgressBar" object ProgressBarPageProvider : SettingsPageProvider { override val name = "ProgressBar" - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt index 89b10ee2cb84..572746b14535 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt @@ -117,15 +117,14 @@ object SliderPageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner).setUiLayoutFn { - Preference( - object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - } - ) - } + @Composable + fun Entry() { + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + } + ) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt index 603fceed9900..b83a02637371 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt @@ -50,15 +50,12 @@ object IntroPreferencePageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner).setUiLayoutFn { - Preference( - object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - } - ) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt index d7de9b4f2045..3bb526ef7996 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt @@ -23,9 +23,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.ListPreference @@ -33,6 +31,8 @@ import com.android.settingslib.spa.widget.preference.ListPreferenceModel import com.android.settingslib.spa.widget.preference.ListPreferenceOption import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow @@ -41,30 +41,24 @@ private const val TITLE = "Sample ListPreference" object ListPreferencePageProvider : SettingsPageProvider { override val name = "ListPreference" - private val owner = createSettingsPage() - override fun buildEntry(arguments: Bundle?) = listOf( - SettingsEntryBuilder.create("ListPreference", owner) - .setUiLayoutFn { + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { SampleListPreference() - }.build(), - SettingsEntryBuilder.create("ListPreference not changeable", owner) - .setUiLayoutFn { SampleNotChangeableListPreference() - }.build(), - ) - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) } + } } - override fun getTitle(arguments: Bundle?) = TITLE + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } } @Composable diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePageProvider.kt index 0d85c0e3262f..f548160ef336 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePageProvider.kt @@ -59,14 +59,12 @@ object MainSwitchPreferencePageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt index 1626b025e2f7..831b43942e98 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt @@ -17,45 +17,44 @@ package com.android.settingslib.spa.gallery.preference import android.os.Bundle -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category private const val TITLE = "Category: Preference" object PreferenceMainPageProvider : SettingsPageProvider { override val name = "PreferenceMain" - private val owner = createSettingsPage() - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - return listOf( - PreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - SwitchPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - MainSwitchPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ListPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - TwoTargetSwitchPreferencePageProvider.buildInjectEntry() - .setLink(fromPage = owner).build(), - ZeroStatePreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - IntroPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - TopIntroPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ) - } - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { + PreferencePageProvider.Entry() + ListPreferencePageProvider.Entry() + } + Category { + SwitchPreferencePageProvider.Entry() + MainSwitchPreferencePageProvider.Entry() + TwoTargetSwitchPreferencePageProvider.Entry() + } + Category { + ZeroStatePreferencePageProvider.Entry() + IntroPreferencePageProvider.Entry() + TopIntroPreferencePageProvider.Entry() } + } } - override fun getTitle(arguments: Bundle?): String { - return TITLE + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt deleted file mode 100644 index fc6f10f79ceb..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.gallery.preference - -import android.os.Bundle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import com.android.settingslib.spa.framework.common.PageModel -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -private const val TAG = "PreferencePageModel" - -class PreferencePageModel : PageModel() { - companion object { - // Defines all the resources for this page. - // In real Settings App, resources data is defined in xml, rather than SPP. - const val PAGE_TITLE = "Sample Preference" - const val SIMPLE_PREFERENCE_TITLE = "Preference" - const val SIMPLE_PREFERENCE_SUMMARY = "Simple summary" - const val DISABLE_PREFERENCE_TITLE = "Disabled" - const val DISABLE_PREFERENCE_SUMMARY = "Disabled summary" - const val ASYNC_PREFERENCE_TITLE = "Async Preference" - const val ASYNC_PREFERENCE_SUMMARY = "Async summary" - const val MANUAL_UPDATE_PREFERENCE_TITLE = "Manual Updater" - const val AUTO_UPDATE_PREFERENCE_TITLE = "Auto Updater" - val SIMPLE_PREFERENCE_KEYWORDS = listOf("simple keyword1", "simple keyword2") - - @Composable - fun create(): PreferencePageModel { - val pageModel: PreferencePageModel = viewModel() - pageModel.initOnce() - return pageModel - } - } - - private val spaLogger = SpaEnvironmentFactory.instance.logger - - val asyncSummary = mutableStateOf("(loading)") - val asyncEnable = mutableStateOf(false) - - private val manualUpdater = mutableStateOf(0) - - private val autoUpdater = object : MutableLiveData<String>(" ") { - private var tick = 0 - private var updateJob: Job? = null - override fun onActive() { - spaLogger.message(TAG, "autoUpdater.active") - updateJob = viewModelScope.launch(Dispatchers.IO) { - while (true) { - delay(1000L) - tick++ - spaLogger.message(TAG, "autoUpdater.value $tick") - postValue(tick.toString()) - } - } - } - - override fun onInactive() { - spaLogger.message(TAG, "autoUpdater.inactive") - updateJob?.cancel() - } - } - - override fun initialize(arguments: Bundle?) { - spaLogger.message(TAG, "initialize with args " + arguments.toString()) - viewModelScope.launch(Dispatchers.IO) { - // Loading your data here. - delay(2000L) - asyncSummary.value = ASYNC_PREFERENCE_SUMMARY - asyncEnable.value = true - } - } - - fun getManualUpdaterSummary(): State<String> { - spaLogger.message(TAG, "getManualUpdaterSummary") - return derivedStateOf { manualUpdater.value.toString() } - } - - fun manualUpdaterOnClick() { - spaLogger.message(TAG, "manualUpdaterOnClick") - manualUpdater.value = manualUpdater.value + 1 - } - - fun getAutoUpdaterSummary(): LiveData<String> { - spaLogger.message(TAG, "getAutoUpdaterSummary") - return autoUpdater - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt index 6d1d34628efa..f7649b91f558 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt @@ -18,187 +18,100 @@ package com.android.settingslib.spa.gallery.preference import android.os.Bundle import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Autorenew import androidx.compose.material.icons.outlined.DisabledByDefault -import androidx.compose.material.icons.outlined.TouchApp import androidx.compose.runtime.Composable -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.EntrySearchData -import com.android.settingslib.spa.framework.common.EntryStatusData -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.AUTO_UPDATE_PREFERENCE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_SUMMARY -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.MANUAL_UPDATE_PREFERENCE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.PAGE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_KEYWORDS -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_SUMMARY -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_TITLE import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category import com.android.settingslib.spa.widget.ui.SettingsIcon - -private const val TAG = "PreferencePage" +import kotlinx.coroutines.delay object PreferencePageProvider : SettingsPageProvider { - // Defines all entry name in this page. - // Note that entry name would be used in log. DO NOT change it once it is set. - // One can still change the display name for better readability if necessary. - enum class EntryEnum(val displayName: String) { - SIMPLE_PREFERENCE("preference"), - SUMMARY_PREFERENCE("preference_with_summary"), - SINGLE_LINE_SUMMARY_PREFERENCE("preference_with_single_line_summary"), - DISABLED_PREFERENCE("preference_disable"), - ASYNC_SUMMARY_PREFERENCE("preference_with_async_summary"), - MANUAL_UPDATE_PREFERENCE("preference_actionable"), - AUTO_UPDATE_PREFERENCE("preference_auto_update"), - } - - override val name = SettingsPageProviderEnum.PREFERENCE.name - override val displayName = SettingsPageProviderEnum.PREFERENCE.displayName - private val spaLogger = SpaEnvironmentFactory.instance.logger - private val owner = createSettingsPage() - - private fun createEntry(entry: EntryEnum): SettingsEntryBuilder { - return SettingsEntryBuilder.create(owner, entry.name, entry.displayName) - } - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - createEntry(EntryEnum.SIMPLE_PREFERENCE) - .setMacro { - spaLogger.message(TAG, "create macro for ${EntryEnum.SIMPLE_PREFERENCE}") - SimplePreferenceMacro(title = SIMPLE_PREFERENCE_TITLE) - } - .setStatusDataFn { EntryStatusData(isDisabled = false) } - .build() - ) - entryList.add( - createEntry(EntryEnum.SUMMARY_PREFERENCE) - .setMacro { - spaLogger.message(TAG, "create macro for ${EntryEnum.SUMMARY_PREFERENCE}") - SimplePreferenceMacro( - title = SIMPLE_PREFERENCE_TITLE, - summary = SIMPLE_PREFERENCE_SUMMARY, - searchKeywords = SIMPLE_PREFERENCE_KEYWORDS, - ) - } - .setStatusDataFn { EntryStatusData(isDisabled = true) } - .build() - ) - entryList.add(singleLineSummaryEntry()) - entryList.add( - createEntry(EntryEnum.DISABLED_PREFERENCE) - .setHasMutableStatus(true) - .setMacro { - spaLogger.message(TAG, "create macro for ${EntryEnum.DISABLED_PREFERENCE}") - SimplePreferenceMacro( - title = DISABLE_PREFERENCE_TITLE, - summary = DISABLE_PREFERENCE_SUMMARY, - disabled = true, - icon = Icons.Outlined.DisabledByDefault, - ) - } - .setStatusDataFn { EntryStatusData(isDisabled = true) } - .build() - ) - entryList.add( - createEntry(EntryEnum.ASYNC_SUMMARY_PREFERENCE) - .setHasMutableStatus(true) - .setSearchDataFn { - EntrySearchData(title = ASYNC_PREFERENCE_TITLE) - } - .setStatusDataFn { EntryStatusData(isDisabled = false) } - .setUiLayoutFn { - val model = PreferencePageModel.create() - Preference( - object : PreferenceModel { - override val title = ASYNC_PREFERENCE_TITLE - override val summary = { model.asyncSummary.value } - override val enabled = { model.asyncEnable.value } - } - ) - }.build() - ) - entryList.add( - createEntry(EntryEnum.MANUAL_UPDATE_PREFERENCE) - .setUiLayoutFn { - val model = PreferencePageModel.create() - val manualUpdaterSummary = remember { model.getManualUpdaterSummary() } - Preference( - object : PreferenceModel { - override val title = MANUAL_UPDATE_PREFERENCE_TITLE - override val summary = { manualUpdaterSummary.value } - override val onClick = { model.manualUpdaterOnClick() } - override val icon = @Composable { - SettingsIcon(imageVector = Icons.Outlined.TouchApp) - } - } - ) - }.build() - ) - entryList.add( - createEntry(EntryEnum.AUTO_UPDATE_PREFERENCE) - .setUiLayoutFn { - val model = PreferencePageModel.create() - val autoUpdaterSummary = remember { - model.getAutoUpdaterSummary() - }.observeAsState(" ") - Preference( - object : PreferenceModel { - override val title = AUTO_UPDATE_PREFERENCE_TITLE - override val summary = { autoUpdaterSummary.value } - override val icon = @Composable { - SettingsIcon(imageVector = Icons.Outlined.Autorenew) - } - } - ) - }.build() - ) + override val name = "Preference" + private const val PAGE_TITLE = "Sample Preference" - return entryList - } + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(PAGE_TITLE) { + Category { + Preference(object : PreferenceModel { + override val title = "Preference" + }) + Preference(object : PreferenceModel { + override val title = "Preference" + override val summary = { "Simple summary" } + }) + val summary = stringResource(R.string.single_line_summary_preference_summary) + Preference( + model = object : PreferenceModel { + override val title = + stringResource(R.string.single_line_summary_preference_title) + override val summary = { summary } + }, + singleLineSummary = true, + ) + } + Category { + Preference(object : PreferenceModel { + override val title = "Disabled" + override val summary = { "Disabled summary" } + override val enabled = { false } + override val icon = @Composable { + SettingsIcon(imageVector = Icons.Outlined.DisabledByDefault) + } + }) + } + Category { + Preference(object : PreferenceModel { + override val title = "Preference" + val asyncSummary by produceState(initialValue = " ") { + delay(1000L) + value = "Async summary" + } + override val summary = { asyncSummary } + }) - private fun singleLineSummaryEntry() = createEntry(EntryEnum.SINGLE_LINE_SUMMARY_PREFERENCE) - .setUiLayoutFn { - val summary = stringResource(R.string.single_line_summary_preference_summary) - Preference( - model = object : PreferenceModel { - override val title: String = - stringResource(R.string.single_line_summary_preference_title) - override val summary = { summary } - }, - singleLineSummary = true, - ) - } - .build() + var count by remember { mutableIntStateOf(0) } + Preference(object : PreferenceModel { + override val title = "Click me" + override val summary = { count.toString() } + override val onClick: (() -> Unit) = { count++ } + }) - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = owner) - .setMacro { - spaLogger.message(TAG, "create macro for INJECT entry") - SimplePreferenceMacro( - title = PAGE_TITLE, - clickRoute = SettingsPageProviderEnum.PREFERENCE.name - ) + var ticks by remember { mutableIntStateOf(0) } + LaunchedEffect(ticks) { + delay(1000L) + ticks++ + } + Preference(object : PreferenceModel { + override val title = "Ticker" + override val summary = { ticks.toString() } + }) } + } } - override fun getTitle(arguments: Bundle?): String { - return PAGE_TITLE + @Composable + fun Entry() { + Preference(model = object : PreferenceModel { + override val title = PAGE_TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt index f2225fa86136..9508d504a5d8 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt @@ -27,16 +27,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category import com.android.settingslib.spa.widget.ui.SettingsIcon import kotlinx.coroutines.delay @@ -44,56 +43,26 @@ private const val TITLE = "Sample SwitchPreference" object SwitchPreferencePageProvider : SettingsPageProvider { override val name = "SwitchPreference" - private val owner = createSettingsPage() - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference", owner) - .setUiLayoutFn { - SampleSwitchPreference() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference with summary", owner) - .setUiLayoutFn { - SampleSwitchPreferenceWithSummary() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference with async summary", owner) - .setUiLayoutFn { - SampleSwitchPreferenceWithAsyncSummary() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference not changeable", owner) - .setUiLayoutFn { - SampleNotChangeableSwitchPreference() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference with icon", owner) - .setUiLayoutFn { - SampleSwitchPreferenceWithIcon() - }.build() - ) - - return entryList - } - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { + SampleSwitchPreference() + SampleSwitchPreferenceWithSummary() + SampleSwitchPreferenceWithAsyncSummary() + SampleNotChangeableSwitchPreference() + SampleSwitchPreferenceWithIcon() } + } } - override fun getTitle(arguments: Bundle?): String { - return TITLE + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt index b251266e0574..ee08e30ab4ae 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt @@ -50,15 +50,12 @@ object TopIntroPreferencePageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner).setUiLayoutFn { - Preference( - object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - } - ) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt index 19de31dab046..1a89bb2dc4f4 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt @@ -25,66 +25,40 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category import kotlinx.coroutines.delay private const val TITLE = "Sample TwoTargetSwitchPreference" object TwoTargetSwitchPreferencePageProvider : SettingsPageProvider { override val name = "TwoTargetSwitchPreference" - private val owner = createSettingsPage() - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - SettingsEntryBuilder.create( "TwoTargetSwitchPreference", owner) - .setUiLayoutFn { - SampleTwoTargetSwitchPreference() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "TwoTargetSwitchPreference with summary", owner) - .setUiLayoutFn { - SampleTwoTargetSwitchPreferenceWithSummary() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "TwoTargetSwitchPreference with async summary", owner) - .setUiLayoutFn { - SampleTwoTargetSwitchPreferenceWithAsyncSummary() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "TwoTargetSwitchPreference not changeable", owner) - .setUiLayoutFn { - SampleNotChangeableTwoTargetSwitchPreference() - }.build() - ) - - return entryList - } - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { + SampleTwoTargetSwitchPreference() + SampleTwoTargetSwitchPreferenceWithSummary() + SampleTwoTargetSwitchPreferenceWithAsyncSummary() + SampleNotChangeableTwoTargetSwitchPreference() } + } } - override fun getTitle(arguments: Bundle?): String { - return TITLE + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt index 4a9c5c8fad4f..04b5ceb796e7 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt @@ -53,14 +53,12 @@ object ZeroStatePreferencePageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt index 66cc38f74b07..c9a6557d60ef 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt @@ -17,7 +17,7 @@ package com.android.settingslib.spa.gallery.scaffold import android.os.Bundle -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator @@ -34,13 +34,13 @@ object PagerMainPageProvider : SettingsPageProvider { ScrollablePagerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) - fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } override fun getTitle(arguments: Bundle?) = TITLE } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt index eac06e3eb52b..0d7cad108b7d 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt @@ -18,9 +18,7 @@ package com.android.settingslib.spa.gallery.scaffold import android.os.Bundle import androidx.compose.runtime.Composable -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel @@ -32,15 +30,13 @@ private const val TITLE = "Sample SearchScaffold" object SearchScaffoldPageProvider : SettingsPageProvider { override val name = "SearchScaffold" - private val owner = createSettingsPage() - - fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } @Composable override fun Page(arguments: Bundle?) { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt index a0ab2ce6945d..7b02fcb59cd8 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt @@ -27,9 +27,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.gallery.R @@ -49,15 +47,13 @@ private const val TITLE = "Sample SuwScaffold" object SuwScaffoldPageProvider : SettingsPageProvider { override val name = "SuwScaffold" - private val owner = createSettingsPage() - - fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } @Composable override fun Page(arguments: Bundle?) { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt index 7a1fad016d0e..4d3a78a583fc 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt @@ -19,7 +19,6 @@ package com.android.settingslib.spa.gallery.ui import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.EntrySearchData import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider @@ -31,7 +30,6 @@ import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro import com.android.settingslib.spa.widget.scaffold.RegularScaffold import com.android.settingslib.spa.widget.ui.Category -import com.android.settingslib.spa.widget.ui.CategoryTitle private const val TITLE = "Sample Category" @@ -39,15 +37,14 @@ object CategoryPageProvider : SettingsPageProvider { override val name = "Category" private val owner = createSettingsPage() - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) + @Composable + fun Entry() { + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) } - .setSearchDataFn { EntrySearchData(title = TITLE) } + ) } override fun getTitle(arguments: Bundle?): String { @@ -70,7 +67,6 @@ object CategoryPageProvider : SettingsPageProvider { SettingsEntryBuilder.create("Preference 3", owner) .setMacro { SimplePreferenceMacro(title = "Preference 2", summary = "Summary 3") } .build() - ) entryList.add( SettingsEntryBuilder.create("Preference 4", owner) @@ -84,11 +80,11 @@ object CategoryPageProvider : SettingsPageProvider { override fun Page(arguments: Bundle?) { val entries = buildEntry(arguments) RegularScaffold(title = getTitle(arguments)) { - CategoryTitle("Category A") - entries[0].UiLayout() - entries[1].UiLayout() - - Category("Category B") { + Category("Category A") { + entries[0].UiLayout() + entries[1].UiLayout() + } + Category { entries[2].UiLayout() entries[3].UiLayout() } @@ -99,7 +95,5 @@ object CategoryPageProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable private fun SpinnerPagePreview() { - SettingsTheme { - SpinnerPageProvider.Page(null) - } + SettingsTheme { CategoryPageProvider.Page(null) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt index f897d8c58030..e919129e9dac 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt @@ -21,10 +21,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.android.settingslib.spa.framework.common.EntrySearchData -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.preference.Preference @@ -37,17 +34,12 @@ private const val TITLE = "Sample Copyable" object CopyablePageProvider : SettingsPageProvider { override val name = "Copyable" - private val owner = createSettingsPage() - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } - .setSearchDataFn { EntrySearchData(title = TITLE) } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } @Composable diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt index 5c5c504a4310..7a4b63291375 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt @@ -23,9 +23,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference @@ -39,14 +37,12 @@ private const val TITLE = "Sample Spinner" object SpinnerPageProvider : SettingsPageProvider { override val name = "Spinner" - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index f8c791aab0d0..ab95162fb142 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -24,6 +24,7 @@ object SettingsDimension { val paddingExtraSmall = 4.dp val paddingSmall = if (isSpaExpressiveEnabled) 8.dp else 4.dp val paddingExtraSmall5 = 10.dp + val paddingExtraSmall6 = 12.dp val paddingLarge = 16.dp val paddingExtraLarge = 24.dp @@ -36,9 +37,9 @@ object SettingsDimension { val itemIconSize = 24.dp val itemIconContainerSize = 72.dp - val itemPaddingStart = paddingExtraLarge + val itemPaddingStart = if (isSpaExpressiveEnabled) paddingLarge else paddingExtraLarge val itemPaddingEnd = paddingLarge - val itemPaddingVertical = paddingLarge + val itemPaddingVertical = if (isSpaExpressiveEnabled) paddingExtraSmall6 else paddingLarge val itemPadding = PaddingValues( start = itemPaddingStart, top = itemPaddingVertical, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt index f7c5414a420c..c78771566f64 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt @@ -24,5 +24,7 @@ object SettingsShape { val CornerMedium = RoundedCornerShape(12.dp) + val categoryCorner = RoundedCornerShape(20.dp) + val CornerExtraLarge = RoundedCornerShape(28.dp) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt index 185fd2974fb1..38707b0378bc 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt @@ -57,6 +57,7 @@ import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraLarg import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraSmall import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight import com.android.settingslib.spa.widget.ui.SettingsBody import com.android.settingslib.spa.widget.ui.SettingsTitle @@ -159,7 +160,9 @@ fun BannerHeader(imageVector: ImageVector?, iconColor: Color, onDismiss: (() -> @Composable fun BannerTitleHeader(title: String, onDismiss: (() -> Unit)? = null) { Row(Modifier.fillMaxWidth()) { - Box(modifier = Modifier.weight(1f)) { SettingsTitle(title) } + Box(modifier = Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.titleMedium.toSemiBoldWeight()) + } Spacer(modifier = Modifier.padding(SettingsDimension.paddingSmall)) DismissButton(onDismiss) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt index 5bb57b8ed1df..203a8bd39fae 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt @@ -56,6 +56,7 @@ import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.theme.divider import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight data class ActionButton( val text: String, @@ -129,7 +130,7 @@ private fun RowScope.ActionButton(actionButton: ActionButton) { Text( text = actionButton.text, textAlign = TextAlign.Center, - style = MaterialTheme.typography.labelMedium, + style = MaterialTheme.typography.labelLarge.toSemiBoldWeight(), ) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt index 265864e1b3fd..490936fa7a47 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -99,7 +100,16 @@ private fun AlertDialogPresenter.SettingsAlertDialog( dismissButton?.let { { if (isSpaExpressiveEnabled) DismissButton(it) else Button(it) } }, - title = title?.let { { CenterRow { Text(it) } } }, + title = + title?.let { + { + CenterRow { + if (isSpaExpressiveEnabled) + Text(it, style = MaterialTheme.typography.bodyLarge) + else Text(it) + } + } + }, text = text?.let { { CenterRow { Column(Modifier.verticalScroll(rememberScrollState())) { text() } } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index 23a8e78e6c4a..c68ec78b1ba6 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -16,6 +16,7 @@ package com.android.settingslib.spa.widget.preference +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -25,16 +26,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled +import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled import com.android.settingslib.spa.widget.ui.SettingsTitle @Composable @@ -51,10 +56,17 @@ internal fun BaseLayout( widget: @Composable () -> Unit = {}, ) { Row( - modifier = modifier - .fillMaxWidth() - .semantics(mergeDescendants = true) {} - .padding(end = paddingEnd), + modifier = + modifier + .fillMaxWidth() + .semantics(mergeDescendants = true) {} + .then( + if (isSpaExpressiveEnabled) + Modifier.clip(SettingsShape.CornerExtraSmall) + .background(MaterialTheme.colorScheme.surfaceBright) + else Modifier + ) + .padding(end = paddingEnd), verticalAlignment = Alignment.CenterVertically, ) { val alphaModifier = Modifier.alphaForEnabled(enabled()) @@ -63,20 +75,14 @@ internal fun BaseLayout( title = title, titleContentDescription = titleContentDescription, subTitle = subTitle, - modifier = alphaModifier - .weight(1f) - .padding(vertical = paddingVertical), + modifier = alphaModifier.weight(1f).padding(vertical = paddingVertical), ) widget() } } @Composable -internal fun BaseIcon( - icon: @Composable (() -> Unit)?, - modifier: Modifier, - paddingStart: Dp, -) { +internal fun BaseIcon(icon: @Composable (() -> Unit)?, modifier: Modifier, paddingStart: Dp) { if (icon != null) { Box( modifier = modifier.size(SettingsDimension.itemIconContainerSize), @@ -107,11 +113,6 @@ private fun Titles( @Composable private fun BaseLayoutPreview() { SettingsTheme { - BaseLayout( - title = "Title", - subTitle = { - HorizontalDivider(thickness = 10.dp) - } - ) + BaseLayout(title = "Title", subTitle = { HorizontalDivider(thickness = 10.dp) }) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt index 22a57554eeaf..77073765d5a9 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight @Composable fun IntroPreference( @@ -112,7 +113,7 @@ private fun IntroTitle(title: String) { Text( text = title, textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleLarge.toSemiBoldWeight(), color = MaterialTheme.colorScheme.onSurface, ) } @@ -126,7 +127,7 @@ private fun IntroDescription(descriptions: List<String>?) { Text( text = description, textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = SettingsDimension.paddingExtraSmall), ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt index 3f2e7723c585..b771f367e697 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt @@ -47,6 +47,7 @@ import androidx.graphics.shapes.CornerRounding import androidx.graphics.shapes.RoundedPolygon import androidx.graphics.shapes.star import androidx.graphics.shapes.toPath +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight @Composable fun ZeroStatePreference(icon: ImageVector, text: String? = null, description: String? = null) { @@ -80,7 +81,7 @@ fun ZeroStatePreference(icon: ImageVector, text: String? = null, description: St Text( text = text, textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium.toSemiBoldWeight(), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 24.dp), ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt index 48cd145da124..6c5581fb4b50 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt @@ -16,9 +16,13 @@ package com.android.settingslib.spa.widget.ui +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.TouchApp import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,25 +31,31 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel -/** - * A category title that is placed before a group of similar items. - */ +/** A category title that is placed before a group of similar items. */ @Composable fun CategoryTitle(title: String) { Text( text = title, - modifier = Modifier.padding( - start = SettingsDimension.itemPaddingStart, - top = 20.dp, - end = SettingsDimension.itemPaddingEnd, - bottom = 8.dp, - ), + modifier = + Modifier.padding( + start = SettingsDimension.itemPaddingStart, + top = 20.dp, + end = + if (isSpaExpressiveEnabled) SettingsDimension.paddingSmall + else SettingsDimension.itemPaddingEnd, + bottom = 8.dp, + ), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelMedium, ) @@ -56,14 +66,31 @@ fun CategoryTitle(title: String) { * visually separates groups of items. */ @Composable -fun Category(title: String, content: @Composable ColumnScope.() -> Unit) { - Column { +fun Category(title: String? = null, content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = + if (isSpaExpressiveEnabled) + Modifier.padding( + horizontal = SettingsDimension.paddingLarge, + vertical = SettingsDimension.paddingSmall, + ) + else Modifier + ) { var displayTitle by remember { mutableStateOf(false) } - if (displayTitle) CategoryTitle(title = title) + if (title != null && displayTitle) CategoryTitle(title = title) Column( - modifier = Modifier.onGloballyPositioned { coordinates -> - displayTitle = coordinates.size.height > 0 - }, + modifier = + Modifier.onGloballyPositioned { coordinates -> + displayTitle = coordinates.size.height > 0 + } + .then( + if (isSpaExpressiveEnabled) + Modifier.fillMaxWidth().clip(SettingsShape.categoryCorner) + else Modifier + ), + verticalArrangement = + if (isSpaExpressiveEnabled) Arrangement.spacedBy(SettingsDimension.paddingTiny) + else Arrangement.Top, content = content, ) } @@ -73,6 +100,21 @@ fun Category(title: String, content: @Composable ColumnScope.() -> Unit) { @Composable private fun CategoryPreview() { SettingsTheme { - CategoryTitle("Appearance") + Category("Appearance") { + Preference( + object : PreferenceModel { + override val title = "Title" + override val summary = { "Summary" } + } + ) + Preference( + object : PreferenceModel { + override val title = "Title" + override val summary = { "Summary" } + override val icon = + @Composable { SettingsIcon(imageVector = Icons.Outlined.TouchApp) } + } + ) + } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 616ab072ae20..612c193da9c3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -709,6 +709,9 @@ public class BluetoothUtils { @WorkerThread public static boolean hasConnectedBroadcastSourceForBtDevice( @Nullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager) { + if (Flags.audioSharingHysteresisModeFix()) { + return hasActiveLocalBroadcastSourceForBtDevice(device, localBtManager); + } LocalBluetoothLeBroadcastAssistant assistant = localBtManager == null ? null diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index a3f9e515a0bc..364e95c61ca8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -52,6 +52,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.android.settingslib.R; +import com.android.settingslib.flags.Flags; import com.google.common.collect.ImmutableList; @@ -1134,20 +1135,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null"); return; } - List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices(); - List<BluetoothDevice> devicesInSharing = - connectedDevices.stream() - .filter( - bluetoothDevice -> { - List<BluetoothLeBroadcastReceiveState> sourceList = - mServiceBroadcastAssistant.getAllSources( - bluetoothDevice); - return !sourceList.isEmpty() - && sourceList.stream() - .anyMatch(BluetoothUtils::isConnected); - }) - .collect(Collectors.toList()); - if (devicesInSharing.isEmpty()) { + List<BluetoothDevice> devicesInBroadcast = getDevicesInBroadcast(); + if (devicesInBroadcast.isEmpty()) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no sinks in broadcast"); return; } @@ -1156,7 +1145,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { BluetoothDevice targetDevice = null; // Find the earliest connected device in sharing session. int targetDeviceIdx = -1; - for (BluetoothDevice device : devicesInSharing) { + for (BluetoothDevice device : devicesInBroadcast) { if (devices.contains(device)) { int idx = devices.indexOf(device); if (idx > targetDeviceIdx) { @@ -1169,10 +1158,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null"); return; } - Log.d( - TAG, - "updateFallbackActiveDeviceIfNeeded, set active device: " - + targetDevice.getAnonymizedAddress()); CachedBluetoothDevice targetCachedDevice = mDeviceManager.findDevice(targetDevice); if (targetCachedDevice == null) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find cached bt device"); @@ -1180,16 +1165,37 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } int fallbackActiveGroupId = getFallbackActiveGroupId(); if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID - && getGroupId(targetCachedDevice) == fallbackActiveGroupId) { + && BluetoothUtils.getGroupId(targetCachedDevice) == fallbackActiveGroupId) { Log.d( TAG, "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: " + fallbackActiveGroupId); return; } + Log.d( + TAG, + "updateFallbackActiveDeviceIfNeeded, set active device: " + + targetDevice.getAnonymizedAddress()); targetCachedDevice.setActive(); } + private List<BluetoothDevice> getDevicesInBroadcast() { + boolean hysteresisModeFixEnabled = Flags.audioSharingHysteresisModeFix(); + List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices(); + return connectedDevices.stream() + .filter( + bluetoothDevice -> { + List<BluetoothLeBroadcastReceiveState> sourceList = + mServiceBroadcastAssistant.getAllSources( + bluetoothDevice); + return !sourceList.isEmpty() && sourceList.stream().anyMatch( + source -> hysteresisModeFixEnabled + ? BluetoothUtils.isSourceMatched(source, mBroadcastId) + : BluetoothUtils.isConnected(source)); + }) + .collect(Collectors.toList()); + } + private int getFallbackActiveGroupId() { return Settings.Secure.getInt( mContext.getContentResolver(), @@ -1197,23 +1203,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { BluetoothCsipSetCoordinator.GROUP_ID_INVALID); } - private int getGroupId(CachedBluetoothDevice cachedDevice) { - int groupId = cachedDevice.getGroupId(); - String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress(); - if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { - Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress); - return groupId; - } - for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { - if (profile instanceof LeAudioProfile) { - Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress); - return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice()); - } - } - Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress); - return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; - } - private void notifyBroadcastStateChange(@BroadcastState int state) { if (!mContext.getPackageName().equals(SETTINGS_PKG)) { Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings."); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt index 5656f38a0a11..36276696e3ec 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt @@ -63,7 +63,8 @@ data class DeviceSettingsConfig( }, moreSettingsHelpItem = readParcelable( DeviceSettingItem::class.java.classLoader - ) + ), + extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY, ) } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 8eedb35a3181..0e060dfdd447 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -47,6 +47,7 @@ import android.provider.Settings; import android.util.Pair; import com.android.internal.R; +import com.android.settingslib.flags.Flags; import com.android.settingslib.widget.AdaptiveIcon; import com.google.common.collect.ImmutableList; @@ -605,6 +606,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSource_leadDeviceConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class); BluetoothDevice memberDevice = mock(BluetoothDevice.class); @@ -630,6 +632,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSource_memberDeviceConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class); BluetoothDevice memberDevice = mock(BluetoothDevice.class); @@ -655,6 +658,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSource_deviceNotConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); List<Long> bisSyncState = new ArrayList<>(); @@ -672,6 +676,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSourceForBtDevice_deviceConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); List<Long> bisSyncState = new ArrayList<>(); bisSyncState.add(1L); when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); @@ -688,6 +693,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSourceForBtDevice_deviceNotConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); List<Long> bisSyncState = new ArrayList<>(); when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); @@ -702,6 +708,106 @@ public class BluetoothUtilsTest { } @Test + public void hasConnectedBroadcastSource_hysteresisFix_leadDeviceHasActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class); + BluetoothDevice memberDevice = mock(BluetoothDevice.class); + when(memberCachedDevice.getDevice()).thenReturn(memberDevice); + Set<CachedBluetoothDevice> memberCachedDevices = new HashSet<>(); + memberCachedDevices.add(memberCachedDevice); + when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberCachedDevices); + + + when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + when(mAssistant.getAllSources(memberDevice)).thenReturn(Collections.emptyList()); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSource( + mCachedBluetoothDevice, mLocalBluetoothManager)) + .isTrue(); + } + + @Test + public void hasConnectedBroadcastSource_hysteresisFix_memberDeviceHasActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class); + BluetoothDevice memberDevice = mock(BluetoothDevice.class); + when(memberCachedDevice.getDevice()).thenReturn(memberDevice); + Set<CachedBluetoothDevice> memberCachedDevices = new HashSet<>(); + memberCachedDevices.add(memberCachedDevice); + when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberCachedDevices); + + when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(memberDevice)).thenReturn(sourceList); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(Collections.emptyList()); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSource( + mCachedBluetoothDevice, mLocalBluetoothManager)) + .isTrue(); + } + + @Test + public void hasConnectedBroadcastSource_hysteresisFix_deviceNoActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + + when(mBroadcast.getLatestBroadcastId()).thenReturn(UNKNOWN_VALUE_PLACEHOLDER); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSource( + mCachedBluetoothDevice, mLocalBluetoothManager)) + .isFalse(); + } + + @Test + public void hasConnectedBroadcastSourceForBtDevice_hysteresisFix_deviceHasActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSourceForBtDevice( + mBluetoothDevice, mLocalBluetoothManager)) + .isTrue(); + } + + @Test + public void hasConnectedBroadcastSourceForBtDevice_hysteresisFix_deviceNoActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(UNKNOWN_VALUE_PLACEHOLDER); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSourceForBtDevice( + mBluetoothDevice, mLocalBluetoothManager)) + .isFalse(); + } + + @Test public void testHasActiveLocalBroadcastSourceForBtDevice_hasActiveLocalSource() { when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt index 7f1729387a22..ebaad342f9f2 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt @@ -86,6 +86,7 @@ class DeviceSettingsConfigTest { assertThat(fromParcel.moreSettingsHelpItem?.packageName).isEqualTo("package_name_2") assertThat(fromParcel.moreSettingsHelpItem?.className).isEqualTo("class_name_2") assertThat(fromParcel.moreSettingsHelpItem?.intentAction).isEqualTo("intent_action_2") + assertThat(fromParcel.extras.getString("key1")).isEqualTo("value1") } private fun writeAndRead(item: DeviceSettingsConfig): DeviceSettingsConfig { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppPreferenceTest.java deleted file mode 100644 index 6c8fd50d1896..000000000000 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppPreferenceTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.widget; - -import static com.google.common.truth.Truth.assertThat; - -import android.content.Context; -import android.view.View; - -import androidx.preference.PreferenceViewHolder; - -import com.android.settingslib.widget.preference.app.R; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; - -@RunWith(RobolectricTestRunner.class) -public class AppPreferenceTest { - - private Context mContext; - private View mRootView; - private AppPreference mPref; - private PreferenceViewHolder mHolder; - - @Before - public void setUp() { - mContext = RuntimeEnvironment.application; - mRootView = View.inflate(mContext, R.layout.preference_app, null /* parent */); - mHolder = PreferenceViewHolder.createInstanceForTests(mRootView); - mPref = new AppPreference(mContext); - } - - @Test - public void setProgress_showProgress() { - mPref.setProgress(1); - mPref.onBindViewHolder(mHolder); - - assertThat(mHolder.findViewById(android.R.id.progress).getVisibility()) - .isEqualTo(View.VISIBLE); - } - - @Test - public void foobar_testName() { - float iconSize = mContext.getResources().getDimension(com.android.settingslib.widget.theme.R.dimen.secondary_app_icon_size); - assertThat(Float.floatToIntBits(iconSize)).isEqualTo(Float.floatToIntBits(32)); - } -} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index a9d4c89efe98..cb1411bdd91e 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -559,6 +559,13 @@ flag { } flag { + name: "volume_redesign" + namespace: "systemui" + description: "Enables Volume BC25 visuals update" + bug: "368308908" +} + +flag { name: "clipboard_shared_transitions" namespace: "systemui" description: "Show shared transitions from clipboard" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt index e41a7df39c21..a88ad946d95b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt @@ -21,11 +21,10 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.SceneScope -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.communal.shared.model.CommunalBackgroundType +import com.android.systemui.communal.ui.viewmodel.CommunalUserActionsViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.dagger.SysUISingleton @@ -33,38 +32,32 @@ import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Scene import javax.inject.Inject -import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow /** The communal scene shows glanceable hub when the device is locked and docked. */ @SysUISingleton class CommunalScene @Inject constructor( - private val viewModel: CommunalViewModel, + private val contentViewModel: CommunalViewModel, + actionsViewModelFactory: CommunalUserActionsViewModel.Factory, private val communalColors: CommunalColors, private val communalContent: CommunalContent, ) : ExclusiveActivatable(), Scene { override val key = Scenes.Communal - override val userActions: Flow<Map<UserAction, UserActionResult>> = - MutableStateFlow( - mapOf( - Swipe(SwipeDirection.End) to Scenes.Lockscreen, - ) - ) - .asStateFlow() + private val actionsViewModel: CommunalUserActionsViewModel = actionsViewModelFactory.create() + + override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions override suspend fun onActivated(): Nothing { - awaitCancellation() + actionsViewModel.activate() } @Composable override fun SceneScope.Content(modifier: Modifier) { val backgroundType by - viewModel.communalBackground.collectAsStateWithLifecycle( + contentViewModel.communalBackground.collectAsStateWithLifecycle( initialValue = CommunalBackgroundType.ANIMATED ) @@ -72,7 +65,7 @@ constructor( backgroundType = backgroundType, colors = communalColors, content = communalContent, - viewModel = viewModel, + viewModel = contentViewModel, modifier = modifier.horizontalNestedScrollToScene(), ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 4162891c0e0b..6f1349f20e4d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -18,7 +18,6 @@ package com.android.systemui.shade.ui.composable -import android.view.HapticFeedbackConstants import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -40,20 +39,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.SceneScope import com.android.compose.windowsizeclass.LocalWindowSizeClass -import com.android.systemui.scene.shared.model.Scenes /** Renders a lightweight shade UI container, as an overlay. */ @Composable @@ -62,13 +58,6 @@ fun SceneScope.OverlayShade( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { - val view = LocalView.current - LaunchedEffect(Unit) { - if (layoutState.currentTransition?.fromContent == Scenes.Gone) { - view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START) - } - } - Box(modifier) { Scrim(onClicked = onScrimClicked) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index db0fe3e3f79d..ef415b151200 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -16,7 +16,6 @@ package com.android.systemui.shade.ui.composable -import android.view.HapticFeedbackConstants import android.view.ViewGroup import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState @@ -60,7 +59,6 @@ import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp @@ -226,12 +224,6 @@ private fun SceneScope.ShadeScene( shadeSession: SaveableSession, usingCollapsedLandscapeMedia: Boolean, ) { - val view = LocalView.current - LaunchedEffect(Unit) { - if (layoutState.currentTransition?.fromContent == Scenes.Gone) { - view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START) - } - } val shadeMode by viewModel.shadeMode.collectAsStateWithLifecycle() when (shadeMode) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 9891025ad7d3..367faed7b7f7 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -188,38 +188,47 @@ internal class DraggableHandlerImpl( return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation) } - private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes { - val fromSource = - startedPosition?.let { position -> - layoutImpl.swipeSourceDetector.source( - layoutImpl.lastSize, - position.round(), - layoutImpl.density, - orientation, - ) - } + private fun resolveSwipeSource(startedPosition: Offset?): SwipeSource.Resolved? { + if (startedPosition == null) return null + return layoutImpl.swipeSourceDetector.source( + layoutSize = layoutImpl.lastSize, + position = startedPosition.round(), + density = layoutImpl.density, + orientation = orientation, + ) + } - val upOrLeft = - Swipe.Resolved( - direction = - when (orientation) { - Orientation.Horizontal -> SwipeDirection.Resolved.Left - Orientation.Vertical -> SwipeDirection.Resolved.Up - }, - pointerCount = pointersDown, - fromSource = fromSource, - ) + private fun resolveSwipe( + pointersDown: Int, + fromSource: SwipeSource.Resolved?, + isUpOrLeft: Boolean, + ): Swipe.Resolved { + return Swipe.Resolved( + direction = + when (orientation) { + Orientation.Horizontal -> + if (isUpOrLeft) { + SwipeDirection.Resolved.Left + } else { + SwipeDirection.Resolved.Right + } - val downOrRight = - Swipe.Resolved( - direction = - when (orientation) { - Orientation.Horizontal -> SwipeDirection.Resolved.Right - Orientation.Vertical -> SwipeDirection.Resolved.Down - }, - pointerCount = pointersDown, - fromSource = fromSource, - ) + Orientation.Vertical -> + if (isUpOrLeft) { + SwipeDirection.Resolved.Up + } else { + SwipeDirection.Resolved.Down + } + }, + pointerCount = pointersDown, + fromSource = fromSource, + ) + } + + private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes { + val fromSource = resolveSwipeSource(startedPosition) + val upOrLeft = resolveSwipe(pointersDown, fromSource, isUpOrLeft = true) + val downOrRight = resolveSwipe(pointersDown, fromSource, isUpOrLeft = false) return if (fromSource == null) { Swipes( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt new file mode 100644 index 000000000000..58b59ffd8894 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.communal.ui.viewmodel + +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.SceneFamilies +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableSceneContainer +class CommunalUserActionsViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: CommunalUserActionsViewModel + + @Before + fun setUp() { + underTest = kosmos.communalUserActionsViewModel + underTest.activateIn(testScope) + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun actions_singleShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Single, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) + assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade)) + + setUpState( + isShadeTouchable = false, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Single, + ) + assertThat(actions).isEmpty() + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = true, + shadeMode = ShadeMode.Single, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) + assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade)) + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun actions_splitShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Split, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade)) + + setUpState( + isShadeTouchable = false, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Split, + ) + assertThat(actions).isEmpty() + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = true, + shadeMode = ShadeMode.Split, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade)) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun actions_dualShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Dual, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo(UserActionResult(Overlays.NotificationsShade)) + + setUpState( + isShadeTouchable = false, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Dual, + ) + assertThat(actions).isEmpty() + + setUpState(isShadeTouchable = true, isDeviceUnlocked = true, shadeMode = ShadeMode.Dual) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo(UserActionResult(Overlays.NotificationsShade)) + } + + private fun TestScope.setUpState( + isShadeTouchable: Boolean, + isDeviceUnlocked: Boolean, + shadeMode: ShadeMode, + ) { + if (isShadeTouchable) { + kosmos.powerInteractor.setAwakeForTest() + } else { + kosmos.powerInteractor.setAsleepForTest() + } + + if (isDeviceUnlocked) { + unlockDevice() + } else { + lockDevice() + } + + if (shadeMode == ShadeMode.Dual) { + assertThat(DualShade.isEnabled).isTrue() + } else { + assertThat(DualShade.isEnabled).isFalse() + kosmos.fakeShadeRepository.setShadeLayoutWide(shadeMode == ShadeMode.Split) + } + runCurrent() + } + + private fun TestScope.lockDevice() { + val deviceUnlockStatus by + collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus) + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + runCurrent() + } + + private fun TestScope.unlockDevice() { + val deviceUnlockStatus by + collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason") + runCurrent() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt index 29035ce2aa0a..d97909a1347e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt @@ -19,17 +19,22 @@ package com.android.systemui.keyguard.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.DismissAction import com.android.systemui.keyguard.shared.model.KeyguardDone +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.domain.interactor.powerInteractor @@ -38,11 +43,13 @@ import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.Transition import com.android.systemui.scene.data.repository.setSceneTransition +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -80,6 +87,7 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { alternateBouncerInteractor = kosmos.alternateBouncerInteractor, shadeInteractor = { kosmos.shadeInteractor }, keyguardInteractor = { kosmos.keyguardInteractor }, + sceneInteractor = { kosmos.sceneInteractor }, ) } @@ -178,7 +186,11 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { ) assertThat(executeDismissAction).isNull() + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) kosmos.setSceneTransition(Idle(Scenes.Gone)) + kosmos.sceneInteractor.changeScene(Scenes.Gone, "") assertThat(executeDismissAction).isNotNull() } @@ -301,4 +313,78 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { underTest.setKeyguardDone(KeyguardDone.IMMEDIATE) assertThat(keyguardDoneTiming).isEqualTo(KeyguardDone.IMMEDIATE) } + + @Test + @EnableSceneContainer + fun dismissAction_executesBeforeItsReset_sceneContainerOn_swipeAuth_fromQsScene() = + testScope.runTest { + val canSwipeToEnter by collectLastValue(kosmos.deviceEntryInteractor.canSwipeToEnter) + val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene) + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(currentScene!!) + ) + kosmos.sceneInteractor.setTransitionState(transitionState) + val executeDismissAction by collectLastValue(underTest.executeDismissAction) + val resetDismissAction by collectLastValue(underTest.resetDismissAction) + assertThat(executeDismissAction).isNull() + assertThat(resetDismissAction).isNull() + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + assertThat(canSwipeToEnter).isTrue() + kosmos.sceneInteractor.changeScene(Scenes.QuickSettings, "") + transitionState.value = ObservableTransitionState.Idle(Scenes.QuickSettings) + assertThat(currentScene).isEqualTo(Scenes.QuickSettings) + + assertThat(executeDismissAction).isNull() + assertThat(resetDismissAction).isNull() + + val dismissAction = + DismissAction.RunImmediately( + onDismissAction = { KeyguardDone.LATER }, + onCancelAction = {}, + message = "message", + willAnimateOnLockscreen = true, + ) + underTest.setDismissAction(dismissAction) + // Should still be null because the transition to Gone has not yet happened. + assertThat(executeDismissAction).isNull() + assertThat(resetDismissAction).isNull() + + transitionState.value = + ObservableTransitionState.Transition.ChangeScene( + fromScene = Scenes.QuickSettings, + toScene = Scenes.Gone, + currentScene = flowOf(Scenes.QuickSettings), + currentOverlays = emptySet(), + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + runCurrent() + assertThat(executeDismissAction).isNull() + assertThat(resetDismissAction).isNull() + + transitionState.value = + ObservableTransitionState.Transition.ChangeScene( + fromScene = Scenes.QuickSettings, + toScene = Scenes.Gone, + currentScene = flowOf(Scenes.Gone), + currentOverlays = emptySet(), + progress = flowOf(1f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + kosmos.sceneInteractor.changeScene(Scenes.Gone, "") + assertThat(currentScene).isEqualTo(Scenes.Gone) + runCurrent() + assertThat(executeDismissAction).isNotNull() + assertThat(resetDismissAction).isNull() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt index a330cf01624f..fb1bf281715d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt @@ -147,7 +147,7 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { } } - private fun expectedLeftDestination( + private fun expectedStartDestination( isCommunalAvailable: Boolean, isShadeTouchable: Boolean, ): SceneKey? { @@ -246,17 +246,17 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { ) ) - val leftScene by + val startScene by collectLastValue( - (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let { - scene -> - kosmos.sceneInteractor.resolveSceneFamily(scene) - } ?: flowOf(null) + (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene) + ?.toScene + ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) } + ?: flowOf(null) ) - assertThat(leftScene) + assertThat(startScene) .isEqualTo( - expectedLeftDestination( + expectedStartDestination( isCommunalAvailable = isCommunalAvailable, isShadeTouchable = isShadeTouchable, ) @@ -341,17 +341,17 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() { ) ) - val leftScene by + val startScene by collectLastValue( - (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let { - scene -> - kosmos.sceneInteractor.resolveSceneFamily(scene) - } ?: flowOf(null) + (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene) + ?.toScene + ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) } + ?: flowOf(null) ) - assertThat(leftScene) + assertThat(startScene) .isEqualTo( - expectedLeftDestination( + expectedStartDestination( isCommunalAvailable = isCommunalAvailable, isShadeTouchable = isShadeTouchable, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt new file mode 100644 index 000000000000..4bbdfa44e087 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.composefragment.viewmodel + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.testing.TestLifecycleOwner +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.testKosmos +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before + +@OptIn(ExperimentalCoroutinesApi::class) +abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() { + protected val kosmos = testKosmos() + + protected val lifecycleOwner = + TestLifecycleOwner( + initialState = Lifecycle.State.CREATED, + coroutineDispatcher = kosmos.testDispatcher, + ) + + protected val underTest by lazy { + kosmos.qsFragmentComposeViewModelFactory.create(lifecycleOwner.lifecycleScope) + } + + @Before + fun setUp() { + Dispatchers.setMain(kosmos.testDispatcher) + } + + @After + fun teardown() { + Dispatchers.resetMain() + } + + protected inline fun TestScope.testWithinLifecycle( + crossinline block: suspend TestScope.() -> TestResult + ): TestResult { + return runTest { + lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED) + lifecycleOwner.lifecycleScope.launch { underTest.activate() } + block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt new file mode 100644 index 000000000000..acd69af2736a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.composefragment.viewmodel + +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.sysuiStatusBarStateController +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@SmallTest +@RunWith(Parameterized::class) +@RunWithLooper +class QSFragmentComposeViewModelForceQSTest(private val testData: TestData) : + AbstractQSFragmentComposeViewModelTest() { + + @Test + fun forceQs_orRealExpansion() = + with(kosmos) { + testScope.testWithinLifecycle { + val expansionState by collectLastValue(underTest.expansionState) + + with(testData) { + sysuiStatusBarStateController.setState(statusBarState) + underTest.isQSExpanded = expanded + underTest.isStackScrollerOverscrolling = stackScrollerOverScrolling + fakeDeviceEntryRepository.setBypassEnabled(bypassEnabled) + underTest.isTransitioningToFullShade = transitioningToFullShade + underTest.isInSplitShade = inSplitShade + + underTest.qsExpansionValue = EXPANSION + assertThat(expansionState!!.progress) + .isEqualTo(if (expectedForceQS) 1f else EXPANSION) + } + } + } + + data class TestData( + val statusBarState: Int, + val expanded: Boolean, + val stackScrollerOverScrolling: Boolean, + val bypassEnabled: Boolean, + val transitioningToFullShade: Boolean, + val inSplitShade: Boolean, + ) { + private val inKeyguard = statusBarState == StatusBarState.KEYGUARD + + private val showCollapsedOnKeyguard = + bypassEnabled || (transitioningToFullShade && !inSplitShade) + + val expectedForceQS = + (expanded || stackScrollerOverScrolling) && (inKeyguard && !showCollapsedOnKeyguard) + } + + companion object { + private const val EXPANSION = 0.3f + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun createTestData(): List<TestData> { + return statusBarStates.flatMap { statusBarState -> + (0u..31u).map { bitfield -> + TestData( + statusBarState, + expanded = (bitfield and 1u) == 1u, + stackScrollerOverScrolling = (bitfield and 2u) == 2u, + bypassEnabled = (bitfield and 4u) == 4u, + transitioningToFullShade = (bitfield and 8u) == 8u, + inSplitShade = (bitfield and 16u) == 16u, + ) + } + } + } + + private val statusBarStates = + setOf(StatusBarState.SHADE, StatusBarState.KEYGUARD, StatusBarState.SHADE_LOCKED) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt index 6f20e70f84a8..c19e4b834c7c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt @@ -19,64 +19,28 @@ package com.android.systemui.qs.composefragment.viewmodel import android.app.StatusBarManager import android.content.testableContext import android.testing.TestableLooper.RunWithLooper -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.qs.fgsManagerController +import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor import com.android.systemui.res.R import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.sysuiStatusBarStateController -import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestResult -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper -@OptIn(ExperimentalCoroutinesApi::class) -class QSFragmentComposeViewModelTest : SysuiTestCase() { - private val kosmos = testKosmos() - - private val lifecycleOwner = - TestLifecycleOwner( - initialState = Lifecycle.State.CREATED, - coroutineDispatcher = kosmos.testDispatcher, - ) - - private val underTest by lazy { - kosmos.qsFragmentComposeViewModelFactory.create(lifecycleOwner.lifecycleScope) - } - - @Before - fun setUp() { - Dispatchers.setMain(kosmos.testDispatcher) - } - - @After - fun teardown() { - Dispatchers.resetMain() - } +class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() { @Test fun qsExpansionValueChanges_correctExpansionState() = @@ -205,16 +169,30 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { } } - private inline fun TestScope.testWithinLifecycle( - crossinline block: suspend TestScope.() -> TestResult - ): TestResult { - return runTest { - lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED) - block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) } + @Test + fun squishinessInExpansion_setInInteractor() = + with(kosmos) { + testScope.testWithinLifecycle { + val squishiness by collectLastValue(tileSquishinessInteractor.squishiness) + + underTest.squishinessFractionValue = 0.3f + assertThat(squishiness).isWithin(epsilon).of(0.3f.constrainSquishiness()) + + underTest.squishinessFractionValue = 0f + assertThat(squishiness).isWithin(epsilon).of(0f.constrainSquishiness()) + + underTest.squishinessFractionValue = 1f + assertThat(squishiness).isWithin(epsilon).of(1f.constrainSquishiness()) + } } - } companion object { private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS + + private fun Float.constrainSquishiness(): Float { + return (0.1f + this * 0.9f).coerceIn(0f, 1f) + } + + private const val epsilon = 0.001f } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt index 9e90090549dd..a9a527fb8df6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt @@ -22,10 +22,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testScope import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository -import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout +import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel -import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel -import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -44,8 +42,7 @@ class InfiniteGridLayoutTest : SysuiTestCase() { } } - private val underTest = - with(kosmos) { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) } + private val underTest = kosmos.infiniteGridLayout @Test fun correctPagination_underOnePage_sameOrder() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index c9e958dd1cc0..d2bf9b888ef0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -50,9 +50,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor -import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver import com.android.systemui.scene.domain.startable.sceneContainerStartable -import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel @@ -185,7 +183,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { val actions by collectLastValue(kosmos.shadeUserActionsViewModel.actions) - val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) kosmos.assertCurrentScene(Scenes.Lockscreen) @@ -195,9 +192,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene - assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home) - assertThat(homeScene).isEqualTo(Scenes.Lockscreen) - kosmos.emulateUserDrivenTransition(to = homeScene) + assertThat(upDestinationSceneKey).isEqualTo(Scenes.Lockscreen) + kosmos.emulateUserDrivenTransition(to = Scenes.Lockscreen) } @Test @@ -205,7 +201,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { val actions by collectLastValue(kosmos.shadeUserActionsViewModel.actions) val canSwipeToEnter by collectLastValue(kosmos.deviceEntryInteractor.canSwipeToEnter) - val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) @@ -222,9 +217,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene - assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home) - assertThat(homeScene).isEqualTo(Scenes.Gone) - kosmos.emulateUserDrivenTransition(to = homeScene) + assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) + kosmos.emulateUserDrivenTransition(to = Scenes.Gone) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt new file mode 100644 index 000000000000..664315d19494 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.ui.viewmodel + +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.HapticFeedbackConstants +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.ObservableTransitionState.Transition.ShowOrHideOverlay +import com.android.compose.animation.scene.OverlayKey +import com.android.compose.animation.scene.SceneKey +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.haptics.msdl.fakeMSDLPlayer +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.sceneContainerHapticsViewModelFactory +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.testKosmos +import com.google.android.msdl.data.model.MSDLToken +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableSceneContainer +class SceneContainerHapticsViewModelTest() : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val msdlPlayer = kosmos.fakeMSDLPlayer + private val view = mock<View>() + + private lateinit var underTest: SceneContainerHapticsViewModel + + @Before + fun setup() { + underTest = kosmos.sceneContainerHapticsViewModelFactory.create(view) + underTest.activateIn(testScope) + } + + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + @DisableFlags(Flags.FLAG_DUAL_SHADE) + @Test + fun onValidSceneTransition_withMSDL_playsMSDLShadePullHaptics() = + testScope.runTest { + // GIVEN a valid scene transition to play haptics + val validTransition = createTransitionState(from = Scenes.Gone, to = Scenes.Shade) + + // WHEN the transition occurs + sceneInteractor.setTransitionState(MutableStateFlow(validTransition)) + runCurrent() + + // THEN the expected token plays without interaction properties + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR) + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + @DisableFlags(Flags.FLAG_DUAL_SHADE) + @Test + fun onInValidSceneTransition_withMSDL_doesNotPlayMSDLShadePullHaptics() = + testScope.runTest { + // GIVEN an invalid scene transition to play haptics + val invalidTransition = createTransitionState(from = Scenes.Shade, to = Scenes.Gone) + + // WHEN the transition occurs + sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition)) + runCurrent() + + // THEN the no token plays with no interaction properties + assertThat(msdlPlayer.latestTokenPlayed).isNull() + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + + @DisableFlags(Flags.FLAG_DUAL_SHADE, Flags.FLAG_MSDL_FEEDBACK) + @Test + fun onValidSceneTransition_withoutMSDL_playsHapticConstantForShadePullHaptics() = + testScope.runTest { + // GIVEN a valid scene transition to play haptics + val validTransition = createTransitionState(from = Scenes.Gone, to = Scenes.Shade) + + // WHEN the transition occurs + sceneInteractor.setTransitionState(MutableStateFlow(validTransition)) + runCurrent() + + // THEN the expected haptic feedback constant plays + verify(view).performHapticFeedback(eq(HapticFeedbackConstants.GESTURE_START)) + } + + @DisableFlags(Flags.FLAG_DUAL_SHADE, Flags.FLAG_MSDL_FEEDBACK) + @Test + fun onInValidSceneTransition_withoutMSDL_doesNotPlayHapticConstantForShadePullHaptics() = + testScope.runTest { + // GIVEN an invalid scene transition to play haptics + val invalidTransition = createTransitionState(from = Scenes.Shade, to = Scenes.Gone) + + // WHEN the transition occurs + sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition)) + runCurrent() + + // THEN the view does not play a haptic feedback constant + verifyZeroInteractions(view) + } + + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK, Flags.FLAG_DUAL_SHADE) + @Test + fun onValidOverlayTransition_withMSDL_playsMSDLShadePullHaptics() = + testScope.runTest { + // GIVEN a valid scene transition to play haptics + val validTransition = + createTransitionState(from = Scenes.Gone, to = Overlays.NotificationsShade) + + // WHEN the transition occurs + sceneInteractor.setTransitionState(MutableStateFlow(validTransition)) + runCurrent() + + // THEN the expected token plays without interaction properties + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR) + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK, Flags.FLAG_DUAL_SHADE) + @Test + fun onInValidOverlayTransition_withMSDL_doesNotPlayMSDLShadePullHaptics() = + testScope.runTest { + // GIVEN an invalid scene transition to play haptics + val invalidTransition = + createTransitionState(from = Scenes.Bouncer, to = Overlays.NotificationsShade) + + // WHEN the transition occurs + sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition)) + runCurrent() + + // THEN the no token plays with no interaction properties + assertThat(msdlPlayer.latestTokenPlayed).isNull() + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + + @EnableFlags(Flags.FLAG_DUAL_SHADE) + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) + @Test + fun onValidOverlayTransition_withoutMSDL_playsHapticConstantForShadePullHaptics() = + testScope.runTest { + // GIVEN a valid scene transition to play haptics + val validTransition = + createTransitionState(from = Scenes.Gone, to = Overlays.NotificationsShade) + + // WHEN the transition occurs + sceneInteractor.setTransitionState(MutableStateFlow(validTransition)) + runCurrent() + + // THEN the expected haptic feedback constant plays + verify(view).performHapticFeedback(eq(HapticFeedbackConstants.GESTURE_START)) + } + + @EnableFlags(Flags.FLAG_DUAL_SHADE) + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) + @Test + fun onInValidOverlayTransition_withoutMSDL_doesNotPlayHapticConstantForShadePullHaptics() = + testScope.runTest { + // GIVEN an invalid scene transition to play haptics + val invalidTransition = + createTransitionState(from = Scenes.Bouncer, to = Overlays.NotificationsShade) + + // WHEN the transition occurs + sceneInteractor.setTransitionState(MutableStateFlow(invalidTransition)) + runCurrent() + + // THEN the view does not play a haptic feedback constant + verifyZeroInteractions(view) + } + + private fun createTransitionState(from: SceneKey, to: ContentKey) = + when (to) { + is SceneKey -> + ObservableTransitionState.Transition( + fromScene = from, + toScene = to, + currentScene = flowOf(from), + progress = MutableStateFlow(0.2f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + is OverlayKey -> + ShowOrHideOverlay( + overlay = to, + fromContent = from, + toContent = to, + currentScene = from, + currentOverlays = sceneInteractor.currentOverlays, + progress = MutableStateFlow(0.2f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index e60e742e9917..a37f511cef69 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -21,23 +21,21 @@ package com.android.systemui.scene.ui.viewmodel import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.MotionEvent +import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.DefaultEdgeDetector import com.android.systemui.SysuiTestCase -import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.data.repository.fakePowerRepository -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.fakeOverlaysByKeys import com.android.systemui.scene.sceneContainerConfig -import com.android.systemui.scene.sceneContainerGestureFilterFactory -import com.android.systemui.scene.shared.logger.sceneLogger +import com.android.systemui.scene.sceneContainerViewModelFactory import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource @@ -72,6 +70,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository } private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig } private val falsingManager by lazy { kosmos.fakeFalsingManager } + private val view = mock<View>() private lateinit var underTest: SceneContainerViewModel @@ -81,16 +80,10 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Before fun setUp() { underTest = - SceneContainerViewModel( - sceneInteractor = sceneInteractor, - falsingInteractor = kosmos.falsingInteractor, - powerInteractor = kosmos.powerInteractor, - shadeInteractor = kosmos.shadeInteractor, - splitEdgeDetector = kosmos.splitEdgeDetector, - logger = kosmos.sceneLogger, - gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory, - displayId = kosmos.displayTracker.defaultDisplayId, - motionEventHandlerReceiver = { motionEventHandler -> + kosmos.sceneContainerViewModelFactory.create( + view, + kosmos.displayTracker.defaultDisplayId, + { motionEventHandler -> this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler }, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt index 9f3e126ed1e8..15d68813808c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.qs.ui.adapter.fakeQSSceneAdapter import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver +import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade @@ -76,6 +77,7 @@ class ShadeUserActionsViewModelTest : SysuiTestCase() { @Before fun setUp() { + kosmos.sceneContainerStartable.start() underTest.activateIn(testScope) } @@ -232,6 +234,20 @@ class ShadeUserActionsViewModelTest : SysuiTestCase() { .isEmpty() } + @Test + fun upTransitionSceneKey_backToCommunal() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + kosmos.sceneInteractor.changeScene(Scenes.Communal, "") + assertThat(currentScene).isEqualTo(Scenes.Communal) + kosmos.sceneInteractor.changeScene(Scenes.Shade, "") + assertThat(currentScene).isEqualTo(Scenes.Shade) + + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Communal)) + } + private fun TestScope.setDeviceEntered(isEntered: Boolean) { if (isEntered) { // Unlock the device marking the device has entered. diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java index 55ccaa68c855..92bc95af5931 100644 --- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java +++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java @@ -70,4 +70,12 @@ public interface CoreStartable extends Dumpable { * {@link #onBootCompleted()} will never be called before {@link #start()}. */ default void onBootCompleted() { } + + /** No op implementation that can be used when feature flagging on the Dagger Module level. */ + CoreStartable NOP = new Nop(); + + class Nop implements CoreStartable { + @Override + public void start() {} + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt new file mode 100644 index 000000000000..e35fdfe9087c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.viewmodel + +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor +import com.android.systemui.scene.shared.model.SceneFamilies +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.shade.ui.viewmodel.dualShadeActions +import com.android.systemui.shade.ui.viewmodel.singleShadeActions +import com.android.systemui.shade.ui.viewmodel.splitShadeActions +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** Provides scene container user actions and results. */ +class CommunalUserActionsViewModel +@AssistedInject +constructor( + private val deviceUnlockedInteractor: DeviceUnlockedInteractor, + private val shadeInteractor: ShadeInteractor, +) : UserActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + shadeInteractor.isShadeTouchable + .flatMapLatestConflated { isShadeTouchable -> + if (!isShadeTouchable) { + flowOf(emptyMap()) + } else { + combine( + deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }, + shadeInteractor.shadeMode, + ) { isDeviceUnlocked, shadeMode -> + buildList { + val bouncerOrGone = + if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer + add(Swipe.Up to bouncerOrGone) + + // "Home" is either Lockscreen, or Gone - if the device is entered. + add(Swipe.End to SceneFamilies.Home) + + addAll( + when (shadeMode) { + ShadeMode.Single -> singleShadeActions() + ShadeMode.Split -> splitShadeActions() + ShadeMode.Dual -> dualShadeActions() + } + ) + } + .associate { it } + } + } + } + .collect { setActions(it) } + } + + @AssistedFactory + interface Factory { + fun create(): CommunalUserActionsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt index 18b14951ee70..258232b30670 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardDone import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -64,6 +65,7 @@ constructor( alternateBouncerInteractor: AlternateBouncerInteractor, shadeInteractor: Lazy<ShadeInteractor>, keyguardInteractor: Lazy<KeyguardInteractor>, + sceneInteractor: Lazy<SceneInteractor>, ) { val dismissAction: Flow<DismissAction> = repository.dismissAction @@ -125,7 +127,20 @@ constructor( val executeDismissAction: Flow<() -> KeyguardDone> = merge( - finishedTransitionToGone, + if (SceneContainerFlag.isEnabled) { + // Using currentScene instead of finishedTransitionToGone because of a race + // condition that forms between finishedTransitionToGone and + // isOnShadeWhileUnlocked where the latter emits false before the former emits + // true, causing the merge to not emit until it's too late. + sceneInteractor + .get() + .currentScene + .map { it == Scenes.Gone } + .distinctUntilChanged() + .filter { it } + } else { + finishedTransitionToGone + }, isOnShadeWhileUnlocked.filter { it }.map {}, dismissInteractor.dismissKeyguardRequestWithImmediateDismissAction, ) @@ -135,10 +150,24 @@ constructor( val resetDismissAction: Flow<Unit> = combine( - transitionInteractor.isFinishedIn( - scene = Scenes.Gone, - stateWithoutSceneContainer = GONE, - ), + if (SceneContainerFlag.isEnabled) { + // Using currentScene instead of isFinishedIn because of a race condition that + // forms between isFinishedIn(Gone) and isOnShadeWhileUnlocked where the latter + // emits false before the former emits true, causing the evaluation of the + // combine to come up with true, temporarily, before settling on false, which is + // a valid final state. That causes an incorrect reset of the dismiss action to + // occur before it gets executed. + sceneInteractor + .get() + .currentScene + .map { it == Scenes.Gone } + .distinctUntilChanged() + } else { + transitionInteractor.isFinishedIn( + scene = Scenes.Gone, + stateWithoutSceneContainer = GONE, + ) + }, transitionInteractor.isFinishedIn( scene = Scenes.Bouncer, stateWithoutSceneContainer = PRIMARY_BOUNCER, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt index 3b266f945aab..6f29004d4f3f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt @@ -18,20 +18,18 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor -import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.shade.ui.viewmodel.dualShadeActions +import com.android.systemui.shade.ui.viewmodel.singleShadeActions +import com.android.systemui.shade.ui.viewmodel.splitShadeActions import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -62,7 +60,7 @@ constructor( ) { isDeviceUnlocked, isCommunalAvailable, shadeMode -> buildList { if (isCommunalAvailable) { - add(Swipe.Left to Scenes.Communal) + add(Swipe.Start to Scenes.Communal) } add(Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer) @@ -81,45 +79,6 @@ constructor( .collect { setActions(it) } } - private fun singleShadeActions(): Array<Pair<UserAction, UserActionResult>> { - return arrayOf( - // Swiping down, not from the edge, always goes to shade. - Swipe.Down to Scenes.Shade, - swipeDown(pointerCount = 2) to Scenes.Shade, - // Swiping down from the top edge goes to QS. - swipeDownFromTop(pointerCount = 1) to Scenes.QuickSettings, - swipeDownFromTop(pointerCount = 2) to Scenes.QuickSettings, - ) - } - - private fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> { - val splitShadeSceneKey = UserActionResult(Scenes.Shade, ToSplitShade) - return arrayOf( - // Swiping down, not from the edge, always goes to shade. - Swipe.Down to splitShadeSceneKey, - swipeDown(pointerCount = 2) to splitShadeSceneKey, - // Swiping down from the top edge goes to QS. - swipeDownFromTop(pointerCount = 1) to splitShadeSceneKey, - swipeDownFromTop(pointerCount = 2) to splitShadeSceneKey, - ) - } - - private fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> { - return arrayOf( - Swipe.Down to Overlays.NotificationsShade, - Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to - Overlays.QuickSettingsShade, - ) - } - - private fun swipeDownFromTop(pointerCount: Int): Swipe { - return Swipe(SwipeDirection.Down, fromSource = Edge.Top, pointerCount = pointerCount) - } - - private fun swipeDown(pointerCount: Int): Swipe { - return Swipe(SwipeDirection.Down, pointerCount = pointerCount) - } - @AssistedFactory interface Factory { fun create(): LockscreenUserActionsViewModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index ba0d9384c7a4..66ac01ab95a0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -180,6 +180,7 @@ constructor( qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS) qsMediaHost.init(MediaHierarchyManager.LOCATION_QS) setListenerCollections() + lifecycleScope.launch { viewModel.activate() } } override fun onCreateView( @@ -331,7 +332,7 @@ constructor( } override fun setOverscrolling(overscrolling: Boolean) { - viewModel.stackScrollerOverscrollingValue = overscrolling + viewModel.isStackScrollerOverscrolling = overscrolling } override fun setExpanded(qsExpanded: Boolean) { @@ -410,11 +411,11 @@ constructor( qsTransitionFraction: Float, qsSquishinessFraction: Float, ) { - super.setTransitionToFullShadeProgress( - isTransitioningToFullShade, - qsTransitionFraction, - qsSquishinessFraction, - ) + viewModel.isTransitioningToFullShade = isTransitioningToFullShade + viewModel.lockscreenToShadeProgressValue = qsTransitionFraction + if (isTransitioningToFullShade) { + viewModel.squishinessFractionValue = qsSquishinessFraction + } } override fun setFancyClipping( diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index 7300ee1053ff..2d4e358414d5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -24,16 +24,19 @@ import androidx.lifecycle.LifecycleCoroutineScope import com.android.systemui.Dumpable import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.FooterActionsController +import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel.QSExpansionState import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel +import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository -import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.util.LargeScreenUtils import com.android.systemui.util.asIndenting import com.android.systemui.util.printSection @@ -50,6 +53,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -61,13 +65,14 @@ constructor( private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, private val sysuiStatusBarStateController: SysuiStatusBarStateController, - private val keyguardBypassController: KeyguardBypassController, + private val deviceEntryInteractor: DeviceEntryInteractor, private val disableFlagsRepository: DisableFlagsRepository, private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator, private val configurationInteractor: ConfigurationInteractor, private val largeScreenHeaderHelper: LargeScreenHeaderHelper, + private val squishinessInteractor: TileSquishinessInteractor, @Assisted private val lifecycleScope: LifecycleCoroutineScope, -) : Dumpable { +) : Dumpable, ExclusiveActivatable() { val footerActionsViewModel = footerActionsViewModelFactory.create(lifecycleScope).also { lifecycleScope.launch { footerActionsController.init() } @@ -110,7 +115,7 @@ constructor( _panelFraction.value = value } - private val _squishinessFraction = MutableStateFlow(0f) + private val _squishinessFraction = MutableStateFlow(1f) var squishinessFractionValue: Float get() = _squishinessFraction.value set(value) { @@ -131,7 +136,7 @@ constructor( private val _headerAnimating = MutableStateFlow(false) private val _stackScrollerOverscrolling = MutableStateFlow(false) - var stackScrollerOverscrollingValue: Boolean + var isStackScrollerOverscrolling: Boolean get() = _stackScrollerOverscrolling.value set(value) { _stackScrollerOverscrolling.value = value @@ -150,8 +155,6 @@ constructor( disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(), ) - private val _showCollapsedOnKeyguard = MutableStateFlow(false) - private val _keyguardAndExpanded = MutableStateFlow(false) /** @@ -177,21 +180,65 @@ constructor( awaitClose { sysuiStatusBarStateController.removeCallback(callback) } } + .onStart { emit(sysuiStatusBarStateController.state) } .stateIn( lifecycleScope, SharingStarted.WhileSubscribed(), sysuiStatusBarStateController.state, ) + private val isKeyguardState = + statusBarState + .map { it == StatusBarState.KEYGUARD } + .stateIn( + lifecycleScope, + SharingStarted.WhileSubscribed(), + statusBarState.value == StatusBarState.KEYGUARD, + ) + private val _viewHeight = MutableStateFlow(0) private val _headerTranslation = MutableStateFlow(0f) private val _inSplitShade = MutableStateFlow(false) + var isInSplitShade: Boolean + get() = _inSplitShade.value + set(value) { + _inSplitShade.value = value + } private val _transitioningToFullShade = MutableStateFlow(false) + var isTransitioningToFullShade: Boolean + get() = _transitioningToFullShade.value + set(value) { + _transitioningToFullShade.value = value + } - private val _lockscreenToShadeProgress = MutableStateFlow(false) + private val isBypassEnabled = deviceEntryInteractor.isBypassEnabled + + private val showCollapsedOnKeyguard = + combine( + isBypassEnabled, + _transitioningToFullShade, + _inSplitShade, + ::calculateShowCollapsedOnKeyguard, + ) + .stateIn( + lifecycleScope, + SharingStarted.WhileSubscribed(), + calculateShowCollapsedOnKeyguard( + isBypassEnabled.value, + isTransitioningToFullShade, + isInSplitShade, + ), + ) + + private val _lockscreenToShadeProgress = MutableStateFlow(0.0f) + var lockscreenToShadeProgressValue: Float + get() = _lockscreenToShadeProgress.value + set(value) { + _lockscreenToShadeProgress.value = value + } private val _overscrolling = MutableStateFlow(false) @@ -212,12 +259,32 @@ constructor( _heightOverride.value = value } + private val forceQS = + combine( + _qsExpanded, + _stackScrollerOverscrolling, + isKeyguardState, + showCollapsedOnKeyguard, + ::calculateForceQs, + ) + .stateIn( + lifecycleScope, + SharingStarted.WhileSubscribed(), + calculateForceQs( + isQSExpanded, + isStackScrollerOverscrolling, + isKeyguardState.value, + showCollapsedOnKeyguard.value, + ), + ) + val expansionState: StateFlow<QSExpansionState> = - combine(_stackScrollerOverscrolling, _qsExpanded, _qsExpansion) { args: Array<Any> -> - val expansion = args[2] as Float - QSExpansionState(expansion.coerceIn(0f, 1f)) - } - .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState(0f)) + combine(_qsExpansion, forceQS, ::calculateExpansionState) + .stateIn( + lifecycleScope, + SharingStarted.WhileSubscribed(), + calculateExpansionState(_qsExpansion.value, forceQS.value), + ) /** * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for @@ -225,6 +292,16 @@ constructor( */ var collapseExpandAccessibilityAction: Runnable? = null + override suspend fun onActivated(): Nothing { + hydrateSquishinessInteractor() + } + + private suspend fun hydrateSquishinessInteractor(): Nothing { + _squishinessFraction.collect { + squishinessInteractor.setSquishinessValue(it.constrainSquishiness()) + } + } + override fun dump(pw: PrintWriter, args: Array<out String>) { pw.asIndenting().run { printSection("Quick Settings state") { @@ -238,13 +315,17 @@ constructor( println("panelExpansionFraction", panelExpansionFractionValue) println("squishinessFraction", squishinessFractionValue) println("expansionState", expansionState.value) + println("forceQS", forceQS.value) } printSection("Shade state") { - println("stackOverscrolling", stackScrollerOverscrollingValue) + println("stackOverscrolling", isStackScrollerOverscrolling) println("statusBarState", StatusBarState.toString(statusBarState.value)) + println("isKeyguardState", isKeyguardState.value) println("isSmallScreen", isSmallScreenValue) println("heightOverride", "${heightOverrideValue}px") println("qqsHeaderHeight", "${qqsHeaderHeight.value}px") + println("isSplitShade", isInSplitShade) + println("showCollapsedOnKeyguard", showCollapsedOnKeyguard.value) } } } @@ -257,3 +338,35 @@ constructor( // In the future, this will have other relevant elements like squishiness. data class QSExpansionState(@FloatRange(0.0, 1.0) val progress: Float) } + +private fun Float.constrainSquishiness(): Float { + return (0.1f + this * 0.9f).coerceIn(0f, 1f) +} + +// Helper methods for combining flows. + +private fun calculateExpansionState(expansion: Float, forceQs: Boolean): QSExpansionState { + return if (forceQs) { + QSExpansionState(1f) + } else { + QSExpansionState(expansion.coerceIn(0f, 1f)) + } +} + +private fun calculateForceQs( + isQSExpanded: Boolean, + isStackOverScrolling: Boolean, + isKeyguardShowing: Boolean, + shouldShowCollapsedOnKeyguard: Boolean, +): Boolean { + return (isQSExpanded || isStackOverScrolling) && + (isKeyguardShowing && !shouldShowCollapsedOnKeyguard) +} + +private fun calculateShowCollapsedOnKeyguard( + isBypassEnabled: Boolean, + isTransitioningToFullShade: Boolean, + isInSplitShade: Boolean, +): Boolean { + return isBypassEnabled || (isTransitioningToFullShade && !isInSplitShade) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepository.kt new file mode 100644 index 000000000000..76ba9af2f475 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepository.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.data.repository + +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class TileSquishinessRepository @Inject constructor() { + private val _squishiness = MutableStateFlow(1f) + val squishiness = _squishiness.asStateFlow() + + fun setSquishinessValue(value: Float) { + _squishiness.value = value + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractor.kt new file mode 100644 index 000000000000..4fdbc7647c78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractor.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.panels.data.repository.TileSquishinessRepository +import javax.inject.Inject + +@SysUISingleton +class TileSquishinessInteractor +@Inject +constructor(private val repository: TileSquishinessRepository) { + val squishiness = repository.squishiness + + fun setSquishinessValue(value: Float) { + repository.setSquishinessValue(value) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index 8998a7f5d815..a645b51404e7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -41,6 +41,7 @@ fun SceneScope.QuickQuickSettings( val sizedTiles by viewModel.tileViewModels.collectAsStateWithLifecycle(initialValue = emptyList()) val tiles = sizedTiles.fastMap { it.tile } + val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() DisposableEffect(tiles) { val token = Any() @@ -62,6 +63,7 @@ fun SceneScope.QuickQuickSettings( tile = it.tile, iconOnly = it.isIcon, modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), + squishiness = { squishiness }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 8c2fb252d13c..bf4c113e3ae9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -73,6 +73,7 @@ fun LargeTileContent( secondaryLabel: String?, icon: Icon, colors: TileColors, + squishiness: () -> Float, accessibilityUiState: AccessibilityUiState? = null, toggleClickSupported: Boolean = false, iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), @@ -89,6 +90,7 @@ fun LargeTileContent( modifier = Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) { Modifier.clip(iconShape) + .verticalSquish(squishiness) .background(colors.iconBackground, { 1f }) .combinedClickable( onClick = onClick, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index e6edba513189..3ba49add530e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -33,6 +33,7 @@ import com.android.systemui.qs.panels.ui.compose.rememberEditListState import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel +import com.android.systemui.qs.panels.ui.viewmodel.TileSquishinessViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey @@ -45,6 +46,7 @@ class InfiniteGridLayout constructor( private val iconTilesViewModel: IconTilesViewModel, private val gridSizeViewModel: FixedColumnsSizeViewModel, + private val squishinessViewModel: TileSquishinessViewModel, ) : PaginatableGridLayout { @Composable @@ -60,6 +62,7 @@ constructor( } val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle() val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) } + val squishiness by squishinessViewModel.squishiness.collectAsStateWithLifecycle() VerticalSpannedGrid( columns = columns, @@ -72,6 +75,7 @@ constructor( tile = it.tile, iconOnly = iconTilesViewModel.isIconTile(it.tile.spec), modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), + squishiness = { squishiness }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt new file mode 100644 index 000000000000..ada1ef4de15d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.ui.compose.infinitegrid + +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import kotlin.math.roundToInt + +/** + * Modifier to squish the vertical bounds of a composable (usually a QS tile). + * + * It will squish the vertical bounds of the inner composable node by the value returned by + * [squishiness] on the measure/layout pass. + * + * The squished composable will be center aligned. + */ +fun Modifier.verticalSquish(squishiness: () -> Float): Modifier { + return layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + val actualHeight = placeable.height + val squishedHeight = actualHeight * squishiness() + // Center the content by moving it UP (squishedHeight < actualHeight) + val scroll = (squishedHeight - actualHeight) / 2 + + layout(placeable.width, squishedHeight.roundToInt()) { + placeable.place(0, scroll.roundToInt()) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index afcbed6db53b..4bd5b2d68c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -98,7 +98,12 @@ fun TileLazyGrid( } @Composable -fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) { +fun Tile( + tile: TileViewModel, + iconOnly: Boolean, + squishiness: () -> Float, + modifier: Modifier = Modifier, +) { val state by tile.state.collectAsStateWithLifecycle(tile.currentState) val resources = resources() val uiState = remember(state, resources) { state.toUiState(resources) } @@ -119,6 +124,7 @@ fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) { onClick = tile::onClick, onLongClick = tile::onLongClick, uiState = uiState, + squishiness = squishiness, modifier = modifier, ) { expandable -> val icon = getTileIcon(icon = uiState.icon) @@ -144,6 +150,7 @@ fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) { }, onLongClick = { tile.onLongClick(expandable) }, accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, ) } } @@ -155,12 +162,17 @@ private fun TileContainer( shape: Shape, iconOnly: Boolean, uiState: TileUiState, + squishiness: () -> Float, modifier: Modifier = Modifier, onClick: (Expandable) -> Unit = {}, onLongClick: (Expandable) -> Unit = {}, content: @Composable BoxScope.(Expandable) -> Unit, ) { - Expandable(color = color, shape = shape, modifier = modifier.clip(shape)) { + Expandable( + color = color, + shape = shape, + modifier = modifier.clip(shape).verticalSquish(squishiness), + ) { val longPressLabel = longPressLabel() Box( modifier = diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt index eee905f9f894..88e3019ba163 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt @@ -42,6 +42,7 @@ constructor( tilesInteractor: CurrentTilesInteractor, fixedColumnsSizeViewModel: FixedColumnsSizeViewModel, quickQuickSettingsRowInteractor: QuickQuickSettingsRowInteractor, + val squishinessViewModel: TileSquishinessViewModel, private val iconTilesViewModel: IconTilesViewModel, @Application private val applicationScope: CoroutineScope, ) { @@ -52,7 +53,7 @@ constructor( quickQuickSettingsRowInteractor.rows.stateIn( applicationScope, SharingStarted.WhileSubscribed(), - quickQuickSettingsRowInteractor.defaultRows + quickQuickSettingsRowInteractor.defaultRows, ) val tileViewModels: StateFlow<List<SizedTile<TileViewModel>>> = @@ -60,12 +61,7 @@ constructor( .flatMapLatest { columns -> tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) -> tiles - .map { - SizedTileImpl( - TileViewModel(it.tile, it.spec), - it.spec.width, - ) - } + .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) } .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() } } } @@ -73,15 +69,10 @@ constructor( applicationScope, SharingStarted.WhileSubscribed(), tilesInteractor.currentTiles.value - .map { - SizedTileImpl( - TileViewModel(it.tile, it.spec), - it.spec.width, - ) - } + .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) } .let { splitInRowsSequence(it, columns.value).take(rows.value).toList().flatten() - } + }, ) private val TileSpec.width: Int diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModel.kt new file mode 100644 index 000000000000..0c4d5de0edf9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModel.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.ui.viewmodel + +import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor +import javax.inject.Inject + +/** View model to track the squishiness of tiles. */ +class TileSquishinessViewModel +@Inject +constructor(tileSquishinessInteractor: TileSquishinessInteractor) { + val squishiness = tileSquishinessInteractor.squishiness +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index a7e7d8bb34dc..a8be5804d04a 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -108,7 +108,11 @@ object SceneWindowRootViewBinder { traceName = "SceneWindowRootViewBinder", minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = { - viewModelFactory.create(view.context.displayId, motionEventHandlerReceiver) + viewModelFactory.create( + view, + view.context.displayId, + motionEventHandlerReceiver, + ) }, ) { viewModel -> try { diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt index 5ff507a45d2e..fc172e8ca1d8 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt @@ -16,16 +16,13 @@ package com.android.systemui.scene.ui.viewmodel -import com.android.compose.animation.scene.Edge -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.shade.ui.viewmodel.dualShadeActions +import com.android.systemui.shade.ui.viewmodel.singleShadeActions +import com.android.systemui.shade.ui.viewmodel.splitShadeActions import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -36,41 +33,21 @@ constructor(private val shadeInteractor: ShadeInteractor) : UserActionsViewModel override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { shadeInteractor.shadeMode.collect { shadeMode -> setActions( - when (shadeMode) { - ShadeMode.Single -> singleShadeActions() - ShadeMode.Split -> splitShadeActions() - ShadeMode.Dual -> dualShadeActions() - } + buildList { + addAll( + when (shadeMode) { + ShadeMode.Single -> + singleShadeActions(requireTwoPointersForTopEdgeForQs = true) + ShadeMode.Split -> splitShadeActions() + ShadeMode.Dual -> dualShadeActions() + } + ) + } + .associate { it } ) } } - private fun singleShadeActions(): Map<UserAction, UserActionResult> { - return mapOf( - Swipe.Down to Scenes.Shade, - swipeDownFromTopWithTwoFingers() to Scenes.QuickSettings, - ) - } - - private fun splitShadeActions(): Map<UserAction, UserActionResult> { - return mapOf( - Swipe.Down to UserActionResult(Scenes.Shade, ToSplitShade), - swipeDownFromTopWithTwoFingers() to UserActionResult(Scenes.Shade, ToSplitShade), - ) - } - - private fun dualShadeActions(): Map<UserAction, UserActionResult> { - return mapOf( - Swipe.Down to Overlays.NotificationsShade, - Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to - Overlays.QuickSettingsShade, - ) - } - - private fun swipeDownFromTopWithTwoFingers(): UserAction { - return Swipe(direction = SwipeDirection.Down, pointerCount = 2, fromSource = Edge.Top) - } - @AssistedFactory interface Factory { fun create(): GoneUserActionsViewModel diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt new file mode 100644 index 000000000000..4ef8e0fc3167 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModel.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.ui.viewmodel + +import android.view.HapticFeedbackConstants +import android.view.View +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.MSDLPlayer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged + +/** + * Models haptics UI state for the scene container. + * + * This model gets a [View] to play haptics using the [View.performHapticFeedback] API. This should + * be the only purpose of this reference. + */ +class SceneContainerHapticsViewModel +@AssistedInject +constructor( + @Assisted private val view: View, + sceneInteractor: SceneInteractor, + shadeInteractor: ShadeInteractor, + private val msdlPlayer: MSDLPlayer, +) : ExclusiveActivatable() { + + /** Should haptics be played by pulling down the shade */ + private val isShadePullHapticsRequired: Flow<Boolean> = + combine(shadeInteractor.isUserInteracting, sceneInteractor.transitionState) { + interacting, + transitionState -> + interacting && transitionState.isValidForShadePullHaptics() + } + .distinctUntilChanged() + + override suspend fun onActivated(): Nothing { + isShadePullHapticsRequired.collect { playShadePullHaptics -> + if (!playShadePullHaptics) return@collect + + if (Flags.msdlFeedback()) { + msdlPlayer.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR) + } else { + view.performHapticFeedback(HapticFeedbackConstants.GESTURE_START) + } + } + awaitCancellation() + } + + private fun ObservableTransitionState.isValidForShadePullHaptics(): Boolean { + val validOrigin = + isTransitioning(from = Scenes.Gone) || isTransitioning(from = Scenes.Lockscreen) + val validDestination = + isTransitioning(to = Scenes.Shade) || + isTransitioning(to = Scenes.QuickSettings) || + isTransitioning(to = Overlays.QuickSettingsShade) || + isTransitioning(to = Overlays.NotificationsShade) + return validOrigin && validDestination + } + + @AssistedFactory + interface Factory { + fun create(view: View): SceneContainerHapticsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 0bf2d499721b..f5053853846c 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -17,8 +17,10 @@ package com.android.systemui.scene.ui.viewmodel import android.view.MotionEvent +import android.view.View import androidx.compose.runtime.getValue import androidx.compose.ui.geometry.Offset +import com.android.app.tracing.coroutines.launch import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.DefaultEdgeDetector import com.android.compose.animation.scene.ObservableTransitionState @@ -60,6 +62,8 @@ constructor( private val splitEdgeDetector: SplitEdgeDetector, private val logger: SceneLogger, gestureFilterFactory: SceneContainerGestureFilter.Factory, + hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory, + @Assisted view: View, @Assisted displayId: Int, @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ) : ExclusiveActivatable() { @@ -72,6 +76,8 @@ constructor( /** Whether the container is visible. */ val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible) + private val hapticsViewModel = hapticsViewModelFactory.create(view) + /** * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the * [UserAction]s for this container. @@ -107,6 +113,7 @@ constructor( coroutineScope { launch { hydrator.activate() } launch { gestureFilter.activate() } + launch("SceneContainerHapticsViewModel") { hapticsViewModel.activate() } } awaitCancellation() } finally { @@ -281,6 +288,7 @@ constructor( @AssistedFactory interface Factory { fun create( + view: View, displayId: Int, motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ): SceneContainerViewModel diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 0c05dbde6117..5896659e9898 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -23,6 +23,7 @@ import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE; import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; +import static com.android.systemui.Flags.msdlFeedback; import static com.android.systemui.Flags.predictiveBackAnimateShade; import static com.android.systemui.Flags.smartspaceRelocateToBottom; import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; @@ -236,6 +237,9 @@ import com.android.wm.shell.animation.FlingAnimationUtils; import dalvik.annotation.optimization.NeverCompile; +import com.google.android.msdl.data.model.MSDLToken; +import com.google.android.msdl.domain.MSDLPlayer; + import kotlin.Unit; import kotlinx.coroutines.CoroutineDispatcher; @@ -312,6 +316,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final StatusBarStateListener mStatusBarStateListener = new StatusBarStateListener(); private final NotificationPanelView mView; private final VibratorHelper mVibratorHelper; + private final MSDLPlayer mMSDLPlayer; private final MetricsLogger mMetricsLogger; private final ConfigurationController mConfigurationController; private final Provider<FlingAnimationUtils.Builder> mFlingAnimationUtilsBuilder; @@ -777,7 +782,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump SplitShadeStateController splitShadeStateController, PowerInteractor powerInteractor, KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm, - NaturalScrollingSettingObserver naturalScrollingSettingObserver) { + NaturalScrollingSettingObserver naturalScrollingSettingObserver, + MSDLPlayer msdlPlayer) { SceneContainerFlag.assertInLegacyMode(); keyguardStateController.addCallback(new KeyguardStateController.Callback() { @Override @@ -855,6 +861,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mNotificationsDragEnabled = mResources.getBoolean( R.bool.config_enableNotificationShadeDrag); mVibratorHelper = vibratorHelper; + mMSDLPlayer = msdlPlayer; mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation); mStatusBarTouchableRegionManager = statusBarTouchableRegionManager; mSystemClock = systemClock; @@ -2911,7 +2918,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } if (!mStatusBarStateController.isDozing()) { - mVibratorHelper.performHapticFeedback(mView, HapticFeedbackConstants.REJECT); + performHapticFeedback(HapticFeedbackConstants.REJECT); } } @@ -3279,7 +3286,20 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } public void performHapticFeedback(int constant) { - mVibratorHelper.performHapticFeedback(mView, constant); + if (msdlFeedback()) { + MSDLToken token; + switch (constant) { + case HapticFeedbackConstants.GESTURE_START -> + token = MSDLToken.SWIPE_THRESHOLD_INDICATOR; + case HapticFeedbackConstants.REJECT -> token = MSDLToken.FAILURE; + default -> token = null; + } + if (token != null) { + mMSDLPlayer.playToken(token, null); + } + } else { + mVibratorHelper.performHapticFeedback(mView, constant); + } } private class ShadeHeadsUpTrackerImpl implements ShadeHeadsUpTracker { @@ -3736,10 +3756,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void maybeVibrateOnOpening(boolean openingWithTouch) { if (mVibrateOnOpening && mBarState != KEYGUARD && mBarState != SHADE_LOCKED) { if (!openingWithTouch || !mHasVibratedOnOpen) { - mVibratorHelper.performHapticFeedback( - mView, - HapticFeedbackConstants.GESTURE_START - ); + performHapticFeedback(HapticFeedbackConstants.GESTURE_START); mHasVibratedOnOpen = true; mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true"); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt new file mode 100644 index 000000000000..65b6231705d4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.ui.viewmodel + +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge + +/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the single shade. */ +fun singleShadeActions( + requireTwoPointersForTopEdgeForQs: Boolean = false +): Array<Pair<UserAction, UserActionResult>> { + return arrayOf( + // Swiping down, not from the edge, always goes to shade. + Swipe.Down to Scenes.Shade, + swipeDown(pointerCount = 2) to Scenes.Shade, + + // Swiping down from the top edge. + swipeDownFromTop(pointerCount = 1) to + if (requireTwoPointersForTopEdgeForQs) { + Scenes.Shade + } else { + Scenes.QuickSettings + }, + swipeDownFromTop(pointerCount = 2) to Scenes.QuickSettings, + ) +} + +/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the split shade. */ +fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> { + val splitShadeSceneKey = UserActionResult(Scenes.Shade, ToSplitShade) + return arrayOf( + // Swiping down, not from the edge, always goes to shade. + Swipe.Down to splitShadeSceneKey, + swipeDown(pointerCount = 2) to splitShadeSceneKey, + // Swiping down from the top edge goes to QS. + swipeDownFromTop(pointerCount = 1) to splitShadeSceneKey, + swipeDownFromTop(pointerCount = 2) to splitShadeSceneKey, + ) +} + +/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the dual shade. */ +fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> { + return arrayOf( + Swipe.Down to Overlays.NotificationsShade, + Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to + Overlays.QuickSettingsShade, + ) +} + +private fun swipeDownFromTop(pointerCount: Int): Swipe { + return Swipe(SwipeDirection.Down, fromSource = Edge.Top, pointerCount = pointerCount) +} + +private fun swipeDown(pointerCount: Int): Swipe { + return Swipe(SwipeDirection.Down, pointerCount = pointerCount) +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt index f8a850a357f1..cc6e8c246ff7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt @@ -16,11 +16,13 @@ package com.android.systemui.shade.ui.viewmodel +import com.android.app.tracing.coroutines.flow.map import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.qs.ui.adapter.QSSceneAdapter +import com.android.systemui.scene.domain.interactor.SceneBackInteractor import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade @@ -41,21 +43,23 @@ class ShadeUserActionsViewModel constructor( private val qsSceneAdapter: QSSceneAdapter, private val shadeInteractor: ShadeInteractor, + private val sceneBackInteractor: SceneBackInteractor, ) : UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { combine( shadeInteractor.shadeMode, qsSceneAdapter.isCustomizerShowing, - ) { shadeMode, isCustomizerShowing -> + sceneBackInteractor.backScene.map { it ?: SceneFamilies.Home }, + ) { shadeMode, isCustomizerShowing, backScene -> buildMap<UserAction, UserActionResult> { if (!isCustomizerShowing) { set( Swipe(SwipeDirection.Up), UserActionResult( - SceneFamilies.Home, - ToSplitShade.takeIf { shadeMode is ShadeMode.Split } - ) + backScene, + ToSplitShade.takeIf { shadeMode is ShadeMode.Split }, + ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt new file mode 100644 index 000000000000..57c8bc6133e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.core + +import android.app.StatusBarManager +import android.content.Context +import android.os.Binder +import android.os.RemoteException +import android.view.WindowInsets +import com.android.internal.statusbar.IStatusBarService +import com.android.internal.statusbar.RegisterStatusBarResult +import com.android.systemui.CoreStartable +import com.android.systemui.InitController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.navigationbar.NavigationBarController +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore +import dagger.Lazy +import javax.inject.Inject + +@SysUISingleton +class CommandQueueInitializer +@Inject +constructor( + private val context: Context, + private val commandQueue: CommandQueue, + private val commandQueueCallbacksLazy: Lazy<CommandQueue.Callbacks>, + private val statusBarModeRepository: StatusBarModeRepositoryStore, + private val initController: InitController, + private val barService: IStatusBarService, + private val navigationBarController: NavigationBarController, +) : CoreStartable { + + override fun start() { + StatusBarSimpleFragment.assertInNewMode() + val result: RegisterStatusBarResult = + try { + barService.registerStatusBar(commandQueue) + } catch (ex: RemoteException) { + ex.rethrowFromSystemServer() + return + } + + createNavigationBar(result) + + if ((result.mTransientBarTypes and WindowInsets.Type.statusBars()) != 0) { + statusBarModeRepository.defaultDisplay.showTransient() + } + val displayId = context.display.displayId + val commandQueueCallbacks = commandQueueCallbacksLazy.get() + commandQueueCallbacks.onSystemBarAttributesChanged( + displayId, + result.mAppearance, + result.mAppearanceRegions, + result.mNavbarColorManagedByIme, + result.mBehavior, + result.mRequestedVisibleTypes, + result.mPackageName, + result.mLetterboxDetails, + ) + + // StatusBarManagerService has a back up of IME token and it's restored here. + commandQueueCallbacks.setImeWindowStatus( + displayId, + result.mImeWindowVis, + result.mImeBackDisposition, + result.mShowImeSwitcher, + ) + + // Set up the initial icon state + val numIcons: Int = result.mIcons.size + for (i in 0 until numIcons) { + commandQueue.setIcon(result.mIcons.keyAt(i), result.mIcons.valueAt(i)) + } + + // set the initial view visibility + val disabledFlags1 = result.mDisabledFlags1 + val disabledFlags2 = result.mDisabledFlags2 + initController.addPostInitTask { + commandQueue.disable(displayId, disabledFlags1, disabledFlags2, /* animate= */ false) + try { + // NOTE(b/262059863): Force-update the disable flags after applying the flags + // returned from registerStatusBar(). The result's disabled flags may be stale + // if StatusBarManager's disabled flags are updated between registering the bar + // and this handling this post-init task. We force an update in this case, and use a + // new token to not conflict with any other disabled flags already requested by + // SysUI + val token = Binder() + barService.disable(StatusBarManager.DISABLE_HOME, token, context.packageName) + barService.disable(0, token, context.packageName) + } catch (ex: RemoteException) { + ex.rethrowFromSystemServer() + } + } + } + + private fun createNavigationBar(result: RegisterStatusBarResult) { + navigationBarController.createNavigationBars(/* includeDefaultDisplay= */ true, result) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt new file mode 100644 index 000000000000..8bd990b83a63 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.core + +import android.view.View +import com.android.systemui.CoreStartable +import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.plugins.DarkIconDispatcher +import com.android.systemui.plugins.PluginDependencyProvider +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.shade.NotificationShadeWindowViewController +import com.android.systemui.shade.ShadeSurface +import com.android.systemui.statusbar.AutoHideUiElement +import com.android.systemui.statusbar.NotificationRemoteInputManager +import com.android.systemui.statusbar.data.model.StatusBarMode +import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore +import com.android.systemui.statusbar.phone.AutoHideController +import com.android.systemui.statusbar.phone.CentralSurfaces +import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions +import com.android.systemui.statusbar.phone.PhoneStatusBarViewController +import com.android.systemui.statusbar.window.StatusBarWindowController +import com.android.systemui.statusbar.window.data.model.StatusBarWindowState +import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore +import com.android.wm.shell.bubbles.Bubbles +import dagger.Lazy +import java.io.PrintWriter +import java.util.Optional +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +/** + * Class responsible for managing the lifecycle and state of the status bar. + * + * It is a temporary class, created to pull status bar related logic out of CentralSurfacesImpl. The + * plan is break it out into individual classes. + */ +@SysUISingleton +class StatusBarOrchestrator +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val statusBarInitializer: StatusBarInitializer, + private val statusBarWindowController: StatusBarWindowController, + private val statusBarModeRepository: StatusBarModeRepositoryStore, + private val demoModeController: DemoModeController, + private val pluginDependencyProvider: PluginDependencyProvider, + private val autoHideController: AutoHideController, + private val remoteInputManager: NotificationRemoteInputManager, + private val notificationShadeWindowViewControllerLazy: + Lazy<NotificationShadeWindowViewController>, + private val shadeSurface: ShadeSurface, + private val bubblesOptional: Optional<Bubbles>, + private val statusBarWindowStateRepositoryStore: StatusBarWindowStateRepositoryStore, + powerInteractor: PowerInteractor, + primaryBouncerInteractor: PrimaryBouncerInteractor, +) : CoreStartable { + + private val phoneStatusBarViewController = + MutableStateFlow<PhoneStatusBarViewController?>(value = null) + + private val phoneStatusBarTransitions = + MutableStateFlow<PhoneStatusBarTransitions?>(value = null) + + private val shouldAnimateNextBarModeChange = + combine( + statusBarModeRepository.defaultDisplay.isTransientShown, + powerInteractor.isAwake, + statusBarWindowStateRepositoryStore.defaultDisplay.windowState, + ) { isTransientShown, isDeviceAwake, statusBarWindowState -> + !isTransientShown && + isDeviceAwake && + statusBarWindowState != StatusBarWindowState.Hidden + } + + private val controllerAndBouncerShowing = + combine( + phoneStatusBarViewController.filterNotNull(), + primaryBouncerInteractor.isShowing, + ::Pair, + ) + + private val barTransitionsAndDeviceAsleep = + combine(phoneStatusBarTransitions.filterNotNull(), powerInteractor.isAsleep, ::Pair) + + private val statusBarVisible = + combine( + statusBarModeRepository.defaultDisplay.statusBarMode, + statusBarWindowStateRepositoryStore.defaultDisplay.windowState, + ) { mode, statusBarWindowState -> + mode != StatusBarMode.LIGHTS_OUT && + mode != StatusBarMode.LIGHTS_OUT_TRANSPARENT && + statusBarWindowState != StatusBarWindowState.Hidden + } + + private val barModeUpdate = + combine( + shouldAnimateNextBarModeChange, + phoneStatusBarTransitions.filterNotNull(), + statusBarModeRepository.defaultDisplay.statusBarMode, + ::Triple, + ) + .distinctUntilChangedBy { (_, barTransitions, statusBarMode) -> + // We only want to collect when either bar transitions or status bar mode + // changed. + Pair(barTransitions, statusBarMode) + } + + override fun start() { + StatusBarSimpleFragment.assertInNewMode() + applicationScope.launch { + launch { + controllerAndBouncerShowing.collect { (controller, bouncerShowing) -> + setBouncerShowingForStatusBarComponents(controller, bouncerShowing) + } + } + launch { + barTransitionsAndDeviceAsleep.collect { (barTransitions, deviceAsleep) -> + if (deviceAsleep) { + barTransitions.finishAnimations() + } + } + } + launch { statusBarVisible.collect { updateBubblesVisibility(it) } } + launch { + barModeUpdate.collect { (animate, barTransitions, statusBarMode) -> + updateBarMode(animate, barTransitions, statusBarMode) + } + } + } + createAndAddWindow() + setupPluginDependencies() + setUpAutoHide() + } + + private fun createAndAddWindow() { + initializeStatusBarFragment() + statusBarWindowController.attach() + } + + private fun initializeStatusBarFragment() { + statusBarInitializer.statusBarViewUpdatedListener = + object : StatusBarInitializer.OnStatusBarViewUpdatedListener { + override fun onStatusBarViewUpdated( + statusBarViewController: PhoneStatusBarViewController, + statusBarTransitions: PhoneStatusBarTransitions, + ) { + phoneStatusBarViewController.value = statusBarViewController + phoneStatusBarTransitions.value = statusBarTransitions + + notificationShadeWindowViewControllerLazy + .get() + .setStatusBarViewController(statusBarViewController) + // Ensure we re-propagate panel expansion values to the panel controller and + // any listeners it may have, such as PanelBar. This will also ensure we + // re-display the notification panel if necessary (for example, if + // a heads-up notification was being displayed and should continue being + // displayed). + shadeSurface.updateExpansionAndVisibility() + } + } + } + + private fun setupPluginDependencies() { + pluginDependencyProvider.allowPluginDependency(DarkIconDispatcher::class.java) + pluginDependencyProvider.allowPluginDependency(StatusBarStateController::class.java) + } + + private fun setUpAutoHide() { + autoHideController.setStatusBar( + object : AutoHideUiElement { + override fun synchronizeState() {} + + override fun shouldHideOnTouch(): Boolean { + return !remoteInputManager.isRemoteInputActive + } + + override fun isVisible(): Boolean { + return statusBarModeRepository.defaultDisplay.isTransientShown.value + } + + override fun hide() { + statusBarModeRepository.defaultDisplay.clearTransient() + } + }) + } + + private fun updateBarMode( + animate: Boolean, + barTransitions: PhoneStatusBarTransitions, + barMode: StatusBarMode, + ) { + if (!demoModeController.isInDemoMode) { + barTransitions.transitionTo(barMode.toTransitionModeInt(), animate) + } + autoHideController.touchAutoHide() + } + + private fun updateBubblesVisibility(statusBarVisible: Boolean) { + bubblesOptional.ifPresent { bubbles: Bubbles -> + bubbles.onStatusBarVisibilityChanged(statusBarVisible) + } + } + + private fun setBouncerShowingForStatusBarComponents( + controller: PhoneStatusBarViewController, + bouncerShowing: Boolean, + ) { + val importance = + if (bouncerShowing) { + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } else { + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + controller.setImportantForAccessibility(importance) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println(statusBarWindowStateRepositoryStore.defaultDisplay.windowState.value) + CentralSurfaces.dumpBarTransitions( + pw, + "PhoneStatusBarTransitions", + phoneStatusBarTransitions.value, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt index 3903ff3c2b9b..cf238d553225 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -46,6 +46,7 @@ import dagger.multibindings.IntoMap */ @Module(includes = [StatusBarDataLayerModule::class, SystemBarUtilsProxyImpl.Module::class]) abstract class StatusBarModule { + @Binds @IntoMap @ClassKey(OngoingCallController::class) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt index da29b0fd0dc7..ec5ebc3651ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt @@ -43,7 +43,7 @@ interface RichOngoingNotificationContentExtractor { entry: NotificationEntry, builder: Notification.Builder, systemUIContext: Context, - packageContext: Context + packageContext: Context, ): RichOngoingContentModel? } @@ -52,7 +52,7 @@ class NoOpRichOngoingNotificationContentExtractor : RichOngoingNotificationConte entry: NotificationEntry, builder: Notification.Builder, systemUIContext: Context, - packageContext: Context + packageContext: Context, ): RichOngoingContentModel? = null } @@ -68,7 +68,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : entry: NotificationEntry, builder: Notification.Builder, systemUIContext: Context, - packageContext: Context + packageContext: Context, ): RichOngoingContentModel? { val sbn = entry.sbn val notification = sbn.notification @@ -89,7 +89,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : null } } - } else if (builder.style is Notification.EnRouteStyle) { + } else if (builder.style is Notification.ProgressStyle) { parseEnRouteNotification(notification, icon) } else null } catch (e: Exception) { @@ -104,7 +104,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : */ private fun parseTimerNotification( notification: Notification, - icon: IconModel + icon: IconModel, ): TimerContentModel { // sortKey=1 0|↺7|RUNNING|▶16:21:58.523|Σ0:05:00|Δ0:00:03|⏳0:04:57 // sortKey=1 0|↺7|PAUSED|Σ0:05:00|Δ0:04:54|⏳0:00:06 @@ -132,7 +132,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : resumeIntent = notification.findStartIntent(), addMinuteAction = notification.findAddMinuteAction(), resetAction = notification.findResetAction(), - ) + ), ) } "RUNNING" -> { @@ -149,7 +149,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : pauseIntent = notification.findPauseIntent(), addMinuteAction = notification.findAddMinuteAction(), resetAction = notification.findResetAction(), - ) + ), ) } else -> error("unknown state ($state) in sortKey=$sortKey") @@ -192,7 +192,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : val localDateTime = LocalDateTime.of( LocalDate.now(), - LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000) + LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000), ) val offset = ZoneId.systemDefault().rules.getOffset(localDateTime) return localDateTime.toInstant(offset).toEpochMilli() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 1d3f0e1f6dc3..5f4f72f293a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -241,10 +241,10 @@ import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.startingsurface.SplashscreenContentDrawer; import com.android.wm.shell.startingsurface.StartingSurface; -import dagger.Lazy; - import dalvik.annotation.optimization.NeverCompile; +import dagger.Lazy; + import java.io.PrintWriter; import java.io.StringWriter; import java.util.Map; @@ -304,6 +304,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }; void onStatusBarWindowStateChanged(@WindowVisibleState int state) { + StatusBarSimpleFragment.assertInLegacyMode(); mStatusBarWindowState = state; updateBubblesVisibility(); } @@ -813,8 +814,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mStartingSurfaceOptional = startingSurfaceOptional; mDreamManager = dreamManager; lockscreenShadeTransitionController.setCentralSurfaces(this); - statusBarWindowStateController.addListener(this::onStatusBarWindowStateChanged); - + if (!StatusBarSimpleFragment.isEnabled()) { + statusBarWindowStateController.addListener(this::onStatusBarWindowStateChanged); + } mScreenOffAnimationController = screenOffAnimationController; ShadeExpansionListener shadeExpansionListener = this::onPanelExpansionChanged; @@ -901,10 +903,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mWallpaperSupported = mWallpaperManager.isWallpaperSupported(); RegisterStatusBarResult result = null; - try { - result = mBarService.registerStatusBar(mCommandQueue); - } catch (RemoteException ex) { - ex.rethrowFromSystemServer(); + if (!StatusBarSimpleFragment.isEnabled()) { + try { + result = mBarService.registerStatusBar(mCommandQueue); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } } createAndAddWindows(result); @@ -912,30 +916,45 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // Set up the initial notification state. This needs to happen before CommandQueue.disable() setUpPresenter(); - if ((result.mTransientBarTypes & WindowInsets.Type.statusBars()) != 0) { - mStatusBarModeRepository.getDefaultDisplay().showTransient(); - } - mCommandQueueCallbacks.onSystemBarAttributesChanged(mDisplayId, result.mAppearance, - result.mAppearanceRegions, result.mNavbarColorManagedByIme, result.mBehavior, - result.mRequestedVisibleTypes, result.mPackageName, result.mLetterboxDetails); - - // StatusBarManagerService has a back up of IME token and it's restored here. - mCommandQueueCallbacks.setImeWindowStatus(mDisplayId, result.mImeWindowVis, - result.mImeBackDisposition, result.mShowImeSwitcher); - - // Set up the initial icon state - int numIcons = result.mIcons.size(); - for (int i = 0; i < numIcons; i++) { - mCommandQueue.setIcon(result.mIcons.keyAt(i), result.mIcons.valueAt(i)); - } - - if (DEBUG) { - Log.d(TAG, String.format( - "init: icons=%d disabled=0x%08x lights=0x%08x imeButton=0x%08x", - numIcons, - result.mDisabledFlags1, + // When the StatusBarSimpleFragment flag is enabled, this logic will be done in + // StatusBarOrchestrator + if (!StatusBarSimpleFragment.isEnabled()) { + if ((result.mTransientBarTypes & WindowInsets.Type.statusBars()) != 0) { + mStatusBarModeRepository.getDefaultDisplay().showTransient(); + } + mCommandQueueCallbacks.onSystemBarAttributesChanged( + mDisplayId, result.mAppearance, - result.mImeWindowVis)); + result.mAppearanceRegions, + result.mNavbarColorManagedByIme, + result.mBehavior, + result.mRequestedVisibleTypes, + result.mPackageName, + result.mLetterboxDetails); + + // StatusBarManagerService has a back up of IME token and it's restored here. + mCommandQueueCallbacks.setImeWindowStatus( + mDisplayId, + result.mImeWindowVis, + result.mImeBackDisposition, + result.mShowImeSwitcher); + + // Set up the initial icon state + int numIcons = result.mIcons.size(); + for (int i = 0; i < numIcons; i++) { + mCommandQueue.setIcon(result.mIcons.keyAt(i), result.mIcons.valueAt(i)); + } + + if (DEBUG) { + Log.d( + TAG, + String.format( + "init: icons=%d disabled=0x%08x lights=0x%08x imeButton=0x%08x", + numIcons, + result.mDisabledFlags1, + result.mAppearance, + result.mImeWindowVis)); + } } IntentFilter internalFilter = new IntentFilter(); @@ -1005,24 +1024,30 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mAccessibilityFloatingMenuController.init(); - // set the initial view visibility - int disabledFlags1 = result.mDisabledFlags1; - int disabledFlags2 = result.mDisabledFlags2; - mInitController.addPostInitTask(() -> { - setUpDisableFlags(disabledFlags1, disabledFlags2); - try { - // NOTE(b/262059863): Force-update the disable flags after applying the flags - // returned from registerStatusBar(). The result's disabled flags may be stale - // if StatusBarManager's disabled flags are updated between registering the bar and - // this handling this post-init task. We force an update in this case, and use a new - // token to not conflict with any other disabled flags already requested by SysUI - Binder token = new Binder(); - mBarService.disable(DISABLE_HOME, token, mContext.getPackageName()); - mBarService.disable(0, token, mContext.getPackageName()); - } catch (RemoteException ex) { - ex.rethrowFromSystemServer(); - } - }); + // When the StatusBarSimpleFragment flag is enabled, this logic will be done in + // StatusBarOrchestrator + if (!StatusBarSimpleFragment.isEnabled()) { + // set the initial view visibility + int disabledFlags1 = result.mDisabledFlags1; + int disabledFlags2 = result.mDisabledFlags2; + mInitController.addPostInitTask( + () -> { + setUpDisableFlags(disabledFlags1, disabledFlags2); + try { + // NOTE(b/262059863): Force-update the disable flags after applying the + // flags returned from registerStatusBar(). The result's disabled flags + // may be stale if StatusBarManager's disabled flags are updated between + // registering the bar and this handling this post-init task. We force + // an update in this case, and use a new token to not conflict with any + // other disabled flags already requested by SysUI + Binder token = new Binder(); + mBarService.disable(DISABLE_HOME, token, mContext.getPackageName()); + mBarService.disable(0, token, mContext.getPackageName()); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + }); + } registerCallbacks(); @@ -1101,7 +1126,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { /** * @deprecated use {@link - * WindowRootViewVisibilityInteractor.isLockscreenOrShadeVisible} instead. + * WindowRootViewVisibilityInteractor#isLockscreenOrShadeVisible()} instead. */ @VisibleForTesting @Deprecated void initShadeVisibilityListener() { @@ -1168,13 +1193,16 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mWallpaperController.setRootView(getNotificationShadeWindowView()); mDemoModeController.addCallback(mDemoModeCallback); - mJavaAdapter.alwaysCollectFlow( - mStatusBarModeRepository.getDefaultDisplay().isTransientShown(), - this::onTransientShownChanged); - mJavaAdapter.alwaysCollectFlow( - mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode(), - this::updateBarMode); - + // When the StatusBarSimpleFragment flag is enabled, this logic will be done in + // StatusBarOrchestrator. + if (!StatusBarSimpleFragment.isEnabled()) { + mJavaAdapter.alwaysCollectFlow( + mStatusBarModeRepository.getDefaultDisplay().isTransientShown(), + this::onTransientShownChanged); + mJavaAdapter.alwaysCollectFlow( + mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode(), + this::updateBarMode); + } mCommandQueueCallbacks = mCommandQueueCallbacksLazy.get(); mCommandQueue.addCallback(mCommandQueueCallbacks); @@ -1184,59 +1212,70 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator); mWakeUpCoordinator.onPanelExpansionChanged(currentState); - // Allow plugins to reference DarkIconDispatcher and StatusBarStateController - mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class); - mPluginDependencyProvider.allowPluginDependency(StatusBarStateController.class); - - // Set up CollapsedStatusBarFragment and PhoneStatusBarView - mStatusBarInitializer.setStatusBarViewUpdatedListener( - (statusBarViewController, statusBarTransitions) -> { - mPhoneStatusBarViewController = statusBarViewController; - mStatusBarTransitions = statusBarTransitions; - getNotificationShadeWindowViewController() - .setStatusBarViewController(mPhoneStatusBarViewController); - // Ensure we re-propagate panel expansion values to the panel controller and - // any listeners it may have, such as PanelBar. This will also ensure we - // re-display the notification panel if necessary (for example, if - // a heads-up notification was being displayed and should continue being - // displayed). - mShadeSurface.updateExpansionAndVisibility(); - setBouncerShowingForStatusBarComponents(mBouncerShowing); - checkBarModes(); - }); - // When the flag is on, we register the fragment as a core startable and this is not needed + // When the StatusBarSimpleFragment flag is enabled, all this logic will be done in + // StatusBarOrchestrator. if (!StatusBarSimpleFragment.isEnabled()) { + // Allow plugins to reference DarkIconDispatcher and StatusBarStateController + mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class); + mPluginDependencyProvider.allowPluginDependency(StatusBarStateController.class); + + // Set up CollapsedStatusBarFragment and PhoneStatusBarView + mStatusBarInitializer.setStatusBarViewUpdatedListener( + (statusBarViewController, statusBarTransitions) -> { + + mPhoneStatusBarViewController = statusBarViewController; + mStatusBarTransitions = statusBarTransitions; + getNotificationShadeWindowViewController() + .setStatusBarViewController(mPhoneStatusBarViewController); + // Ensure we re-propagate panel expansion values to the panel controller and + // any listeners it may have, such as PanelBar. This will also ensure we + // re-display the notification panel if necessary (for example, if + // a heads-up notification was being displayed and should continue being + // displayed). + mShadeSurface.updateExpansionAndVisibility(); + setBouncerShowingForStatusBarComponents(mBouncerShowing); + checkBarModes(); + }); + // When the flag is on, we register the fragment as a core startable and this is not + // needed mStatusBarInitializer.initializeStatusBar(); } mStatusBarTouchableRegionManager.setup(getNotificationShadeWindowView()); - createNavigationBar(result); + if (!StatusBarSimpleFragment.isEnabled()) { + createNavigationBar(result); + } mAmbientIndicationContainer = getNotificationShadeWindowView().findViewById( R.id.ambient_indication_container); - mAutoHideController.setStatusBar(new AutoHideUiElement() { - @Override - public void synchronizeState() { - checkBarModes(); - } + // When the StatusBarSimpleFragment flag is enabled, all this logic will be done in + // StatusBarOrchestrator. + if (!StatusBarSimpleFragment.isEnabled()) { + mAutoHideController.setStatusBar( + new AutoHideUiElement() { + @Override + public void synchronizeState() { + checkBarModes(); + } - @Override - public boolean shouldHideOnTouch() { - return !mRemoteInputManager.isRemoteInputActive(); - } + @Override + public boolean shouldHideOnTouch() { + return !mRemoteInputManager.isRemoteInputActive(); + } - @Override - public boolean isVisible() { - return isTransientShown(); - } + @Override + public boolean isVisible() { + return isTransientShown(); + } - @Override - public void hide() { - mStatusBarModeRepository.getDefaultDisplay().clearTransient(); - } - }); + @Override + public void hide() { + mStatusBarModeRepository.getDefaultDisplay().clearTransient(); + } + }); + } ScrimView scrimBehind = getNotificationShadeWindowView().findViewById(R.id.scrim_behind); ScrimView notificationsScrim = getNotificationShadeWindowView() @@ -1479,12 +1518,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { * @param state2 disable2 flags */ protected void setUpDisableFlags(int state1, int state2) { + StatusBarSimpleFragment.assertInLegacyMode(); mCommandQueue.disable(mDisplayId, state1, state2, false /* animate */); } // TODO(b/117478341): This was left such that CarStatusBar can override this method. // Try to remove this. protected void createNavigationBar(@Nullable RegisterStatusBarResult result) { + StatusBarSimpleFragment.assertInLegacyMode(); mNavigationBarController.createNavigationBars(true /* includeDefaultDisplay */, result); } @@ -1697,14 +1738,16 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void checkBarModes() { if (mDemoModeController.isInDemoMode()) return; - if (mStatusBarTransitions != null) { + // When the StatusBarSimpleFragment flag is enabled, this logic will be done in + // StatusBarOrchestrator. + if (!StatusBarSimpleFragment.isEnabled() && mStatusBarTransitions != null) { checkBarMode( mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode().getValue(), mStatusBarWindowState, mStatusBarTransitions); + mNoAnimationOnNextBarModeChange = false; } mNavigationBarController.checkNavBarModes(mDisplayId); - mNoAnimationOnNextBarModeChange = false; } /** Temporarily hides Bubbles if the status bar is hidden. */ @@ -1728,7 +1771,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } private void finishBarAnimations() { - if (mStatusBarTransitions != null) { + // When the StatusBarSimpleFragment flag is enabled, this logic will be done in + // StatusBarOrchestrator. + if (!StatusBarSimpleFragment.isEnabled() && mStatusBarTransitions != null) { mStatusBarTransitions.finishAnimations(); } mNavigationBarController.finishBarAnimations(mDisplayId); @@ -1770,14 +1815,17 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } pw.print(" mInteractingWindows="); pw.println(mInteractingWindows); - pw.print(" mStatusBarWindowState="); - pw.println(windowStateToString(mStatusBarWindowState)); + if (!StatusBarSimpleFragment.isEnabled()) { + pw.print(" mStatusBarWindowState="); + pw.println(windowStateToString(mStatusBarWindowState)); + } pw.print(" mDozing="); pw.println(mDozing); pw.print(" mWallpaperSupported= "); pw.println(mWallpaperSupported); - CentralSurfaces.dumpBarTransitions( - pw, "PhoneStatusBarTransitions", mStatusBarTransitions); - + if (!StatusBarSimpleFragment.isEnabled()) { + CentralSurfaces.dumpBarTransitions( + pw, "PhoneStatusBarTransitions", mStatusBarTransitions); + } pw.println(" mMediaManager: "); if (mMediaManager != null) { mMediaManager.dump(pw, args); @@ -1850,7 +1898,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private void createAndAddWindows(@Nullable RegisterStatusBarResult result) { makeStatusBarView(result); mNotificationShadeWindowController.attach(); - mStatusBarWindowController.attach(); + // When the StatusBarSimpleFragment flag is enabled, this logic will be done in + // StatusBarOrchestrator + if (!StatusBarSimpleFragment.isEnabled()) { + mStatusBarWindowController.attach(); + } } // called by makeStatusbar and also by PhoneStatusBarView @@ -2475,7 +2527,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { int importance = bouncerShowing ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS : IMPORTANT_FOR_ACCESSIBILITY_AUTO; - if (mPhoneStatusBarViewController != null) { + if (!StatusBarSimpleFragment.isEnabled() && mPhoneStatusBarViewController != null) { mPhoneStatusBarViewController.setImportantForAccessibility(importance); } mShadeSurface.setImportantForAccessibility(importance); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index ee961955df39..93db2db918b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -20,6 +20,7 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED import static android.service.notification.NotificationListenerService.REASON_CLICK; import static com.android.systemui.statusbar.phone.CentralSurfaces.getActivityOptions; +import static com.android.systemui.util.kotlin.NullabilityKt.expectNotNull; import android.app.ActivityManager; import android.app.ActivityOptions; @@ -112,6 +113,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean showOverTheLockScreen); } + private final static String TAG = "StatusBarNotificationActivityStarter"; + private final Context mContext; private final int mDisplayId; @@ -229,6 +232,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit */ @Override public void onNotificationBubbleIconClicked(NotificationEntry entry) { + expectNotNull(TAG, "entry", entry); Runnable action = () -> { mBubblesManagerOptional.ifPresent(bubblesManager -> bubblesManager.onUserChangedBubble(entry, !entry.isBubble())); @@ -255,6 +259,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit */ @Override public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) { + expectNotNull(TAG, "entry", entry); + expectNotNull(TAG, "row", row); mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(), mKeyguardStateController.isVisible(), mNotificationShadeWindowController.getPanelExpanded()); @@ -437,6 +443,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit */ @Override public void onDragSuccess(NotificationEntry entry) { + expectNotNull(TAG, "entry", entry); // this method is not responsible for intent sending. // will focus follow operation only after drag-and-drop that notification. final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true); @@ -529,6 +536,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startNotificationGutsIntent(final Intent intent, final int appUid, ExpandableNotificationRow row) { + expectNotNull(TAG, "intent", intent); + expectNotNull(TAG, "row", row); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt index 13b651e8c0be..5b0319883b5f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt @@ -16,10 +16,20 @@ package com.android.systemui.statusbar.phone.dagger import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.core.CommandQueueInitializer import com.android.systemui.statusbar.core.StatusBarInitializer import com.android.systemui.statusbar.core.StatusBarInitializerImpl +import com.android.systemui.statusbar.core.StatusBarOrchestrator +import com.android.systemui.statusbar.core.StatusBarSimpleFragment +import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks +import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore +import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStoreImpl import dagger.Binds +import dagger.Lazy import dagger.Module +import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap @@ -27,6 +37,16 @@ import dagger.multibindings.IntoMap @Module interface StatusBarPhoneModule { + @Binds + abstract fun windowStateRepoStore( + impl: StatusBarWindowStateRepositoryStoreImpl + ): StatusBarWindowStateRepositoryStore + + @Binds + abstract fun commandQCallbacks( + impl: CentralSurfacesCommandQueueCallbacks + ): CommandQueue.Callbacks + /** Binds {@link StatusBarInitializer} as a {@link CoreStartable}. */ @Binds @IntoMap @@ -34,4 +54,34 @@ interface StatusBarPhoneModule { fun bindStatusBarInitializer(impl: StatusBarInitializerImpl): CoreStartable @Binds fun statusBarInitializer(impl: StatusBarInitializerImpl): StatusBarInitializer + + companion object { + @Provides + @SysUISingleton + @IntoMap + @ClassKey(StatusBarOrchestrator::class) + fun orchestratorCoreStartable( + orchestratorLazy: Lazy<StatusBarOrchestrator> + ): CoreStartable { + return if (StatusBarSimpleFragment.isEnabled) { + orchestratorLazy.get() + } else { + CoreStartable.NOP + } + } + + @Provides + @SysUISingleton + @IntoMap + @ClassKey(CommandQueueInitializer::class) + fun commandQueueInitializerCoreStartable( + initializerLazy: Lazy<CommandQueueInitializer> + ): CoreStartable { + return if (StatusBarSimpleFragment.isEnabled) { + initializerLazy.get() + } else { + CoreStartable.NOP + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/nullability.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Nullability.kt index 298dacde8128..1c760bedff58 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/nullability.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Nullability.kt @@ -16,6 +16,7 @@ package com.android.systemui.util.kotlin +import android.util.Log import java.util.Optional /** @@ -28,3 +29,14 @@ inline fun <T : Any, R> transform(value: T?, block: (T) -> R): R? = value?.let(b */ @Suppress("NOTHING_TO_INLINE") inline fun <T> Optional<T>.getOrNull(): T? = orElse(null) + +/** + * Utility method to check if a value that is technically nullable is actually null. If it is null, + * this will crash development builds (but just log on production/droidfood builds). It can be used + * as a first step to verify if a nullable value can be made non-nullable instead. + */ +fun <T> expectNotNull(logTag: String, name: String, nullable: T?) { + if (nullable == null) { + Log.wtf(logTag, "Expected value of $name to not be null.") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt index 3c50c7b4a212..09b1f45f179b 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt +++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt @@ -1,6 +1,7 @@ package com.android.systemui.wallet.controller import android.content.Intent +import android.os.DeadObjectException import android.os.IBinder import android.util.Log import androidx.annotation.VisibleForTesting @@ -47,7 +48,11 @@ constructor( controller.allWalletCards.collect { cards -> val cardsSize = cards.size Log.i(TAG, "Number of cards registered $cardsSize") - listener?.registerNewWalletCards(cards) + try { + listener?.registerNewWalletCards(cards) + } catch (e: DeadObjectException) { + Log.e(TAG, "Failed to register wallet cards because IWalletCardsUpdatedListener is dead") + } } } } else { @@ -55,7 +60,11 @@ constructor( controller.allWalletCards.collect { cards -> val cardsSize = cards.size Log.i(TAG, "Number of cards registered $cardsSize") - listener?.registerNewWalletCards(cards) + try { + listener?.registerNewWalletCards(cards) + } catch (e: DeadObjectException) { + Log.e(TAG, "Failed to register wallet cards because IWalletCardsUpdatedListener is dead") + } } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt index 5cc64547aa6b..0d369a3ea80c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.bouncer.ui.composable import android.app.AlertDialog import android.platform.test.annotations.MotionTest import android.testing.TestableLooper.RunWithLooper +import android.view.View import androidx.activity.BackEventCompat import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween @@ -52,27 +53,21 @@ import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel -import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.motion.createSysUiComposeMotionTestRule -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.sceneContainerStartable -import com.android.systemui.scene.sceneContainerGestureFilterFactory -import com.android.systemui.scene.shared.logger.sceneLogger +import com.android.systemui.scene.sceneContainerViewModelFactory import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.scene.ui.composable.SceneContainer -import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel -import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector import com.android.systemui.settings.displayTracker -import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.testKosmos import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.awaitCancellation @@ -85,6 +80,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot import platform.test.motion.compose.ComposeRecordingSpec import platform.test.motion.compose.MotionControl @@ -121,24 +117,17 @@ class BouncerPredictiveBackTest : SysuiTestCase() { val navigationDistances = mapOf(Scenes.Lockscreen to 1, Scenes.Bouncer to 0) SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances) } + private val view = mock<View>() private val transitionState by lazy { MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) ) } + private val sceneContainerViewModel by lazy { - SceneContainerViewModel( - sceneInteractor = kosmos.sceneInteractor, - falsingInteractor = kosmos.falsingInteractor, - powerInteractor = kosmos.powerInteractor, - shadeInteractor = kosmos.shadeInteractor, - splitEdgeDetector = kosmos.splitEdgeDetector, - logger = kosmos.sceneLogger, - gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory, - displayId = kosmos.displayTracker.defaultDisplayId, - motionEventHandlerReceiver = {}, - ) + kosmos.sceneContainerViewModelFactory + .create(view, kosmos.displayTracker.defaultDisplayId, {}) .apply { setTransitionState(transitionState) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index a6afd0e499f4..f5a901963f05 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -22,10 +22,6 @@ import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.google.common.truth.Truth.assertThat; -import static kotlinx.coroutines.flow.FlowKt.emptyFlow; -import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow; -import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; @@ -40,6 +36,10 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.flow.FlowKt.emptyFlow; +import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow; +import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; + import android.animation.Animator; import android.annotation.IdRes; import android.content.ContentResolver; @@ -95,6 +95,7 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.haptics.msdl.FakeMSDLPlayer; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewConfigurator; import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository; @@ -151,6 +152,7 @@ import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository; import com.android.systemui.statusbar.notification.ConversationNotificationManager; import com.android.systemui.statusbar.notification.DynamicPrivacyController; +import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger; import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository; @@ -167,7 +169,6 @@ import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.ConfigurationControllerImpl; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; -import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.KeyguardBottomAreaView; import com.android.systemui.statusbar.phone.KeyguardBottomAreaViewController; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -200,12 +201,6 @@ import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.animation.FlingAnimationUtils; -import dagger.Lazy; - -import kotlinx.coroutines.CoroutineDispatcher; -import kotlinx.coroutines.channels.BufferOverflow; -import kotlinx.coroutines.test.TestScope; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -220,6 +215,11 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; +import dagger.Lazy; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.channels.BufferOverflow; +import kotlinx.coroutines.test.TestScope; + public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400; @@ -374,6 +374,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected View.OnLayoutChangeListener mLayoutChangeListener; protected KeyguardStatusViewController mKeyguardStatusViewController; protected ShadeRepository mShadeRepository; + protected FakeMSDLPlayer mMSDLPlayer = mKosmos.getMsdlPlayer(); protected final FalsingManagerFake mFalsingManager = new FalsingManagerFake(); protected final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty(); @@ -761,7 +762,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { new ResourcesSplitShadeStateController(), mPowerInteractor, mKeyguardClockPositionAlgorithm, - mNaturalScrollingSettingObserver); + mNaturalScrollingSettingObserver, + mMSDLPlayer); mNotificationPanelViewController.initDependencies( mCentralSurfaces, null, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index a7fd1609d1ca..43dbb40d7721 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -49,6 +49,7 @@ import android.os.PowerManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; +import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; @@ -69,6 +70,8 @@ import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.phone.KeyguardClockPositionAlgorithm; +import com.google.android.msdl.data.model.MSDLToken; + import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -1458,4 +1461,23 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo assertThat(mNotificationPanelViewController.getFalsingThreshold()).isGreaterThan(14); } + + @Test + @EnableFlags(com.android.systemui.Flags.FLAG_MSDL_FEEDBACK) + public void performHapticFeedback_withMSDL_forGestureStart_deliversDragThresholdToken() { + mNotificationPanelViewController + .performHapticFeedback(HapticFeedbackConstants.GESTURE_START); + + assertThat(mMSDLPlayer.getLatestTokenPlayed()) + .isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR); + } + + @Test + @EnableFlags(com.android.systemui.Flags.FLAG_MSDL_FEEDBACK) + public void performHapticFeedback_withMSDL_forReject_deliversFailureToken() { + mNotificationPanelViewController + .performHapticFeedback(HapticFeedbackConstants.REJECT); + + assertThat(mMSDLPlayer.getLatestTokenPlayed()).isEqualTo(MSDLToken.FAILURE); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt index 90655c3cf4b3..97441f01bcf5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt @@ -33,7 +33,6 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED -import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -157,6 +156,7 @@ class NotificationPanelViewControllerWithCoroutinesTest : } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun doubleTapRequired_onKeyguard_usesPerformHapticFeedback() = runTest { launch(Dispatchers.Main.immediate) { val listener = getFalsingTapListener() @@ -184,6 +184,7 @@ class NotificationPanelViewControllerWithCoroutinesTest : } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun doubleTapRequired_shadeLocked_usesPerformHapticFeedback() = runTest { launch(Dispatchers.Main.immediate) { val listener = getFalsingTapListener() @@ -209,7 +210,7 @@ class NotificationPanelViewControllerWithCoroutinesTest : KEYGUARD /*statusBarState*/, false /*keyguardFadingAway*/, false /*goingToFullShade*/, - SHADE /*oldStatusBarState*/ + SHADE, /*oldStatusBarState*/ ) } advanceUntilIdle() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt new file mode 100644 index 000000000000..2a196c6b979f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.core + +import android.internal.statusbar.fakeStatusBarService +import android.platform.test.annotations.EnableFlags +import android.view.WindowInsets +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.initController +import com.android.systemui.keyguard.data.repository.fakeCommandQueue +import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository +import com.android.systemui.statusbar.mockCommandQueueCallbacks +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.verify + +@EnableFlags(StatusBarSimpleFragment.FLAG_NAME) +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommandQueueInitializerTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val initController = kosmos.initController + private val commandQueue = kosmos.fakeCommandQueue + private val commandQueueCallbacks = kosmos.mockCommandQueueCallbacks + private val statusBarModeRepository = kosmos.fakeStatusBarModeRepository + private val fakeStatusBarService = kosmos.fakeStatusBarService + private val initializer = kosmos.commandQueueInitializer + + @Test + fun start_registersStatusBar() { + initializer.start() + + assertThat(fakeStatusBarService.registeredStatusBar).isNotNull() + } + + @Test + fun start_barResultHasTransientStatusBar_transientStateIsTrue() { + fakeStatusBarService.transientBarTypes = WindowInsets.Type.statusBars() + + initializer.start() + + assertThat(statusBarModeRepository.defaultDisplay.isTransientShown.value).isTrue() + } + + @Test + fun start_barResultDoesNotHaveTransientStatusBar_transientStateIsFalse() { + fakeStatusBarService.transientBarTypes = WindowInsets.Type.navigationBars() + + initializer.start() + + assertThat(statusBarModeRepository.defaultDisplay.isTransientShown.value).isFalse() + } + + @Test + fun start_callsOnSystemBarAttributesChanged_basedOnRegisterBarResult() { + initializer.start() + + verify(commandQueueCallbacks) + .onSystemBarAttributesChanged( + context.displayId, + fakeStatusBarService.appearance, + fakeStatusBarService.appearanceRegions, + fakeStatusBarService.navbarColorManagedByIme, + fakeStatusBarService.behavior, + fakeStatusBarService.requestedVisibleTypes, + fakeStatusBarService.packageName, + fakeStatusBarService.letterboxDetails, + ) + } + + @Test + fun start_callsSetIcon_basedOnRegisterBarResult() { + initializer.start() + + assertThat(commandQueue.icons).isEqualTo(fakeStatusBarService.statusBarIcons) + } + + @Test + fun start_callsSetImeWindowStatus_basedOnRegisterBarResult() { + initializer.start() + + verify(commandQueueCallbacks) + .setImeWindowStatus( + context.displayId, + fakeStatusBarService.imeWindowVis, + fakeStatusBarService.imeBackDisposition, + fakeStatusBarService.showImeSwitcher, + ) + } + + @Test + fun start_afterPostInitTaskExecuted_callsDisableFlags_basedOnRegisterBarResult() { + initializer.start() + + initController.executePostInitTasks() + + assertThat(commandQueue.disableFlags1ForDisplay(context.displayId)) + .isEqualTo(fakeStatusBarService.disabledFlags1) + assertThat(commandQueue.disableFlags2ForDisplay(context.displayId)) + .isEqualTo(fakeStatusBarService.disabledFlags2) + } + + @Test + fun start_beforePostInitTaskExecuted_doesNotCallsDisableFlags() { + initializer.start() + + assertThat(commandQueue.disableFlags1ForDisplay(context.displayId)).isNull() + assertThat(commandQueue.disableFlags2ForDisplay(context.displayId)).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt new file mode 100644 index 000000000000..580336539c37 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.core + +import android.platform.test.annotations.EnableFlags +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.unconfinedTestDispatcher +import com.android.systemui.plugins.DarkIconDispatcher +import com.android.systemui.plugins.mockPluginDependencyProvider +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.shade.mockNotificationShadeWindowViewController +import com.android.systemui.shade.mockShadeSurface +import com.android.systemui.statusbar.data.model.StatusBarMode +import com.android.systemui.statusbar.data.model.StatusBarMode.LIGHTS_OUT +import com.android.systemui.statusbar.data.model.StatusBarMode.LIGHTS_OUT_TRANSPARENT +import com.android.systemui.statusbar.data.model.StatusBarMode.OPAQUE +import com.android.systemui.statusbar.data.model.StatusBarMode.TRANSPARENT +import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository +import com.android.systemui.statusbar.phone.mockPhoneStatusBarTransitions +import com.android.systemui.statusbar.phone.mockPhoneStatusBarViewController +import com.android.systemui.statusbar.window.data.model.StatusBarWindowState +import com.android.systemui.statusbar.window.data.repository.fakeStatusBarWindowStateRepositoryStore +import com.android.systemui.statusbar.window.data.repository.statusBarWindowStateRepositoryStore +import com.android.systemui.statusbar.window.fakeStatusBarWindowController +import com.android.systemui.testKosmos +import com.android.wm.shell.bubbles.bubbles +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +@EnableFlags(StatusBarSimpleFragment.FLAG_NAME) +@SmallTest +@RunWith(AndroidJUnit4::class) +class StatusBarOrchestratorTest : SysuiTestCase() { + + private val kosmos = + testKosmos().also { + it.testDispatcher = it.unconfinedTestDispatcher + it.statusBarWindowStateRepositoryStore = it.fakeStatusBarWindowStateRepositoryStore + } + private val testScope = kosmos.testScope + private val statusBarViewController = kosmos.mockPhoneStatusBarViewController + private val statusBarWindowController = kosmos.fakeStatusBarWindowController + private val statusBarModeRepository = kosmos.fakeStatusBarModeRepository + private val pluginDependencyProvider = kosmos.mockPluginDependencyProvider + private val notificationShadeWindowViewController = + kosmos.mockNotificationShadeWindowViewController + private val shadeSurface = kosmos.mockShadeSurface + private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository + private val fakeStatusBarWindowStateRepositoryStore = + kosmos.fakeStatusBarWindowStateRepositoryStore + private val fakePowerRepository = kosmos.fakePowerRepository + private val mockPhoneStatusBarTransitions = kosmos.mockPhoneStatusBarTransitions + private val mockBubbles = kosmos.bubbles + + private val orchestrator = kosmos.statusBarOrchestrator + + @Test + fun start_setsUpPluginDependencies() { + orchestrator.start() + + verify(pluginDependencyProvider).allowPluginDependency(DarkIconDispatcher::class.java) + verify(pluginDependencyProvider).allowPluginDependency(StatusBarStateController::class.java) + } + + @Test + fun start_attachesWindow() { + orchestrator.start() + + assertThat(statusBarWindowController.isAttached).isTrue() + } + + @Test + fun start_setsStatusBarControllerOnShade() { + orchestrator.start() + + verify(notificationShadeWindowViewController) + .setStatusBarViewController(statusBarViewController) + } + + @Test + fun start_updatesShadeExpansion() { + orchestrator.start() + + verify(shadeSurface).updateExpansionAndVisibility() + } + + @Test + fun bouncerShowing_setsImportanceForA11yToNoHideDescendants() = + testScope.runTest { + orchestrator.start() + + bouncerRepository.setPrimaryShow(isShowing = true) + + verify(statusBarViewController) + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) + } + + @Test + fun bouncerNotShowing_setsImportanceForA11yToNoHideDescendants() = + testScope.runTest { + orchestrator.start() + + bouncerRepository.setPrimaryShow(isShowing = false) + + verify(statusBarViewController) + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) + } + + @Test + fun deviceGoesToSleep_barTransitionsAnimationsAreFinished() = + testScope.runTest { + putDeviceToSleep() + + orchestrator.start() + + verify(mockPhoneStatusBarTransitions).finishAnimations() + } + + @Test + fun deviceIsAwake_barTransitionsAnimationsAreNotFinished() = + testScope.runTest { + awakeDevice() + + orchestrator.start() + + verify(mockPhoneStatusBarTransitions, never()).finishAnimations() + } + + @Test + fun statusBarVisible_notifiesBubbles() = + testScope.runTest { + setStatusBarMode(TRANSPARENT) + setStatusBarWindowState(StatusBarWindowState.Showing) + + orchestrator.start() + + verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ true) + } + + @Test + fun statusBarInLightsOutMode_notifiesBubblesWithStatusBarInvisible() = + testScope.runTest { + setStatusBarMode(LIGHTS_OUT) + setStatusBarWindowState(StatusBarWindowState.Showing) + + orchestrator.start() + + verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ false) + } + + @Test + fun statusBarInLightsOutTransparentMode_notifiesBubblesWithStatusBarInvisible() = + testScope.runTest { + setStatusBarMode(LIGHTS_OUT_TRANSPARENT) + setStatusBarWindowState(StatusBarWindowState.Showing) + + orchestrator.start() + + verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ false) + } + + @Test + fun statusBarWindowNotShowing_notifiesBubblesWithStatusBarInvisible() = + testScope.runTest { + setStatusBarMode(TRANSPARENT) + setStatusBarWindowState(StatusBarWindowState.Hidden) + + orchestrator.start() + + verify(mockBubbles).onStatusBarVisibilityChanged(/* visible= */ false) + } + + @Test + fun statusBarModeChange_transitionsToModeWithAnimation() = + testScope.runTest { + awakeDevice() + clearTransientStatusBar() + setStatusBarWindowState(StatusBarWindowState.Showing) + setStatusBarMode(TRANSPARENT) + + orchestrator.start() + + verify(mockPhoneStatusBarTransitions) + .transitionTo(TRANSPARENT.toTransitionModeInt(), /* animate= */ true) + } + + @Test + fun statusBarModeChange_keepsTransitioningAsModeChanges() = + testScope.runTest { + awakeDevice() + clearTransientStatusBar() + setStatusBarWindowState(StatusBarWindowState.Showing) + setStatusBarMode(TRANSPARENT) + + orchestrator.start() + + verify(mockPhoneStatusBarTransitions) + .transitionTo(TRANSPARENT.toTransitionModeInt(), /* animate= */ true) + + setStatusBarMode(OPAQUE) + verify(mockPhoneStatusBarTransitions) + .transitionTo(OPAQUE.toTransitionModeInt(), /* animate= */ true) + + setStatusBarMode(LIGHTS_OUT) + verify(mockPhoneStatusBarTransitions) + .transitionTo(LIGHTS_OUT.toTransitionModeInt(), /* animate= */ true) + + setStatusBarMode(LIGHTS_OUT_TRANSPARENT) + verify(mockPhoneStatusBarTransitions) + .transitionTo(LIGHTS_OUT_TRANSPARENT.toTransitionModeInt(), /* animate= */ true) + } + + @Test + fun statusBarModeChange_transientIsShown_transitionsToModeWithoutAnimation() = + testScope.runTest { + awakeDevice() + setTransientStatusBar() + setStatusBarWindowState(StatusBarWindowState.Showing) + setStatusBarMode(TRANSPARENT) + + orchestrator.start() + + verify(mockPhoneStatusBarTransitions) + .transitionTo(/* mode= */ TRANSPARENT.toTransitionModeInt(), /* animate= */ false) + } + + @Test + fun statusBarModeChange_windowIsHidden_transitionsToModeWithoutAnimation() = + testScope.runTest { + awakeDevice() + clearTransientStatusBar() + setStatusBarWindowState(StatusBarWindowState.Hidden) + setStatusBarMode(TRANSPARENT) + + orchestrator.start() + + verify(mockPhoneStatusBarTransitions) + .transitionTo(/* mode= */ TRANSPARENT.toTransitionModeInt(), /* animate= */ false) + } + + @Test + fun statusBarModeChange_deviceIsAsleep_transitionsToModeWithoutAnimation() = + testScope.runTest { + putDeviceToSleep() + clearTransientStatusBar() + setStatusBarWindowState(StatusBarWindowState.Showing) + setStatusBarMode(TRANSPARENT) + + orchestrator.start() + + verify(mockPhoneStatusBarTransitions) + .transitionTo(/* mode= */ TRANSPARENT.toTransitionModeInt(), /* animate= */ false) + } + + @Test + fun statusBarModeAnimationConditionsChange_withoutBarModeChange_noNewTransitionsHappen() = + testScope.runTest { + awakeDevice() + clearTransientStatusBar() + setStatusBarWindowState(StatusBarWindowState.Showing) + setStatusBarMode(TRANSPARENT) + + orchestrator.start() + + putDeviceToSleep() + awakeDevice() + setTransientStatusBar() + clearTransientStatusBar() + + verify(mockPhoneStatusBarTransitions, times(1)) + .transitionTo(TRANSPARENT.toTransitionModeInt(), /* animate= */ true) + } + + private fun putDeviceToSleep() { + fakePowerRepository.updateWakefulness( + rawState = WakefulnessState.ASLEEP, + lastWakeReason = WakeSleepReason.KEY, + lastSleepReason = WakeSleepReason.KEY, + powerButtonLaunchGestureTriggered = true, + ) + } + + private fun awakeDevice() { + fakePowerRepository.updateWakefulness( + rawState = WakefulnessState.AWAKE, + lastWakeReason = WakeSleepReason.KEY, + lastSleepReason = WakeSleepReason.KEY, + powerButtonLaunchGestureTriggered = true, + ) + } + + private fun setTransientStatusBar() { + statusBarModeRepository.defaultDisplay.showTransient() + } + + private fun clearTransientStatusBar() { + statusBarModeRepository.defaultDisplay.clearTransient() + } + + private fun setStatusBarWindowState(state: StatusBarWindowState) { + fakeStatusBarWindowStateRepositoryStore.defaultDisplay.setWindowState(state) + } + + private fun setStatusBarMode(statusBarMode: StatusBarMode) { + statusBarModeRepository.defaultDisplay.statusBarMode.value = statusBarMode + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index c710c56fd516..15ea811287b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -169,6 +169,7 @@ import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.core.StatusBarInitializerImpl; +import com.android.systemui.statusbar.core.StatusBarOrchestrator; import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.NotificationActivityStarter; @@ -346,6 +347,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory; @Mock private NotificationSettingsInteractor mNotificationSettingsInteractor; @Mock private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; + @Mock private StatusBarOrchestrator mStatusBarOrchestrator; private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings(); diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt new file mode 100644 index 000000000000..cc0597bc3853 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.internal.statusbar + +import android.app.Notification +import android.content.ComponentName +import android.graphics.Rect +import android.graphics.drawable.Icon +import android.hardware.biometrics.IBiometricContextListener +import android.hardware.biometrics.IBiometricSysuiReceiver +import android.hardware.biometrics.PromptInfo +import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback +import android.media.INearbyMediaDevicesProvider +import android.media.MediaRoute2Info +import android.net.Uri +import android.os.Bundle +import android.os.IBinder +import android.os.UserHandle +import android.util.ArrayMap +import android.view.KeyEvent +import com.android.internal.logging.InstanceId +import com.android.internal.statusbar.IAddTileResultCallback +import com.android.internal.statusbar.ISessionListener +import com.android.internal.statusbar.IStatusBar +import com.android.internal.statusbar.IStatusBarService +import com.android.internal.statusbar.IUndoMediaTransferCallback +import com.android.internal.statusbar.LetterboxDetails +import com.android.internal.statusbar.NotificationVisibility +import com.android.internal.statusbar.RegisterStatusBarResult +import com.android.internal.statusbar.StatusBarIcon +import com.android.internal.view.AppearanceRegion +import org.mockito.kotlin.mock + +class FakeStatusBarService : IStatusBarService.Stub() { + + var registeredStatusBar: IStatusBar? = null + private set + + var statusBarIcons = + ArrayMap<String, StatusBarIcon>().also { + it["slot1"] = mock<StatusBarIcon>() + it["slot2"] = mock<StatusBarIcon>() + } + var disabledFlags1 = 1234567 + var appearance = 123 + var appearanceRegions = + arrayOf( + AppearanceRegion( + /* appearance = */ 123, + /* bounds = */ Rect(/* left= */ 4, /* top= */ 3, /* right= */ 2, /* bottom= */ 1), + ), + AppearanceRegion( + /* appearance = */ 345, + /* bounds = */ Rect(/* left= */ 1, /* top= */ 2, /* right= */ 3, /* bottom= */ 4), + ), + ) + var imeWindowVis = 987 + var imeBackDisposition = 654 + var showImeSwitcher = true + var disabledFlags2 = 7654321 + var navbarColorManagedByIme = true + var behavior = 234 + var requestedVisibleTypes = 345 + var packageName = "fake.bar.ser.vice" + var transientBarTypes = 0 + var letterboxDetails = + arrayOf( + LetterboxDetails( + /* letterboxInnerBounds = */ Rect( + /* left= */ 5, + /* top= */ 6, + /* right= */ 7, + /* bottom= */ 8, + ), + /* letterboxFullBounds = */ Rect( + /* left= */ 1, + /* top= */ 2, + /* right= */ 3, + /* bottom= */ 4, + ), + /* appAppearance = */ 123, + ) + ) + + override fun expandNotificationsPanel() {} + + override fun collapsePanels() {} + + override fun togglePanel() {} + + override fun disable(what: Int, token: IBinder, pkg: String) { + disableForUser(what, token, pkg, userId = 0) + } + + override fun disableForUser(what: Int, token: IBinder, pkg: String, userId: Int) {} + + override fun disable2(what: Int, token: IBinder, pkg: String) { + disable2ForUser(what, token, pkg, userId = 0) + } + + override fun disable2ForUser(what: Int, token: IBinder, pkg: String, userId: Int) {} + + override fun getDisableFlags(token: IBinder, userId: Int): IntArray { + return intArrayOf(disabledFlags1, disabledFlags2) + } + + override fun setIcon( + slot: String, + iconPackage: String, + iconId: Int, + iconLevel: Int, + contentDescription: String, + ) {} + + override fun setIconVisibility(slot: String, visible: Boolean) {} + + override fun removeIcon(slot: String) {} + + override fun setImeWindowStatus( + displayId: Int, + vis: Int, + backDisposition: Int, + showImeSwitcher: Boolean, + ) {} + + override fun expandSettingsPanel(subPanel: String) {} + + override fun registerStatusBar(callbacks: IStatusBar): RegisterStatusBarResult { + registeredStatusBar = callbacks + return RegisterStatusBarResult( + statusBarIcons, + disabledFlags1, + appearance, + appearanceRegions, + imeWindowVis, + imeBackDisposition, + showImeSwitcher, + disabledFlags2, + navbarColorManagedByIme, + behavior, + requestedVisibleTypes, + packageName, + transientBarTypes, + letterboxDetails, + ) + } + + override fun onPanelRevealed(clearNotificationEffects: Boolean, numItems: Int) {} + + override fun onPanelHidden() {} + + override fun clearNotificationEffects() {} + + override fun onNotificationClick(key: String, nv: NotificationVisibility) {} + + override fun onNotificationActionClick( + key: String, + actionIndex: Int, + action: Notification.Action, + nv: NotificationVisibility, + generatedByAssistant: Boolean, + ) {} + + override fun onNotificationError( + pkg: String, + tag: String, + id: Int, + uid: Int, + initialPid: Int, + message: String, + userId: Int, + ) {} + + override fun onClearAllNotifications(userId: Int) {} + + override fun onNotificationClear( + pkg: String, + userId: Int, + key: String, + dismissalSurface: Int, + dismissalSentiment: Int, + nv: NotificationVisibility, + ) {} + + override fun onNotificationVisibilityChanged( + newlyVisibleKeys: Array<NotificationVisibility>, + noLongerVisibleKeys: Array<NotificationVisibility>, + ) {} + + override fun onNotificationExpansionChanged( + key: String, + userAction: Boolean, + expanded: Boolean, + notificationLocation: Int, + ) {} + + override fun onNotificationDirectReplied(key: String) {} + + override fun onNotificationSmartSuggestionsAdded( + key: String, + smartReplyCount: Int, + smartActionCount: Int, + generatedByAssistant: Boolean, + editBeforeSending: Boolean, + ) {} + + override fun onNotificationSmartReplySent( + key: String, + replyIndex: Int, + reply: CharSequence, + notificationLocation: Int, + modifiedBeforeSending: Boolean, + ) {} + + override fun onNotificationSettingsViewed(key: String) {} + + override fun onNotificationBubbleChanged(key: String, isBubble: Boolean, flags: Int) {} + + override fun onBubbleMetadataFlagChanged(key: String, flags: Int) {} + + override fun hideCurrentInputMethodForBubbles(displayId: Int) {} + + override fun grantInlineReplyUriPermission( + key: String, + uri: Uri, + user: UserHandle, + packageName: String, + ) {} + + override fun clearInlineReplyUriPermissions(key: String) {} + + override fun onNotificationFeedbackReceived(key: String, feedback: Bundle) {} + + override fun onGlobalActionsShown() {} + + override fun onGlobalActionsHidden() {} + + override fun shutdown() {} + + override fun reboot(safeMode: Boolean) {} + + override fun restart() {} + + override fun addTile(tile: ComponentName) {} + + override fun remTile(tile: ComponentName) {} + + override fun clickTile(tile: ComponentName) {} + + override fun handleSystemKey(key: KeyEvent) {} + + override fun getLastSystemKey(): Int { + return -1 + } + + override fun showPinningEnterExitToast(entering: Boolean) {} + + override fun showPinningEscapeToast() {} + + override fun showAuthenticationDialog( + promptInfo: PromptInfo, + sysuiReceiver: IBiometricSysuiReceiver, + sensorIds: IntArray, + credentialAllowed: Boolean, + requireConfirmation: Boolean, + userId: Int, + operationId: Long, + opPackageName: String, + requestId: Long, + ) {} + + override fun onBiometricAuthenticated(modality: Int) {} + + override fun onBiometricHelp(modality: Int, message: String) {} + + override fun onBiometricError(modality: Int, error: Int, vendorCode: Int) {} + + override fun hideAuthenticationDialog(requestId: Long) {} + + override fun setBiometicContextListener(listener: IBiometricContextListener) {} + + override fun setUdfpsRefreshRateCallback(callback: IUdfpsRefreshRateRequestCallback) {} + + override fun showInattentiveSleepWarning() {} + + override fun dismissInattentiveSleepWarning(animated: Boolean) {} + + override fun startTracing() {} + + override fun stopTracing() {} + + override fun isTracing(): Boolean { + return false + } + + override fun suppressAmbientDisplay(suppress: Boolean) {} + + override fun requestTileServiceListeningState(componentName: ComponentName, userId: Int) {} + + override fun requestAddTile( + componentName: ComponentName, + label: CharSequence, + icon: Icon, + userId: Int, + callback: IAddTileResultCallback, + ) {} + + override fun cancelRequestAddTile(packageName: String) {} + + override fun setNavBarMode(navBarMode: Int) {} + + override fun getNavBarMode(): Int { + return -1 + } + + override fun registerSessionListener(sessionFlags: Int, listener: ISessionListener) {} + + override fun unregisterSessionListener(sessionFlags: Int, listener: ISessionListener) {} + + override fun onSessionStarted(sessionType: Int, instanceId: InstanceId) {} + + override fun onSessionEnded(sessionType: Int, instanceId: InstanceId) {} + + override fun updateMediaTapToTransferSenderDisplay( + displayState: Int, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback, + ) {} + + override fun updateMediaTapToTransferReceiverDisplay( + displayState: Int, + routeInfo: MediaRoute2Info, + appIcon: Icon, + appName: CharSequence, + ) {} + + override fun registerNearbyMediaDevicesProvider(provider: INearbyMediaDevicesProvider) {} + + override fun unregisterNearbyMediaDevicesProvider(provider: INearbyMediaDevicesProvider) {} + + override fun showRearDisplayDialog(currentBaseState: Int) {} +} diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/StatusBarServiceKosmos.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/StatusBarServiceKosmos.kt new file mode 100644 index 000000000000..1304161e81e1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/StatusBarServiceKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.internal.statusbar + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeStatusBarService by Kosmos.Fixture { FakeStatusBarService() } + +var Kosmos.statusBarService by Kosmos.Fixture { fakeStatusBarService } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/DemoModeKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/DemoModeKosmos.kt new file mode 100644 index 000000000000..39384fdec396 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/DemoModeKosmos.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.mockDemoModeController by Kosmos.Fixture { mock<DemoModeController>() } + +var Kosmos.demoModeController by Kosmos.Fixture { mockDemoModeController } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/InitControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/InitControllerKosmos.kt new file mode 100644 index 000000000000..13169e133c9b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/InitControllerKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.initController by Kosmos.Fixture { InitController() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt new file mode 100644 index 000000000000..1c84133d3821 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.viewmodel + +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.shade.domain.interactor.shadeInteractor + +val Kosmos.communalUserActionsViewModel by Fixture { + CommunalUserActionsViewModel( + deviceUnlockedInteractor = deviceUnlockedInteractor, + shadeInteractor = shadeInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt index 3a59f6a8784f..601c14509107 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt @@ -18,6 +18,8 @@ package com.android.systemui.keyguard.data.repository import android.content.Context +import androidx.collection.ArrayMap +import com.android.internal.statusbar.StatusBarIcon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.settings.DisplayTracker import com.android.systemui.statusbar.CommandQueue @@ -31,6 +33,11 @@ class FakeCommandQueue @Inject constructor() : CommandQueue(mock(Context::class.java), mock(DisplayTracker::class.java)) { private val callbacks = mutableListOf<Callbacks>() + val icons = ArrayMap<String, StatusBarIcon>() + + private val perDisplayDisableFlags1 = mutableMapOf<Int, Int>() + private val perDisplayDisableFlags2 = mutableMapOf<Int, Int>() + override fun addCallback(callback: Callbacks) { callbacks.add(callback) } @@ -44,6 +51,23 @@ class FakeCommandQueue @Inject constructor() : } fun callbackCount(): Int = callbacks.size + + override fun setIcon(slot: String, icon: StatusBarIcon) { + icons[slot] = icon + } + + override fun disable(displayId: Int, state1: Int, state2: Int, animate: Boolean) { + perDisplayDisableFlags1[displayId] = state1 + perDisplayDisableFlags2[displayId] = state2 + } + + override fun disable(displayId: Int, state1: Int, state2: Int) { + disable(displayId, state1, state2, /* animate= */ false) + } + + fun disableFlags1ForDisplay(displayId: Int) = perDisplayDisableFlags1[displayId] + + fun disableFlags2ForDisplay(displayId: Int) = perDisplayDisableFlags2[displayId] } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt index 38bc758232a2..2f13ba4e4966 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -38,5 +39,6 @@ val Kosmos.keyguardDismissActionInteractor by alternateBouncerInteractor = alternateBouncerInteractor, shadeInteractor = { shadeInteractor }, keyguardInteractor = { keyguardInteractor }, + sceneInteractor = { sceneInteractor }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/NavigationBarControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/NavigationBarControllerKosmos.kt new file mode 100644 index 000000000000..9e2039eb6b54 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/NavigationBarControllerKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.navigationbar + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.mockNavigationBarController by Kosmos.Fixture { mock<NavigationBarController>() } + +var Kosmos.navigationBarController by Kosmos.Fixture { mockNavigationBarController } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/PluginDependencyKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/PluginDependencyKosmos.kt new file mode 100644 index 000000000000..f1388e9975bf --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/PluginDependencyKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.plugins + +import android.testing.LeakCheck +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.utils.leaks.FakePluginManager +import org.mockito.Mockito.mock +import org.mockito.kotlin.mock + +val Kosmos.leakCheck by Kosmos.Fixture { LeakCheck() } + +val Kosmos.fakePluginManager by Kosmos.Fixture { FakePluginManager(leakCheck) } + +var Kosmos.pluginManager by Kosmos.Fixture { fakePluginManager } + +val Kosmos.pluginDependencyProvider by Kosmos.Fixture { PluginDependencyProvider { pluginManager } } + +val Kosmos.mockPluginDependencyProvider by Kosmos.Fixture { mock<PluginDependencyProvider>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt index d37d8f39b9ee..dbb3e386cc71 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt @@ -19,14 +19,15 @@ package com.android.systemui.qs.composefragment.viewmodel import android.content.res.mainResources import androidx.lifecycle.LifecycleCoroutineScope import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.footerActionsController import com.android.systemui.qs.footerActionsViewModelFactory +import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModel import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.shade.transition.largeScreenShadeInterpolator import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository -import com.android.systemui.statusbar.phone.keyguardBypassController import com.android.systemui.statusbar.sysuiStatusBarStateController val Kosmos.qsFragmentComposeViewModelFactory by @@ -41,11 +42,12 @@ val Kosmos.qsFragmentComposeViewModelFactory by footerActionsViewModelFactory, footerActionsController, sysuiStatusBarStateController, - keyguardBypassController, + deviceEntryInteractor, disableFlagsRepository, largeScreenShadeInterpolator, configurationInteractor, largeScreenHeaderHelper, + tileSquishinessInteractor, lifecycleScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepositoryKosmos.kt new file mode 100644 index 000000000000..d9fad32aa924 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.tileSquishinessRepository by Kosmos.Fixture { TileSquishinessRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt index 3f62b4d9f9cb..546129fe340e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt @@ -20,6 +20,9 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel +import com.android.systemui.qs.panels.ui.viewmodel.tileSquishinessViewModel val Kosmos.infiniteGridLayout by - Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) } + Kosmos.Fixture { + InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel, tileSquishinessViewModel) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractorKosmos.kt new file mode 100644 index 000000000000..23db70fad3a9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractorKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.panels.data.repository.tileSquishinessRepository + +val Kosmos.tileSquishinessInteractor by + Kosmos.Fixture { TileSquishinessInteractor(tileSquishinessRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt index 40d26242e36c..babbd50ece98 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt @@ -27,6 +27,7 @@ val Kosmos.quickQuickSettingsViewModel by currentTilesInteractor, fixedColumnsSizeViewModel, quickQuickSettingsRowInteractor, + tileSquishinessViewModel, iconTilesViewModel, applicationCoroutineScope, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModelKosmos.kt new file mode 100644 index 000000000000..ecc8cd179a9a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModelKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor + +val Kosmos.tileSquishinessViewModel by + Kosmos.Fixture { TileSquishinessViewModel(tileSquishinessInteractor) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt index 737aaf22b557..f842db4c0026 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt @@ -1,7 +1,9 @@ package com.android.systemui.scene +import android.view.View import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.classifier.domain.interactor.falsingInteractor +import com.android.systemui.haptics.msdl.msdlPlayer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.power.domain.interactor.powerInteractor @@ -13,11 +15,13 @@ import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.FakeOverlay import com.android.systemui.scene.ui.viewmodel.SceneContainerGestureFilter +import com.android.systemui.scene.ui.viewmodel.SceneContainerHapticsViewModel import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector import com.android.systemui.settings.displayTracker import com.android.systemui.shade.domain.interactor.shadeInteractor import kotlinx.coroutines.flow.MutableStateFlow +import org.mockito.kotlin.mock var Kosmos.sceneKeys by Fixture { listOf( @@ -68,18 +72,32 @@ val Kosmos.transitionState by Fixture { } val Kosmos.sceneContainerViewModel by Fixture { - SceneContainerViewModel( - sceneInteractor = sceneInteractor, - falsingInteractor = falsingInteractor, - powerInteractor = powerInteractor, - shadeInteractor = shadeInteractor, - splitEdgeDetector = splitEdgeDetector, - gestureFilterFactory = sceneContainerGestureFilterFactory, - displayId = displayTracker.defaultDisplayId, - motionEventHandlerReceiver = {}, - logger = sceneLogger, - ) - .apply { setTransitionState(transitionState) } + sceneContainerViewModelFactory.create(mock<View>(), displayTracker.defaultDisplayId, {}).apply { + setTransitionState(transitionState) + } +} + +val Kosmos.sceneContainerViewModelFactory by Fixture { + object : SceneContainerViewModel.Factory { + override fun create( + view: View, + displayId: Int, + motionEventHandlerReceiver: (SceneContainerViewModel.MotionEventHandler?) -> Unit, + ): SceneContainerViewModel = + SceneContainerViewModel( + sceneInteractor = sceneInteractor, + falsingInteractor = falsingInteractor, + powerInteractor = powerInteractor, + shadeInteractor = shadeInteractor, + splitEdgeDetector = splitEdgeDetector, + logger = sceneLogger, + gestureFilterFactory = sceneContainerGestureFilterFactory, + hapticsViewModelFactory = sceneContainerHapticsViewModelFactory, + view = view, + displayId = displayId, + motionEventHandlerReceiver = motionEventHandlerReceiver, + ) + } } val Kosmos.sceneContainerGestureFilterFactory by Fixture { @@ -92,3 +110,16 @@ val Kosmos.sceneContainerGestureFilterFactory by Fixture { } } } + +val Kosmos.sceneContainerHapticsViewModelFactory by Fixture { + object : SceneContainerHapticsViewModel.Factory { + override fun create(view: View): SceneContainerHapticsViewModel { + return SceneContainerHapticsViewModel( + view = view, + sceneInteractor = sceneInteractor, + shadeInteractor = shadeInteractor, + msdlPlayer = msdlPlayer, + ) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt index 1ceab68604f3..a9f9c82be98b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt @@ -20,3 +20,13 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.util.mockito.mock var Kosmos.shadeViewController by Kosmos.Fixture { mock<ShadeViewController>() } + +val Kosmos.mockNotificationShadeWindowViewController by + Kosmos.Fixture { mock<NotificationShadeWindowViewController>() } + +var Kosmos.notificationShadeWindowViewController by + Kosmos.Fixture { mockNotificationShadeWindowViewController } + +val Kosmos.mockShadeSurface by Kosmos.Fixture { mock<ShadeSurface>() } + +var Kosmos.shadeSurface by Kosmos.Fixture { mockShadeSurface } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt index 48c5121c71c1..0aeea4e1a2e5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelKosmos.kt @@ -19,11 +19,13 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.qs.ui.adapter.qsSceneAdapter +import com.android.systemui.scene.domain.interactor.sceneBackInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor val Kosmos.shadeUserActionsViewModel: ShadeUserActionsViewModel by Fixture { ShadeUserActionsViewModel( qsSceneAdapter = qsSceneAdapter, shadeInteractor = shadeInteractor, + sceneBackInteractor = sceneBackInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt index 27f7f6823cc7..f571c1be9e6e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/CommandQueueKosmos.kt @@ -19,4 +19,10 @@ package com.android.systemui.statusbar import com.android.systemui.kosmos.Kosmos import com.android.systemui.util.mockito.mock -var Kosmos.commandQueue by Kosmos.Fixture { mock<CommandQueue>() } +val Kosmos.mockCommandQueue by Kosmos.Fixture { mock<CommandQueue>() } + +var Kosmos.commandQueue by Kosmos.Fixture { mockCommandQueue } + +val Kosmos.mockCommandQueueCallbacks by Kosmos.Fixture { mock<CommandQueue.Callbacks>() } + +var Kosmos.commandQueueCallbacks by Kosmos.Fixture { mockCommandQueue } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt index 554bdbe0c382..d436cd4f2ed2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt @@ -19,5 +19,7 @@ package com.android.systemui.statusbar import com.android.systemui.kosmos.Kosmos import com.android.systemui.util.mockito.mock -var Kosmos.notificationRemoteInputManager by +val Kosmos.mockNotificationRemoteInputManager by Kosmos.Fixture { mock<NotificationRemoteInputManager>() } + +var Kosmos.notificationRemoteInputManager by Kosmos.Fixture { mockNotificationRemoteInputManager } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/CommandQueueInitializerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/CommandQueueInitializerKosmos.kt new file mode 100644 index 000000000000..cba4e8efe9fe --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/CommandQueueInitializerKosmos.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.core + +import android.content.testableContext +import android.internal.statusbar.fakeStatusBarService +import com.android.systemui.initController +import com.android.systemui.keyguard.data.repository.fakeCommandQueue +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.navigationbar.mockNavigationBarController +import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository +import com.android.systemui.statusbar.mockCommandQueueCallbacks + +var Kosmos.commandQueueInitializer by + Kosmos.Fixture { + CommandQueueInitializer( + testableContext, + fakeCommandQueue, + { mockCommandQueueCallbacks }, + fakeStatusBarModeRepository, + initController, + fakeStatusBarService, + mockNavigationBarController, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/FakeStatusBarInitializer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/FakeStatusBarInitializer.kt new file mode 100644 index 000000000000..edd660490e4d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/FakeStatusBarInitializer.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.core + +import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewUpdatedListener +import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions +import com.android.systemui.statusbar.phone.PhoneStatusBarViewController + +class FakeStatusBarInitializer( + private val statusBarViewController: PhoneStatusBarViewController, + private val statusBarTransitions: PhoneStatusBarTransitions, +) : StatusBarInitializer { + + override var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? = null + set(value) { + field = value + value?.onStatusBarViewUpdated(statusBarViewController, statusBarTransitions) + } + + override fun initializeStatusBar() {} +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt new file mode 100644 index 000000000000..d10320004454 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.core + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.phone.phoneStatusBarTransitions +import com.android.systemui.statusbar.phone.phoneStatusBarViewController + +val Kosmos.fakeStatusBarInitializer by + Kosmos.Fixture { + FakeStatusBarInitializer(phoneStatusBarViewController, phoneStatusBarTransitions) + } + +var Kosmos.statusBarInitializer by Kosmos.Fixture { fakeStatusBarInitializer } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt new file mode 100644 index 000000000000..c53e44d514f7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.core + +import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.mockDemoModeController +import com.android.systemui.plugins.mockPluginDependencyProvider +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.shade.mockNotificationShadeWindowViewController +import com.android.systemui.shade.mockShadeSurface +import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository +import com.android.systemui.statusbar.mockNotificationRemoteInputManager +import com.android.systemui.statusbar.phone.mockAutoHideController +import com.android.systemui.statusbar.window.data.repository.statusBarWindowStateRepositoryStore +import com.android.systemui.statusbar.window.fakeStatusBarWindowController +import com.android.wm.shell.bubbles.bubblesOptional + +val Kosmos.statusBarOrchestrator by + Kosmos.Fixture { + StatusBarOrchestrator( + applicationCoroutineScope, + fakeStatusBarInitializer, + fakeStatusBarWindowController, + fakeStatusBarModeRepository, + mockDemoModeController, + mockPluginDependencyProvider, + mockAutoHideController, + mockNotificationRemoteInputManager, + { mockNotificationShadeWindowViewController }, + mockShadeSurface, + bubblesOptional, + statusBarWindowStateRepositoryStore, + powerInteractor, + primaryBouncerInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/AutoHideKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/AutoHideKosmos.kt new file mode 100644 index 000000000000..090ce31bd43c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/AutoHideKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.mockAutoHideController by Kosmos.Fixture { mock<AutoHideController>() } + +var Kosmos.autoHideController by Kosmos.Fixture { mockAutoHideController } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/PhoneStatusBarKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/PhoneStatusBarKosmos.kt new file mode 100644 index 000000000000..603ee08b6b28 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/PhoneStatusBarKosmos.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import com.android.systemui.kosmos.Kosmos +import org.mockito.Mockito.mock + +val Kosmos.mockPhoneStatusBarViewController: PhoneStatusBarViewController by + Kosmos.Fixture { mock(PhoneStatusBarViewController::class.java) } + +var Kosmos.phoneStatusBarViewController by Kosmos.Fixture { mockPhoneStatusBarViewController } + +val Kosmos.mockPhoneStatusBarTransitions: PhoneStatusBarTransitions by + Kosmos.Fixture { mock(PhoneStatusBarTransitions::class.java) } + +var Kosmos.phoneStatusBarTransitions by Kosmos.Fixture { mockPhoneStatusBarTransitions } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowController.kt new file mode 100644 index 000000000000..528c9d9ec64d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowController.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.window + +import android.view.View +import android.view.ViewGroup +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.fragments.FragmentHostManager +import java.util.Optional + +class FakeStatusBarWindowController : StatusBarWindowController { + + var isAttached = false + private set + + override val statusBarHeight: Int = 0 + + override fun refreshStatusBarHeight() {} + + override fun attach() { + isAttached = true + } + + override fun addViewToWindow(view: View, layoutParams: ViewGroup.LayoutParams) {} + + override val backgroundView: View + get() = throw NotImplementedError() + + override val fragmentHostManager: FragmentHostManager + get() = throw NotImplementedError() + + override fun wrapAnimationControllerIfInStatusBar( + rootView: View, + animationController: ActivityTransitionAnimator.Controller, + ): Optional<ActivityTransitionAnimator.Controller> = Optional.empty() + + override fun setForceStatusBarVisible(forceStatusBarVisible: Boolean) {} + + override fun setOngoingProcessRequiresStatusBarVisible(visible: Boolean) {} +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt new file mode 100644 index 000000000000..c198b35be289 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.window + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeStatusBarWindowController by Kosmos.Fixture { FakeStatusBarWindowController() } + +var Kosmos.statusBarWindowController by Kosmos.Fixture { fakeStatusBarWindowController } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/FakeStatusBarWindowStatePerDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/FakeStatusBarWindowStatePerDisplayRepository.kt new file mode 100644 index 000000000000..6532a7ecc85a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/FakeStatusBarWindowStatePerDisplayRepository.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.window.data.repository + +import android.view.Display +import com.android.systemui.statusbar.window.data.model.StatusBarWindowState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeStatusBarWindowStateRepositoryStore : StatusBarWindowStateRepositoryStore { + + private val perDisplayRepos = mutableMapOf<Int, FakeStatusBarWindowStatePerDisplayRepository>() + + override val defaultDisplay: FakeStatusBarWindowStatePerDisplayRepository = + forDisplay(Display.DEFAULT_DISPLAY) + + override fun forDisplay(displayId: Int): FakeStatusBarWindowStatePerDisplayRepository = + perDisplayRepos.computeIfAbsent(displayId) { + FakeStatusBarWindowStatePerDisplayRepository() + } +} + +class FakeStatusBarWindowStatePerDisplayRepository : StatusBarWindowStatePerDisplayRepository { + + private val _windowState = MutableStateFlow(StatusBarWindowState.Hidden) + + override val windowState = _windowState.asStateFlow() + + fun setWindowState(state: StatusBarWindowState) { + _windowState.value = state + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt index e2b7f5fa0717..2205a3b6e084 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt @@ -21,6 +21,9 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.settings.displayTracker import com.android.systemui.statusbar.commandQueue +val Kosmos.fakeStatusBarWindowStateRepositoryStore by + Kosmos.Fixture { FakeStatusBarWindowStateRepositoryStore() } + class KosmosStatusBarWindowStatePerDisplayRepositoryFactory(private val kosmos: Kosmos) : StatusBarWindowStatePerDisplayRepositoryFactory { override fun create(displayId: Int): StatusBarWindowStatePerDisplayRepositoryImpl { @@ -32,7 +35,7 @@ class KosmosStatusBarWindowStatePerDisplayRepositoryFactory(private val kosmos: } } -val Kosmos.statusBarWindowStateRepositoryStore by +var Kosmos.statusBarWindowStateRepositoryStore: StatusBarWindowStateRepositoryStore by Kosmos.Fixture { StatusBarWindowStateRepositoryStoreImpl( displayId = displayTracker.defaultDisplayId, diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 082459b8c863..b6c8fc7b80c7 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -16,22 +16,35 @@ package com.android.server.appfunctions; +import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DISABLED; +import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED; +import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB; +import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE; + import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_EXECUTOR; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.WorkerThread; -import android.app.appfunctions.AppFunctionService; +import android.app.appfunctions.AppFunctionManager; +import android.app.appfunctions.AppFunctionManagerHelper; +import android.app.appfunctions.AppFunctionRuntimeMetadata; import android.app.appfunctions.AppFunctionStaticMetadataHelper; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; import android.app.appfunctions.ExecuteAppFunctionResponse; +import android.app.appfunctions.IAppFunctionEnabledCallback; import android.app.appfunctions.IAppFunctionManager; import android.app.appfunctions.IAppFunctionService; import android.app.appfunctions.ICancellationCallback; import android.app.appfunctions.IExecuteAppFunctionCallback; import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback; +import android.app.appsearch.AppSearchBatchResult; import android.app.appsearch.AppSearchManager; +import android.app.appsearch.AppSearchManager.SearchContext; import android.app.appsearch.AppSearchResult; +import android.app.appsearch.GenericDocument; +import android.app.appsearch.GetByDocumentIdRequest; +import android.app.appsearch.PutDocumentsRequest; import android.app.appsearch.observer.DocumentChangeInfo; import android.app.appsearch.observer.ObserverCallback; import android.app.appsearch.observer.ObserverSpec; @@ -40,12 +53,15 @@ import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.CancellationSignal; -import android.os.IBinder; import android.os.ICancellationSignal; +import android.os.OutcomeReceiver; +import android.os.ParcelableException; +import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.infra.AndroidFuture; import com.android.server.SystemService.TargetUser; @@ -54,6 +70,7 @@ import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteL import java.util.Objects; import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; /** Implementation of the AppFunctionManagerService. */ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { @@ -64,6 +81,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { private final ServiceHelper mInternalServiceHelper; private final ServiceConfig mServiceConfig; private final Context mContext; + private final Object mLock = new Object(); public AppFunctionManagerServiceImpl(@NonNull Context context) { this( @@ -180,52 +198,214 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { return; } - var unused = - mCallerValidator - .verifyCallerCanExecuteAppFunction( - callingUid, - callingPid, - requestInternal.getCallingPackage(), - targetPackageName, - requestInternal.getClientRequest().getFunctionIdentifier()) - .thenAccept( - canExecute -> { - if (!canExecute) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_DENIED, - "Caller does not have permission to execute" - + " the appfunction", - /* extras= */ null)); - return; - } - Intent serviceIntent = - mInternalServiceHelper.resolveAppFunctionService( - targetPackageName, targetUser); - if (serviceIntent == null) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse - .RESULT_INTERNAL_ERROR, - "Cannot find the target service.", - /* extras= */ null)); - return; - } - bindAppFunctionServiceUnchecked( - requestInternal, - serviceIntent, - targetUser, - localCancelTransport, - safeExecuteAppFunctionCallback, - /* bindFlags= */ Context.BIND_AUTO_CREATE - | Context.BIND_FOREGROUND_SERVICE); - }) - .exceptionally( - ex -> { - safeExecuteAppFunctionCallback.onResult( - mapExceptionToExecuteAppFunctionResponse(ex)); - return null; - }); + mCallerValidator + .verifyCallerCanExecuteAppFunction( + callingUid, + callingPid, + requestInternal.getCallingPackage(), + targetPackageName, + requestInternal.getClientRequest().getFunctionIdentifier()) + .thenAccept( + canExecute -> { + if (!canExecute) { + safeExecuteAppFunctionCallback.onResult( + ExecuteAppFunctionResponse.newFailure( + ExecuteAppFunctionResponse.RESULT_DENIED, + "Caller does not have permission to execute the" + + " appfunction", + /* extras= */ null)); + } + }) + .thenCompose( + isEnabled -> + isAppFunctionEnabled( + requestInternal.getClientRequest().getFunctionIdentifier(), + requestInternal.getClientRequest().getTargetPackageName(), + getAppSearchManagerAsUser(requestInternal.getUserHandle()), + THREAD_POOL_EXECUTOR)) + .thenAccept( + isEnabled -> { + if (!isEnabled) { + throw new DisabledAppFunctionException( + "The app function is disabled"); + } + }) + .thenAccept( + unused -> { + Intent serviceIntent = + mInternalServiceHelper.resolveAppFunctionService( + targetPackageName, targetUser); + if (serviceIntent == null) { + safeExecuteAppFunctionCallback.onResult( + ExecuteAppFunctionResponse.newFailure( + ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, + "Cannot find the target service.", + /* extras= */ null)); + return; + } + bindAppFunctionServiceUnchecked( + requestInternal, + serviceIntent, + targetUser, + localCancelTransport, + safeExecuteAppFunctionCallback, + /* bindFlags= */ Context.BIND_AUTO_CREATE + | Context.BIND_FOREGROUND_SERVICE); + }) + .exceptionally( + ex -> { + safeExecuteAppFunctionCallback.onResult( + mapExceptionToExecuteAppFunctionResponse(ex)); + return null; + }); + } + + private static AndroidFuture<Boolean> isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull String targetPackage, + @NonNull AppSearchManager appSearchManager, + @NonNull Executor executor) { + AndroidFuture<Boolean> future = new AndroidFuture<>(); + AppFunctionManagerHelper.isAppFunctionEnabled( + functionIdentifier, + targetPackage, + appSearchManager, + executor, + new OutcomeReceiver<>() { + @Override + public void onResult(@NonNull Boolean result) { + future.complete(result); + } + + @Override + public void onError(@NonNull Exception error) { + future.completeExceptionally(error); + } + }); + return future; + } + + @Override + public void setAppFunctionEnabled( + @NonNull String callingPackage, + @NonNull String functionIdentifier, + @NonNull UserHandle userHandle, + @AppFunctionManager.EnabledState int enabledState, + @NonNull IAppFunctionEnabledCallback callback) { + try { + mCallerValidator.validateCallingPackage(callingPackage); + } catch (SecurityException e) { + reportException(callback, e); + return; + } + THREAD_POOL_EXECUTOR.execute( + () -> { + try { + // TODO(357551503): Instead of holding a global lock, hold a per-package + // lock. + synchronized (mLock) { + setAppFunctionEnabledInternalLocked( + callingPackage, functionIdentifier, userHandle, enabledState); + } + callback.onSuccess(); + } catch (Exception e) { + Slog.e(TAG, "Error in setAppFunctionEnabled: ", e); + reportException(callback, e); + } + }); + } + + private static void reportException( + @NonNull IAppFunctionEnabledCallback callback, @NonNull Exception exception) { + try { + callback.onError(new ParcelableException(exception)); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to report the exception", e); + } + } + + /** + * Sets the enabled status of a specified app function. + * <p> + * Required to hold a lock to call this function to avoid document changes during the process. + */ + @WorkerThread + @GuardedBy("mLock") + private void setAppFunctionEnabledInternalLocked( + @NonNull String callingPackage, + @NonNull String functionIdentifier, + @NonNull UserHandle userHandle, + @AppFunctionManager.EnabledState int enabledState) + throws Exception { + AppSearchManager perUserAppSearchManager = getAppSearchManagerAsUser(userHandle); + + if (perUserAppSearchManager == null) { + throw new IllegalStateException( + "AppSearchManager not found for user:" + userHandle.getIdentifier()); + } + SearchContext runtimeMetadataSearchContext = + new SearchContext.Builder(APP_FUNCTION_RUNTIME_METADATA_DB).build(); + + try (FutureAppSearchSession runtimeMetadataSearchSession = + new FutureAppSearchSessionImpl( + perUserAppSearchManager, + THREAD_POOL_EXECUTOR, + runtimeMetadataSearchContext)) { + AppFunctionRuntimeMetadata existingMetadata = + new AppFunctionRuntimeMetadata( + getRuntimeMetadataGenericDocument( + callingPackage, + functionIdentifier, + runtimeMetadataSearchSession)); + AppFunctionRuntimeMetadata.Builder newMetadata = + new AppFunctionRuntimeMetadata.Builder(existingMetadata); + switch (enabledState) { + case AppFunctionManager.APP_FUNCTION_STATE_DEFAULT -> { + newMetadata.setEnabled(null); + } + case APP_FUNCTION_STATE_ENABLED -> { + newMetadata.setEnabled(true); + } + case APP_FUNCTION_STATE_DISABLED -> { + newMetadata.setEnabled(false); + } + default -> + throw new IllegalArgumentException( + "Value of EnabledState is unsupported."); + } + AppSearchBatchResult<String, Void> putDocumentBatchResult = + runtimeMetadataSearchSession + .put( + new PutDocumentsRequest.Builder() + .addGenericDocuments(newMetadata.build()) + .build()) + .get(); + if (!putDocumentBatchResult.isSuccess()) { + throw new IllegalStateException("Failed writing updated doc to AppSearch due to " + + putDocumentBatchResult); + } + } + } + + @WorkerThread + @NonNull + private AppFunctionRuntimeMetadata getRuntimeMetadataGenericDocument( + @NonNull String packageName, + @NonNull String functionId, + @NonNull FutureAppSearchSession runtimeMetadataSearchSession) + throws Exception { + String documentId = + AppFunctionRuntimeMetadata.getDocumentIdForAppFunction(packageName, functionId); + GetByDocumentIdRequest request = + new GetByDocumentIdRequest.Builder(APP_FUNCTION_RUNTIME_NAMESPACE) + .addIds(documentId) + .build(); + AppSearchBatchResult<String, GenericDocument> result = + runtimeMetadataSearchSession.getByDocumentId(request).get(); + if (result.isSuccess()) { + return new AppFunctionRuntimeMetadata((result.getSuccesses().get(documentId))); + } + throw new IllegalArgumentException("Function " + functionId + " does not exist"); } private void bindAppFunctionServiceUnchecked( @@ -302,24 +482,27 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } } + private AppSearchManager getAppSearchManagerAsUser(@NonNull UserHandle userHandle) { + return mContext.createContextAsUser(userHandle, /* flags= */ 0) + .getSystemService(AppSearchManager.class); + } + private ExecuteAppFunctionResponse mapExceptionToExecuteAppFunctionResponse(Throwable e) { if (e instanceof CompletionException) { e = e.getCause(); } - - if (e instanceof AppSearchException) { - AppSearchException appSearchException = (AppSearchException) e; - return ExecuteAppFunctionResponse.newFailure( + int resultCode = ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR; + if (e instanceof AppSearchException appSearchException) { + resultCode = mapAppSearchResultFailureCodeToExecuteAppFunctionResponse( - appSearchException.getResultCode()), - appSearchException.getMessage(), - /* extras= */ null); + appSearchException.getResultCode()); + } else if (e instanceof SecurityException) { + resultCode = ExecuteAppFunctionResponse.RESULT_DENIED; + } else if (e instanceof DisabledAppFunctionException) { + resultCode = ExecuteAppFunctionResponse.RESULT_DISABLED; } - return ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, - e.getMessage(), - /* extras= */ null); + resultCode, e.getMessage(), /* extras= */ null); } private int mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(int resultCode) { @@ -434,4 +617,11 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } } } + + /** Throws when executing a disabled app function. */ + private static class DisabledAppFunctionException extends RuntimeException { + private DisabledAppFunctionException(@NonNull String errorMessage) { + super(errorMessage); + } + } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java index 070a99d5bb28..ffca8491abcd 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java @@ -65,8 +65,7 @@ public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> { @NonNull UserHandle userHandle, @NonNull RunServiceCallCallback<T> callback) { OneOffServiceConnection serviceConnection = - new OneOffServiceConnection( - intent, bindFlags, userHandle, callback); + new OneOffServiceConnection(intent, bindFlags, userHandle, callback); return serviceConnection.bindAndRun(); } @@ -93,7 +92,7 @@ public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> { boolean bindServiceResult = mContext.bindServiceAsUser(mIntent, this, mFlags, mUserHandle); - if(!bindServiceResult) { + if (!bindServiceResult) { safeUnbind(); } diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags index 5b271a3730fc..7474df2a91ca 100644 --- a/services/core/java/com/android/server/EventLogTags.logtags +++ b/services/core/java/com/android/server/EventLogTags.logtags @@ -87,7 +87,7 @@ option java_package com.android.server # replaces 27510 with a row per notification 27531 notification_visibility (key|3),(visibile|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1) # a notification emited noise, vibration, or light -27532 notification_alert (key|3),(buzz|1),(beep|1),(blink|1),(politeness|1) +27532 notification_alert (key|3),(buzz|1),(beep|1),(blink|1),(politeness|1),(mute_reason|1) # a notification was added to a autogroup 27533 notification_autogrouped (key|3) # notification was removed from an autogroup diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java index fbe593fd3df1..682eb768a23e 100644 --- a/services/core/java/com/android/server/PackageWatchdog.java +++ b/services/core/java/com/android/server/PackageWatchdog.java @@ -25,6 +25,7 @@ import static com.android.server.crashrecovery.CrashRecoveryUtils.dumpCrashRecov import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.Context; @@ -91,6 +92,7 @@ import java.util.concurrent.TimeUnit; * Monitors the health of packages on the system and notifies interested observers when packages * fail. On failure, the registered observer with the least user impacting mitigation will * be notified. + * @hide */ public class PackageWatchdog { private static final String TAG = "PackageWatchdog"; @@ -108,13 +110,25 @@ public class PackageWatchdog { private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10; + /** Reason for package failure could not be determined. */ public static final int FAILURE_REASON_UNKNOWN = 0; + + /** The package had a native crash. */ public static final int FAILURE_REASON_NATIVE_CRASH = 1; + + /** The package failed an explicit health check. */ public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2; + + /** The app crashed. */ public static final int FAILURE_REASON_APP_CRASH = 3; + + /** The app was not responding. */ public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4; + + /** The device was boot looping. */ public static final int FAILURE_REASON_BOOT_LOOP = 5; + /** @hide */ @IntDef(prefix = { "FAILURE_REASON_" }, value = { FAILURE_REASON_UNKNOWN, FAILURE_REASON_NATIVE_CRASH, @@ -186,7 +200,8 @@ public class PackageWatchdog { // aborted. private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt"; - @GuardedBy("PackageWatchdog.class") + private static final Object sPackageWatchdogLock = new Object(); + @GuardedBy("sPackageWatchdogLock") private static PackageWatchdog sPackageWatchdog; private final Object mLock = new Object(); @@ -278,8 +293,8 @@ public class PackageWatchdog { } /** Creates or gets singleton instance of PackageWatchdog. */ - public static PackageWatchdog getInstance(Context context) { - synchronized (PackageWatchdog.class) { + public static @NonNull PackageWatchdog getInstance(@NonNull Context context) { + synchronized (sPackageWatchdogLock) { if (sPackageWatchdog == null) { new PackageWatchdog(context); } @@ -290,6 +305,7 @@ public class PackageWatchdog { /** * Called during boot to notify when packages are ready on the device so we can start * binding. + * @hide */ public void onPackagesReady() { synchronized (mLock) { @@ -311,6 +327,7 @@ public class PackageWatchdog { * * <p>Observers are expected to call this on boot. It does not specify any packages but * it will resume observing any packages requested from a previous boot. + * @hide */ public void registerHealthObserver(PackageHealthObserver observer) { synchronized (mLock) { @@ -344,6 +361,7 @@ public class PackageWatchdog { * * <p>If {@code durationMs} is less than 1, a default monitoring duration * {@link #DEFAULT_OBSERVING_DURATION_MS} will be used. + * @hide */ public void startObservingHealth(PackageHealthObserver observer, List<String> packageNames, long durationMs) { @@ -407,6 +425,7 @@ public class PackageWatchdog { * Unregisters {@code observer} from listening to package failure. * Additionally, this stops observing any packages that may have previously been observed * even from a previous boot. + * @hide */ public void unregisterHealthObserver(PackageHealthObserver observer) { mLongTaskHandler.post(() -> { @@ -425,7 +444,7 @@ public class PackageWatchdog { * * <p>This method could be called frequently if there is a severe problem on the device. */ - public void onPackageFailure(List<VersionedPackage> packages, + public void onPackageFailure(@NonNull List<VersionedPackage> packages, @FailureReasons int failureReason) { if (packages == null) { Slog.w(TAG, "Could not resolve a list of failing packages"); @@ -566,6 +585,7 @@ public class PackageWatchdog { * * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots * are not counted in bootloop. + * @hide */ @SuppressWarnings("GuardedBy") public void noteBoot() { @@ -620,7 +640,7 @@ public class PackageWatchdog { // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also // avoid holding lock? // This currently adds about 7ms extra to shutdown thread - /** Writes the package information to file during shutdown. */ + /** @hide Writes the package information to file during shutdown. */ public void writeNow() { synchronized (mLock) { // Must only run synchronous tasks as this runs on the ShutdownThread and no other @@ -674,6 +694,7 @@ public class PackageWatchdog { * Since this method can eventually trigger a rollback, it should be called * only once boot has completed {@code onBootCompleted} and not earlier, because the install * session must be entirely completed before we try to rollback. + * @hide */ public void scheduleCheckAndMitigateNativeCrashes() { Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check " @@ -695,7 +716,9 @@ public class PackageWatchdog { return mPackagesExemptFromImpactLevelThreshold; } - /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */ + /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. + * @hide + */ @Retention(SOURCE) @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0, PackageHealthObserverImpact.USER_IMPACT_LEVEL_10, @@ -787,7 +810,7 @@ public class PackageWatchdog { * Identifier for the observer, should not change across device updates otherwise the * watchdog may drop observing packages with the old name. */ - String getUniqueIdentifier(); + @NonNull String getUniqueIdentifier(); /** * An observer will not be pruned if this is set, even if the observer is not explicitly @@ -804,7 +827,7 @@ public class PackageWatchdog { * <p> A persistent observer may choose to start observing certain failing packages, even if * it has not explicitly asked to watch the package with {@link #startObservingHealth}. */ - default boolean mayObservePackage(String packageName) { + default boolean mayObservePackage(@NonNull String packageName) { return false; } } @@ -1240,7 +1263,7 @@ public class PackageWatchdog { } } - /** Convert a {@code LongArrayQueue} to a String of comma-separated values. */ + /** @hide Convert a {@code LongArrayQueue} to a String of comma-separated values. */ public static String longArrayQueueToString(LongArrayQueue queue) { if (queue.size() > 0) { StringBuilder sb = new StringBuilder(); @@ -1254,7 +1277,7 @@ public class PackageWatchdog { return ""; } - /** Parse a comma-separated String of longs into a LongArrayQueue. */ + /** @hide Parse a comma-separated String of longs into a LongArrayQueue. */ public static LongArrayQueue parseLongArrayQueue(String commaSeparatedValues) { LongArrayQueue result = new LongArrayQueue(); if (!TextUtils.isEmpty(commaSeparatedValues)) { @@ -1268,7 +1291,7 @@ public class PackageWatchdog { /** Dump status of every observer in mAllObservers. */ - public void dump(PrintWriter pw) { + public void dump(@NonNull PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); ipw.println("Package Watchdog status"); ipw.increaseIndent(); @@ -1395,6 +1418,7 @@ public class PackageWatchdog { /** * Increments failure counts of {@code packageName}. * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise + * @hide */ @GuardedBy("mLock") public boolean onPackageFailureLocked(String packageName) { @@ -1514,6 +1538,7 @@ public class PackageWatchdog { } } + /** @hide */ @Retention(SOURCE) @IntDef(value = { HealthCheckState.ACTIVE, @@ -1603,7 +1628,9 @@ public class PackageWatchdog { updateHealthCheckStateLocked(); } - /** Writes the salient fields to disk using {@code out}. */ + /** Writes the salient fields to disk using {@code out}. + * @hide + */ @GuardedBy("mLock") public void writeLocked(TypedXmlSerializer out) throws IOException { out.startTag(null, TAG_PACKAGE); diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java index 5f2fbcedce88..8a81aaa1e636 100644 --- a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java +++ b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java @@ -23,7 +23,10 @@ import com.android.server.RescueParty; import com.android.server.SystemService; -/** This class encapsulate the lifecycle methods of CrashRecovery module. */ +/** This class encapsulate the lifecycle methods of CrashRecovery module. + * + * @hide + */ public class CrashRecoveryModule { private static final String TAG = "CrashRecoveryModule"; diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 62d876102720..03fec0115613 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1480,29 +1480,24 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call brightnessState = clampScreenBrightness(brightnessState); } - if (useDozeBrightness) { - // TODO(b/329676661): Introduce a config property to choose between this brightness - // strategy and DOZE_DEFAULT - // On some devices, when auto-brightness is disabled and the device is dozing, we use - // the current brightness setting scaled by the doze scale factor - if ((Float.isNaN(brightnessState) - || displayBrightnessState.getDisplayBrightnessStrategyName() - .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME)) - && mFlags.isDisplayOffloadEnabled() - && mDisplayOffloadSession != null + if (useDozeBrightness && (Float.isNaN(brightnessState) + || displayBrightnessState.getDisplayBrightnessStrategyName() + .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME))) { + if (mFlags.isDisplayOffloadEnabled() && mDisplayOffloadSession != null && (mAutomaticBrightnessController == null || !mAutomaticBrightnessStrategy.shouldUseAutoBrightness())) { + // TODO(b/329676661): Introduce a config property to choose between this brightness + // strategy and DOZE_DEFAULT + // On some devices, when auto-brightness is disabled and the device is dozing, we + // use the current brightness setting scaled by the doze scale factor rawBrightnessState = getDozeBrightnessForOffload(); brightnessState = clampScreenBrightness(rawBrightnessState); updateScreenBrightnessSetting = false; mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_MANUAL); mTempBrightnessEvent.setFlags( mTempBrightnessEvent.getFlags() | BrightnessEvent.FLAG_DOZE_SCALE); - } - - // Use default brightness when dozing unless overridden. - if (Float.isNaN(brightnessState) - && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()) { + } else if (!mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()) { + // Use default brightness when dozing unless overridden. rawBrightnessState = mScreenBrightnessDozeConfig; brightnessState = clampScreenBrightness(rawBrightnessState); mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT); diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index abb21323f7f0..06f419a785f9 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -118,6 +118,37 @@ public final class NotificationAttentionHelper { Intent.ACTION_MANAGED_PROFILE_AVAILABLE, new Pair<>(Intent.EXTRA_QUIET_MODE, false) ); + // Bits 1, 2, 3, 4 are already taken by: beep|buzz|blink|cooldown + static final int MUTE_REASON_NOT_MUTED = 0; + static final int MUTE_REASON_NOT_AUDIBLE = 1 << 5; + static final int MUTE_REASON_SILENT_UPDATE = 1 << 6; + static final int MUTE_REASON_POST_SILENTLY = 1 << 7; + static final int MUTE_REASON_LISTENER_HINT = 1 << 8; + static final int MUTE_REASON_DND = 1 << 9; + static final int MUTE_REASON_GROUP_ALERT = 1 << 10; + static final int MUTE_REASON_FLAG_SILENT = 1 << 11; + static final int MUTE_REASON_RATE_LIMIT = 1 << 12; + static final int MUTE_REASON_OTHER_INSISTENT_PLAYING = 1 << 13; + static final int MUTE_REASON_SUPPRESSED_BUBBLE = 1 << 14; + static final int MUTE_REASON_COOLDOWN = 1 << 15; + + @IntDef(prefix = { "MUTE_REASON_" }, value = { + MUTE_REASON_NOT_MUTED, + MUTE_REASON_NOT_AUDIBLE, + MUTE_REASON_SILENT_UPDATE, + MUTE_REASON_POST_SILENTLY, + MUTE_REASON_LISTENER_HINT, + MUTE_REASON_DND, + MUTE_REASON_GROUP_ALERT, + MUTE_REASON_FLAG_SILENT, + MUTE_REASON_RATE_LIMIT, + MUTE_REASON_OTHER_INSISTENT_PLAYING, + MUTE_REASON_SUPPRESSED_BUBBLE, + MUTE_REASON_COOLDOWN, + }) + @Retention(RetentionPolicy.SOURCE) + @interface MuteReason {} + private final Context mContext; private final PackageManager mPackageManager; private final TelephonyManager mTelephonyManager; @@ -388,6 +419,7 @@ public final class NotificationAttentionHelper { boolean buzz = false; boolean beep = false; boolean blink = false; + @MuteReason int shouldMuteReason = MUTE_REASON_NOT_MUTED; final String key = record.getKey(); @@ -395,10 +427,6 @@ public final class NotificationAttentionHelper { Log.d(TAG, "buzzBeepBlinkLocked " + record); } - if (isPoliteNotificationFeatureEnabled(record)) { - mStrategy.onNotificationPosted(record); - } - // Should this notification make noise, vibe, or use the LED? final boolean aboveThreshold = mIsAutomotive @@ -443,7 +471,8 @@ public final class NotificationAttentionHelper { boolean vibrateOnly = hasValidVibrate && mNotificationCooldownVibrateUnlocked && mUserPresent; boolean hasAudibleAlert = hasValidSound || hasValidVibrate; - if (hasAudibleAlert && !shouldMuteNotificationLocked(record, signals)) { + shouldMuteReason = shouldMuteNotificationLocked(record, signals, hasAudibleAlert); + if (shouldMuteReason == MUTE_REASON_NOT_MUTED) { if (!sentAccessibilityEvent) { sendAccessibilityEvent(record); sentAccessibilityEvent = true; @@ -541,15 +570,17 @@ public final class NotificationAttentionHelper { } } final int buzzBeepBlinkLoggingCode = - (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0) | getPoliteBit(record); + (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0) + | getPoliteBit(record) | shouldMuteReason; if (buzzBeepBlinkLoggingCode > 0) { MetricsLogger.action(record.getLogMaker() .setCategory(MetricsEvent.NOTIFICATION_ALERT) .setType(MetricsEvent.TYPE_OPEN) .setSubtype(buzzBeepBlinkLoggingCode)); EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0, - getPolitenessState(record)); + getPolitenessState(record), shouldMuteReason); } + if (Flags.politeNotifications()) { // Update last alert time if (buzz || beep) { @@ -594,41 +625,46 @@ public final class NotificationAttentionHelper { mNMP.getNotificationByKey(mVibrateNotificationKey)); } - boolean shouldMuteNotificationLocked(final NotificationRecord record, final Signals signals) { + @MuteReason int shouldMuteNotificationLocked(final NotificationRecord record, + final Signals signals, boolean hasAudibleAlert) { + // Suppressed because no audible alert + if (!hasAudibleAlert) { + return MUTE_REASON_NOT_AUDIBLE; + } // Suppressed because it's a silent update final Notification notification = record.getNotification(); if (record.isUpdate && (notification.flags & FLAG_ONLY_ALERT_ONCE) != 0) { - return true; + return MUTE_REASON_SILENT_UPDATE; } // Suppressed because a user manually unsnoozed something (or similar) if (record.shouldPostSilently()) { - return true; + return MUTE_REASON_POST_SILENTLY; } // muted by listener final String disableEffects = disableNotificationEffects(record, signals.listenerHints); if (disableEffects != null) { ZenLog.traceDisableEffects(record, disableEffects); - return true; + return MUTE_REASON_LISTENER_HINT; } // suppressed due to DND if (record.isIntercepted()) { - return true; + return MUTE_REASON_DND; } // Suppressed because another notification in its group handles alerting if (record.getSbn().isGroup()) { if (notification.suppressAlertingDueToGrouping()) { - return true; + return MUTE_REASON_GROUP_ALERT; } } // Suppressed because notification was explicitly flagged as silent if (android.service.notification.Flags.notificationSilentFlag()) { if (notification.isSilent()) { - return true; + return MUTE_REASON_FLAG_SILENT; } } @@ -636,12 +672,12 @@ public final class NotificationAttentionHelper { final String pkg = record.getSbn().getPackageName(); if (mUsageStats.isAlertRateLimited(pkg)) { Slog.e(TAG, "Muting recently noisy " + record.getKey()); - return true; + return MUTE_REASON_RATE_LIMIT; } // A different looping ringtone, such as an incoming call is playing if (isCurrentlyInsistent() && !isInsistentUpdate(record)) { - return true; + return MUTE_REASON_OTHER_INSISTENT_PLAYING; } // Suppressed since it's a non-interruptive update to a bubble-suppressed notification @@ -650,11 +686,23 @@ public final class NotificationAttentionHelper { if (record.isUpdate && !record.isInterruptive() && isBubbleOrOverflowed && record.getNotification().getBubbleMetadata() != null) { if (record.getNotification().getBubbleMetadata().isNotificationSuppressed()) { - return true; + return MUTE_REASON_SUPPRESSED_BUBBLE; } } - return false; + if (isPoliteNotificationFeatureEnabled(record)) { + // Notify the politeness strategy that an alerting notification is posted + if (!isInsistentUpdate(record)) { + mStrategy.onNotificationPosted(record); + } + + // Suppress if politeness is muted and it's not an update for insistent + if (getPolitenessState(record) == PolitenessStrategy.POLITE_STATE_MUTED) { + return MUTE_REASON_COOLDOWN; + } + } + + return MUTE_REASON_NOT_MUTED; } private boolean isLoopingRingtoneNotification(final NotificationRecord playingRecord) { @@ -1201,12 +1249,6 @@ public final class NotificationAttentionHelper { mApplyPerPackage = applyPerPackage; } - boolean shouldIgnoreNotification(final NotificationRecord record) { - // Ignore auto-group summaries => don't count them as app-posted notifications - // for the cooldown budget - return (record.getSbn().isGroup() && GroupHelper.isAggregatedGroup(record)); - } - /** * Get the key that determines the grouping for the cooldown behavior. * @@ -1358,10 +1400,6 @@ public final class NotificationAttentionHelper { @Override public void onNotificationPosted(final NotificationRecord record) { - if (shouldIgnoreNotification(record)) { - return; - } - long timeSinceLastNotif = System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record); @@ -1434,10 +1472,6 @@ public final class NotificationAttentionHelper { @Override void onNotificationPosted(NotificationRecord record) { if (isAvalancheActive()) { - if (shouldIgnoreNotification(record)) { - return; - } - long timeSinceLastNotif = System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record); diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java index f78c4488cbfb..d206c66ed09a 100644 --- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java +++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java @@ -99,6 +99,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve // True if needing to roll back only rebootless apexes when native crash happens private boolean mTwoPhaseRollbackEnabled; + /** @hide */ @VisibleForTesting public RollbackPackageHealthObserver(Context context, ApexManager apexManager) { mContext = context; @@ -123,7 +124,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve } } - RollbackPackageHealthObserver(Context context) { + public RollbackPackageHealthObserver(@NonNull Context context) { this(context, ApexManager.getInstance()); } @@ -239,8 +240,8 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve return false; } - @Override + @NonNull public String getUniqueIdentifier() { return NAME; } @@ -251,7 +252,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve } @Override - public boolean mayObservePackage(String packageName) { + public boolean mayObservePackage(@NonNull String packageName) { if (getAvailableRollbacks().isEmpty()) { return false; } @@ -281,12 +282,14 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve * This may cause {@code packages} to be rolled back if they crash too freqeuntly. */ @AnyThread - void startObservingHealth(List<String> packages, long durationMs) { + @NonNull + public void startObservingHealth(@NonNull List<String> packages, @NonNull long durationMs) { PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs); } @AnyThread - void notifyRollbackAvailable(RollbackInfo rollback) { + @NonNull + public void notifyRollbackAvailable(@NonNull RollbackInfo rollback) { mHandler.post(() -> { // Enable two-phase rollback when a rebootless apex rollback is made available. // We assume the rebootless apex is stable and is less likely to be the cause @@ -314,7 +317,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve * to check for native crashes and mitigate them if needed. */ @AnyThread - void onBootCompletedAsync() { + public void onBootCompletedAsync() { mHandler.post(()->onBootCompleted()); } diff --git a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java b/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java index 79560ce27919..9cfed02f9355 100644 --- a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java +++ b/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java @@ -51,6 +51,7 @@ import java.util.List; /** * This class handles the logic for logging Watchdog-triggered rollback events. + * @hide */ public final class WatchdogRollbackLogger { private static final String TAG = "WatchdogRollbackLogger"; diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 946b61ad5fd9..2512e11b3fed 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -3316,6 +3316,21 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return false; } + /* + * Attempt to bind the wallpaper given by `componentName`, returning true on success otherwise + * false. + * + * When called, `wallpaper` is in a deliberately inconsistent state. Most fields have been + * updated to describe the desired wallpaper, but the ComponentName is not updated until + * binding is successful. This is required for maybeDetachWallpapers() to work correctly. + * + * The late update of the component field should cause multi-threading headaches with + * WallpaperConnection#onServiceConnected, but doesn't because onServiceConnected required + * `mLock` and `bindWallpaperComponentLocked` is always called with that lock, which prevents a + * race condition. + * + * This is a major motivation for making WallpaperData immutable per b/267170056. + */ boolean bindWallpaperComponentLocked(ComponentName componentName, boolean force, boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) { if (DEBUG_LIVE) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 12d733fc8c1a..ca93075f2925 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2154,7 +2154,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } mAtmService.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, packageName); - mActivityRecordInputSink = new ActivityRecordInputSink(this, sourceRecord); + final boolean appOptInTouchPassThrough = + options != null && options.isAllowPassThroughOnTouchOutside(); + mActivityRecordInputSink = new ActivityRecordInputSink( + this, sourceRecord, appOptInTouchPassThrough); mAppActivityEmbeddingSplitsEnabled = isAppActivityEmbeddingSplitsEnabled(); mAllowUntrustedEmbeddingStateSharing = getAllowUntrustedEmbeddingStateSharingProperty(); @@ -3171,14 +3174,23 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return getWindowConfiguration().canReceiveKeys() && !mWaitForEnteringPinnedMode; } - boolean isResizeable() { - return isResizeable(/* checkPictureInPictureSupport */ true); + /** + * Returns {@code true} if the fixed orientation, aspect ratio, resizability of this activity + * will be ignored. + */ + boolean isUniversalResizeable() { + return mWmService.mConstants.mIgnoreActivityOrientationRequest + && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME + // If the user preference respects aspect ratio, then it becomes non-resizable. + && !mAppCompatController.getAppCompatOverrides().getAppCompatAspectRatioOverrides() + .shouldApplyUserMinAspectRatioOverride(); } - boolean isResizeable(boolean checkPictureInPictureSupport) { + boolean isResizeable() { return mAtmService.mForceResizableActivities || ActivityInfo.isResizeableMode(info.resizeMode) - || (info.supportsPictureInPicture() && checkPictureInPictureSupport) + || info.supportsPictureInPicture() + || isUniversalResizeable() // If the activity can be embedded, it should inherit the bounds of task fragment. || isEmbedded(); } @@ -6397,7 +6409,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // and the token could be null. return; } - r.mDisplayContent.mAppCompatCameraPolicy.onActivityRefreshed(r); + final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy + .getAppCompatCameraPolicy(r); + if (cameraPolicy != null) { + cameraPolicy.onActivityRefreshed(r); + } } static void splashScreenAttachedLocked(IBinder token) { @@ -8162,11 +8178,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override @ActivityInfo.ScreenOrientation protected int getOverrideOrientation() { - final int candidateOrientation; - if (!mWmService.mConstants.mIgnoreActivityOrientationRequest - || info.applicationInfo.category == ApplicationInfo.CATEGORY_GAME) { - candidateOrientation = super.getOverrideOrientation(); - } else { + int candidateOrientation = super.getOverrideOrientation(); + if (isUniversalResizeable() && ActivityInfo.isFixedOrientation(candidateOrientation)) { candidateOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; } return mAppCompatController.getOrientationPolicy() @@ -9442,8 +9455,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (!shouldBeResumed(/* activeActivity */ null)) { return; } - mDisplayContent.mAppCompatCameraPolicy.onActivityConfigurationChanging( - this, newConfig, lastReportedConfig); + + final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( + this); + if (cameraPolicy != null) { + cameraPolicy.onActivityConfigurationChanging(this, newConfig, lastReportedConfig); + } } /** Get process configuration, or global config if the process is not set. */ @@ -10025,7 +10042,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } StringBuilder sb = new StringBuilder(128); sb.append("ActivityRecord{"); - sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(System.identityHashCode(this)); sb.append(" u"); sb.append(mUserId); sb.append(' '); diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java index 1a197875ba31..fa5beca31ec1 100644 --- a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java +++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java @@ -16,13 +16,18 @@ package com.android.server.wm; +import android.app.ActivityOptions; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; +import android.os.Build; import android.os.InputConfig; import android.view.InputWindowHandle; import android.view.SurfaceControl; import android.view.WindowManager; +import com.android.window.flags.Flags; + /** * Creates a InputWindowHandle that catches all touches that would otherwise pass through an * Activity. @@ -35,6 +40,21 @@ class ActivityRecordInputSink { @ChangeId static final long ENABLE_TOUCH_OPAQUE_ACTIVITIES = 194480991L; + // TODO(b/369605358) Update EnabledSince when SDK 36 version code is available. + /** + * If the app's target SDK is 36+, pass-through touches from a cross-uid overlaying activity is + * blocked by default. The activity may opt in to receive pass-through touches using + * {@link ActivityOptions#setAllowPassThroughOnTouchOutside}, which allows the to-be-launched + * cross-uid overlaying activity and other activities in that app to pass through touches. The + * activity needs to ensure that it trusts the overlaying app and its content is not vulnerable + * to UI redressing attacks. + * + * @see ActivityOptions#setAllowPassThroughOnTouchOutside + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT) + static final long ENABLE_OVERLAY_TOUCH_PASS_THROUGH_OPT_IN_ENFORCEMENT = 358129114L; + private final ActivityRecord mActivityRecord; private final boolean mIsCompatEnabled; private final String mName; @@ -42,13 +62,24 @@ class ActivityRecordInputSink { private InputWindowHandleWrapper mInputWindowHandleWrapper; private SurfaceControl mSurfaceControl; - ActivityRecordInputSink(ActivityRecord activityRecord, ActivityRecord sourceRecord) { + ActivityRecordInputSink(ActivityRecord activityRecord, ActivityRecord sourceRecord, + boolean appOptInTouchPassThrough) { mActivityRecord = activityRecord; mIsCompatEnabled = CompatChanges.isChangeEnabled(ENABLE_TOUCH_OPAQUE_ACTIVITIES, mActivityRecord.getUid()); mName = Integer.toHexString(System.identityHashCode(this)) + " ActivityRecordInputSink " + mActivityRecord.mActivityComponent.flattenToShortString(); - if (sourceRecord != null) { + + if (sourceRecord == null) { + return; + } + // If the source activity has target sdk 36+, it is required to opt in to receive + // pass-through touches from the overlaying activity. + final boolean isTouchPassThroughOptInEnforced = CompatChanges.isChangeEnabled( + ENABLE_OVERLAY_TOUCH_PASS_THROUGH_OPT_IN_ENFORCEMENT, + sourceRecord.getUid()); + if (!Flags.touchPassThroughOptIn() || !isTouchPassThroughOptInEnforced + || appOptInTouchPassThrough) { sourceRecord.mAllowedTouchUid = mActivityRecord.getUid(); } } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 4092a0b96c5d..2ba300a71e38 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1029,6 +1029,7 @@ class ActivityStarter { if (requestCode >= 0 && !sourceRecord.finishing) { resultRecord = sourceRecord; } + request.logMessage.append(" (sr=" + System.identityHashCode(sourceRecord) + ")"); } } diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java index f245efd7ff0e..0e666296dc33 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java @@ -255,8 +255,8 @@ class AppCompatAspectRatioOverrides { mActivityRecord.getOverrideOrientation()); final AppCompatCameraOverrides cameraOverrides = mActivityRecord.mAppCompatController.getAppCompatCameraOverrides(); - final AppCompatCameraPolicy cameraPolicy = - mActivityRecord.mAppCompatController.getAppCompatCameraPolicy(); + final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( + mActivityRecord); // Don't resize to split screen size when in book mode if letterbox position is centered return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape) || cameraOverrides.isCameraCompatSplitScreenAspectRatioAllowed() diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java index 51ef87dcab1b..3b023fe451bf 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java @@ -72,6 +72,15 @@ class AppCompatAspectRatioPolicy { float getDesiredAspectRatio(@NonNull Configuration newParentConfig, @NonNull Rect parentBounds) { + // If in camera compat mode, aspect ratio from the camera compat policy has priority over + // default letterbox aspect ratio. + final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( + mActivityRecord); + if (cameraPolicy != null && cameraPolicy.shouldCameraCompatControlAspectRatio( + mActivityRecord)) { + return cameraPolicy.getCameraCompatAspectRatio(mActivityRecord); + } + final float letterboxAspectRatioOverride = mAppCompatOverrides.getAppCompatAspectRatioOverrides() .getFixedOrientationLetterboxAspectRatio(newParentConfig); @@ -114,20 +123,20 @@ class AppCompatAspectRatioPolicy { return mTransparentPolicy.getInheritedMinAspectRatio(); } final ActivityInfo info = mActivityRecord.info; - if (info.applicationInfo == null) { - return info.getMinAspectRatio(); - } final AppCompatAspectRatioOverrides aspectRatioOverrides = mAppCompatOverrides.getAppCompatAspectRatioOverrides(); if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) { return aspectRatioOverrides.getUserMinAspectRatio(); } - final DisplayContent displayContent = mActivityRecord.getDisplayContent(); - final boolean shouldOverrideMinAspectRatioForCamera = displayContent != null - && displayContent.mAppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera( - mActivityRecord); + final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( + mActivityRecord); + final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null + && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord); if (!aspectRatioOverrides.shouldOverrideMinAspectRatio() && !shouldOverrideMinAspectRatioForCamera) { + if (mActivityRecord.isUniversalResizeable()) { + return 0; + } return info.getMinAspectRatio(); } @@ -170,6 +179,9 @@ class AppCompatAspectRatioPolicy { if (mTransparentPolicy.isRunning()) { return mTransparentPolicy.getInheritedMaxAspectRatio(); } + if (mActivityRecord.isUniversalResizeable()) { + return 0; + } return mActivityRecord.info.getMaxAspectRatio(); } diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java index 5338c01666fe..f6090eb89345 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java @@ -18,6 +18,8 @@ package com.android.server.wm; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.pm.ActivityInfo.ScreenOrientation; @@ -74,6 +76,12 @@ class AppCompatCameraPolicy { } } + @Nullable + static AppCompatCameraPolicy getAppCompatCameraPolicy(@NonNull ActivityRecord activityRecord) { + return activityRecord.mDisplayContent != null + ? activityRecord.mDisplayContent.mAppCompatCameraPolicy : null; + } + /** * "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle. * This allows to clear cached values in apps (e.g. display or camera rotation) that influence @@ -167,12 +175,37 @@ class AppCompatCameraPolicy { : SCREEN_ORIENTATION_UNSPECIFIED; } + // TODO(b/369070416): have policies implement the same interface. + boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) { + return (mDisplayRotationCompatPolicy != null + && mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation( + activity)) + || (mCameraCompatFreeformPolicy != null + && mCameraCompatFreeformPolicy.shouldCameraCompatControlOrientation( + activity)); + } + + // TODO(b/369070416): have policies implement the same interface. + boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) { + return (mDisplayRotationCompatPolicy != null + && mDisplayRotationCompatPolicy.shouldCameraCompatControlAspectRatio( + activity)) + || (mCameraCompatFreeformPolicy != null + && mCameraCompatFreeformPolicy.shouldCameraCompatControlAspectRatio( + activity)); + } + + // TODO(b/369070416): have policies implement the same interface. /** - * @return {@code true} if the Camera is active for the provided {@link ActivityRecord}. + * @return {@code true} if the Camera is active for the provided {@link ActivityRecord} and + * any camera compat treatment could be triggered for the current windowing mode. */ - boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) { - return mDisplayRotationCompatPolicy != null - && mDisplayRotationCompatPolicy.isCameraActive(activity, mustBeFullscreen); + private boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity) { + return (mDisplayRotationCompatPolicy != null + && mDisplayRotationCompatPolicy.isCameraRunningAndWindowingModeEligible(activity, + /* mustBeFullscreen */ true)) + || (mCameraCompatFreeformPolicy != null && mCameraCompatFreeformPolicy + .isCameraRunningAndWindowingModeEligible(activity)); } @Nullable @@ -183,12 +216,24 @@ class AppCompatCameraPolicy { return null; } + // TODO(b/369070416): have policies implement the same interface. + float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) { + float displayRotationCompatPolicyAspectRatio = mDisplayRotationCompatPolicy != null + ? mDisplayRotationCompatPolicy.getCameraCompatAspectRatio(activity) + : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; + float cameraCompatFreeformPolicyAspectRatio = mCameraCompatFreeformPolicy != null + ? mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(activity) + : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; + return Math.max(displayRotationCompatPolicyAspectRatio, + cameraCompatFreeformPolicyAspectRatio); + } + /** * Whether we should apply the min aspect ratio per-app override only when an app is connected * to the camera. */ boolean shouldOverrideMinAspectRatioForCamera(@NonNull ActivityRecord activityRecord) { - return isCameraActive(activityRecord, /* mustBeFullscreen= */ true) + return isCameraRunningAndWindowingModeEligible(activityRecord) && activityRecord.mAppCompatController.getAppCompatCameraOverrides() .isOverrideMinAspectRatioForCameraEnabled(); } diff --git a/services/core/java/com/android/server/wm/AppCompatConfiguration.java b/services/core/java/com/android/server/wm/AppCompatConfiguration.java index 42378aaf6c05..38c6de146293 100644 --- a/services/core/java/com/android/server/wm/AppCompatConfiguration.java +++ b/services/core/java/com/android/server/wm/AppCompatConfiguration.java @@ -290,6 +290,10 @@ final class AppCompatConfiguration { // is enabled and activity is connected to the camera in fullscreen. private final boolean mIsCameraCompatSplitScreenAspectRatioEnabled; + // Which aspect ratio to use when camera compat treatment is enabled and an activity eligible + // for treatment is connected to the camera. + private float mCameraCompatAspectRatio; + // Whether activity "refresh" in camera compatibility treatment is enabled. // See RefreshCallbackItem for context. private boolean mIsCameraCompatTreatmentRefreshEnabled = true; @@ -363,6 +367,8 @@ final class AppCompatConfiguration { .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled); mIsCameraCompatSplitScreenAspectRatioEnabled = mContext.getResources().getBoolean( R.bool.config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled); + mCameraCompatAspectRatio = mContext.getResources().getFloat( + R.dimen.config_windowManagerCameraCompatAspectRatio); mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled); @@ -1320,6 +1326,31 @@ final class AppCompatConfiguration { } /** + * Overrides aspect ratio to use when camera compat treatment is enabled and an activity + * eligible for treatment is connected to the camera. + */ + void setCameraCompatAspectRatio(float aspectRatio) { + mCameraCompatAspectRatio = aspectRatio; + } + + /** + * Which aspect ratio to use when camera compat treatment is enabled and an activity eligible + * for treatment is connected to the camera. + */ + float getCameraCompatAspectRatio() { + return mCameraCompatAspectRatio; + } + + /** + * Resets aspect ratio to use when camera compat treatment is enabled and an activity eligible + * for treatment is connected to the camera. + */ + void resetCameraCompatAspectRatio() { + mCameraCompatAspectRatio = mContext.getResources().getFloat(R.dimen + .config_windowManagerCameraCompatAspectRatio); + } + + /** * Checks whether rotation compat policy for immersive apps that prevents auto rotation * into non-optimal screen orientation while in fullscreen is enabled at build time. This is * used when we need to safely initialize a component before the {@link DeviceConfig} flag diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index 173362c16728..6c344c6d850a 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -16,7 +16,6 @@ package com.android.server.wm; import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.pm.PackageManager; import com.android.server.wm.utils.OptPropFactory; @@ -118,14 +117,6 @@ class AppCompatController { return mAppCompatOverrides.getAppCompatResizeOverrides(); } - @Nullable - AppCompatCameraPolicy getAppCompatCameraPolicy() { - if (mActivityRecord.mDisplayContent != null) { - return mActivityRecord.mDisplayContent.mAppCompatCameraPolicy; - } - return null; - } - @NonNull AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() { return mAppCompatReachabilityPolicy; diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java index 7477c6272d89..5bd4aeb64b90 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java @@ -58,16 +58,17 @@ class AppCompatOrientationPolicy { && displayContent.getIgnoreOrientationRequest(); final boolean shouldApplyUserFullscreenOverride = mAppCompatOverrides .getAppCompatAspectRatioOverrides().shouldApplyUserFullscreenOverride(); - final boolean isCameraActive = displayContent != null - && displayContent.mAppCompatCameraPolicy.isCameraActive(mActivityRecord, - /* mustBeFullscreen */ true); + final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy + .getAppCompatCameraPolicy(mActivityRecord); + final boolean shouldCameraCompatControlOrientation = cameraPolicy != null + && cameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord); if (shouldApplyUserFullscreenOverride && isIgnoreOrientationRequestEnabled // Do not override orientation to fullscreen for camera activities. // Fixed-orientation activities are rarely tested in other orientations, and it // often results in sideways or stretched previews. As the camera compat treatment // targets fixed-orientation activities, overriding the orientation disables the // treatment. - && !isCameraActive) { + && !shouldCameraCompatControlOrientation) { Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for " + mActivityRecord + " is overridden to " + screenOrientationToString(SCREEN_ORIENTATION_USER) @@ -113,7 +114,7 @@ class AppCompatOrientationPolicy { // often results in sideways or stretched previews. As the camera compat treatment // targets fixed-orientation activities, overriding the orientation disables the // treatment. - && !isCameraActive) { + && !shouldCameraCompatControlOrientation) { Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate) + " for " + mActivityRecord + " is overridden to " + screenOrientationToString(SCREEN_ORIENTATION_USER)); @@ -192,8 +193,9 @@ class AppCompatOrientationPolicy { + mActivityRecord); return true; } - final AppCompatCameraPolicy cameraPolicy = mActivityRecord.mAppCompatController - .getAppCompatCameraPolicy(); + + final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy + .getAppCompatCameraPolicy(mActivityRecord); if (cameraPolicy != null && cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord)) { Slog.w(TAG, "Ignoring orientation update to " diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java index d6caa1a248b4..290e71d5b37d 100644 --- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java +++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java @@ -29,6 +29,7 @@ import static android.content.res.Configuration.ORIENTATION_UNDEFINED; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; +import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; @@ -123,7 +124,7 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa * </ul> */ @VisibleForTesting - boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) { + boolean isCameraCompatForFreeformEnabledForActivity(@NonNull ActivityRecord activity) { return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled( ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT); } @@ -170,6 +171,36 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa return true; } + boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) { + return isCameraRunningAndWindowingModeEligible(activity); + } + + boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity) { + return activity.inFreeformWindowingMode() + && mCameraStateMonitor.isCameraRunningForActivity(activity); + } + + boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) { + // Camera compat should direct aspect ratio when in camera compat mode, unless an app has a + // different camera compat aspect ratio set: this allows per-app camera compat override + // aspect ratio to be smaller than the default. + return isInCameraCompatMode(activity) && !activity.mAppCompatController + .getAppCompatCameraOverrides().isOverrideMinAspectRatioForCameraEnabled(); + } + + private boolean isInCameraCompatMode(@NonNull ActivityRecord activity) { + return activity.mAppCompatController.getAppCompatCameraOverrides() + .getFreeformCameraCompatMode() != CAMERA_COMPAT_FREEFORM_NONE; + } + + float getCameraCompatAspectRatio(@NonNull ActivityRecord activityRecord) { + if (shouldCameraCompatControlAspectRatio(activityRecord)) { + return activityRecord.mWmService.mAppCompatConfiguration.getCameraCompatAspectRatio(); + } + + return MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; + } + private void forceUpdateActivityAndTask(ActivityRecord cameraActivity) { cameraActivity.recomputeConfiguration(); cameraActivity.updateReportedConfigurationAndSend(); @@ -225,7 +256,7 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa */ private boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity) { int orientation = activity.getRequestedConfigurationOrientation(); - return shouldApplyFreeformTreatmentForCameraCompat(activity) + return isCameraCompatForFreeformEnabledForActivity(activity) && mCameraStateMonitor.isCameraRunningForActivity(activity) && orientation != ORIENTATION_UNDEFINED && activity.inFreeformWindowingMode() diff --git a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java index 192469183a54..3b2f723fb172 100644 --- a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java @@ -188,21 +188,21 @@ public class DesktopAppCompatAspectRatioPolicy { } final ActivityInfo info = mActivityRecord.info; - if (info.applicationInfo == null) { - return info.getMinAspectRatio(); - } - final AppCompatAspectRatioOverrides aspectRatioOverrides = mAppCompatOverrides.getAppCompatAspectRatioOverrides(); if (shouldApplyUserMinAspectRatioOverride(task)) { return aspectRatioOverrides.getUserMinAspectRatio(); } - final DisplayContent dc = task.mDisplayContent; - final boolean shouldOverrideMinAspectRatioForCamera = dc != null - && dc.mAppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord); + final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy( + mActivityRecord); + final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null + && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord); if (!aspectRatioOverrides.shouldOverrideMinAspectRatio() && !shouldOverrideMinAspectRatioForCamera) { + if (mActivityRecord.isUniversalResizeable()) { + return 0; + } return info.getMinAspectRatio(); } @@ -246,6 +246,9 @@ public class DesktopAppCompatAspectRatioPolicy { if (mTransparentPolicy.isRunning()) { return mTransparentPolicy.getInheritedMaxAspectRatio(); } + if (mActivityRecord.isUniversalResizeable()) { + return 0; + } return mActivityRecord.info.getMaxAspectRatio(); } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index e6f6215b5f7f..1ac0bb0e41c6 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -2156,6 +2156,11 @@ public class DisplayPolicy { } mDecorInsets.invalidate(); mDecorInsets.mInfoForRotation[rotation].set(newInfo); + if (!mService.mDisplayEnabled) { + // There could be other pending changes during booting. It might be better to let the + // clients receive the new states earlier. + return true; + } return !sameConfigFrame; } diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index efc38439bfcf..90f8b4900c3f 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -30,6 +30,7 @@ import static android.content.res.Configuration.ORIENTATION_UNDEFINED; import static android.view.Display.TYPE_INTERNAL; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; +import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.server.wm.DisplayRotationReversionController.REVERSION_TYPE_CAMERA_COMPAT; import android.annotation.NonNull; @@ -133,6 +134,11 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp return mLastReportedOrientation; } + float getCameraCompatAspectRatio(@NonNull ActivityRecord unusedActivity) { + // This policy does not apply camera compat aspect ratio by default, only via overrides. + return MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; + } + @ScreenOrientation private synchronized int getOrientationInternal() { if (!isTreatmentEnabledForDisplay()) { @@ -271,7 +277,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) { return isTreatmentEnabledForDisplay() - && isCameraActive(activity, /* mustBeFullscreen */ true) + && isCameraRunningAndWindowingModeEligible(activity, /* mustBeFullscreen */ true) && activity.mAppCompatController.getAppCompatCameraOverrides() .shouldForceRotateForCameraCompat(); } @@ -290,7 +296,17 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp return isTreatmentEnabledForActivity(activity, /* mustBeFullscreen */ true); } - boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) { + boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) { + return isCameraRunningAndWindowingModeEligible(activity, /* mustBeFullscreen= */ true); + } + + boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord unusedActivity) { + // This policy does not apply camera compat aspect ratio by default, only via overrides. + return false; + } + + boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity, + boolean mustBeFullscreen) { // Checking windowing mode on activity level because we don't want to // apply treatment in case of activity embedding. return (!mustBeFullscreen || !activity.inMultiWindowMode()) @@ -299,7 +315,8 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity, boolean mustBeFullscreen) { - return activity != null && isCameraActive(activity, mustBeFullscreen) + return activity != null + && isCameraRunningAndWindowingModeEligible(activity, mustBeFullscreen) && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED // "locked" and "nosensor" values are often used by camera apps that can't // handle dynamic changes so we shouldn't force rotate them. @@ -428,6 +445,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp private boolean shouldOverrideMinAspectRatio(@NonNull ActivityRecord activityRecord) { return activityRecord.mAppCompatController.getAppCompatCameraOverrides() .isOverrideMinAspectRatioForCameraEnabled() - && isCameraActive(activityRecord, /* mustBeFullscreen= */ true); + && isCameraRunningAndWindowingModeEligible(activityRecord, + /* mustBeFullscreen= */ true); } } diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java index db0374e52b1a..e4fd523d5ce7 100644 --- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java +++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java @@ -815,8 +815,7 @@ class ScreenRotationAnimation { if (mDisplayContent.getRotationAnimation() == ScreenRotationAnimation.this) { // It also invokes kill(). mDisplayContent.setRotationAnimation(null); - mDisplayContent.mAppCompatCameraPolicy - .onScreenRotationAnimationFinished(); + mDisplayContent.mAppCompatCameraPolicy.onScreenRotationAnimationFinished(); } else { kill(); } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 14f034bb8445..edbc32827c1a 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -1475,7 +1475,7 @@ class Task extends TaskFragment { // The starting window should keep covering its task when a pure TaskFragment is added // because its bounds may not fill the task. final ActivityRecord top = getTopMostActivity(); - if (top != null) { + if (top != null && !top.hasFixedRotationTransform()) { top.associateStartingWindowWithTaskIfNeeded(); } } @@ -4707,8 +4707,13 @@ class Task extends TaskFragment { // If the moveToFront is a part of finishing transition, then make sure // the z-order of tasks are up-to-date. if (topActivity.mTransitionController.inFinishingTransition(topActivity)) { - Transition.assignLayers(taskDisplayArea, - taskDisplayArea.getPendingTransaction()); + final SurfaceControl.Transaction tx = + taskDisplayArea.getPendingTransaction(); + Transition.assignLayers(taskDisplayArea, tx); + final SurfaceControl leash = topActivity.getFixedRotationLeash(); + if (leash != null) { + tx.setLayer(leash, topActivity.getLastLayer()); + } } } } diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index 06d8c370b914..6d7396f1f477 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -1038,6 +1038,25 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } + private int runSetCameraCompatAspectRatio(PrintWriter pw) throws RemoteException { + final float aspectRatio; + try { + String arg = getNextArgRequired(); + aspectRatio = Float.parseFloat(arg); + } catch (NumberFormatException e) { + getErrPrintWriter().println("Error: bad aspect ratio format " + e); + return -1; + } catch (IllegalArgumentException e) { + getErrPrintWriter().println( + "Error: aspect ratio should be provided as an argument " + e); + return -1; + } + synchronized (mInternal.mGlobalLock) { + mAppCompatConfiguration.setCameraCompatAspectRatio(aspectRatio); + } + return 0; + } + private int runSetLetterboxStyle(PrintWriter pw) throws RemoteException { if (peekNextArg() == null) { getErrPrintWriter().println("Error: No arguments provided."); @@ -1129,6 +1148,9 @@ public class WindowManagerShellCommand extends ShellCommand { runSetBooleanFlag(pw, mAppCompatConfiguration::setCameraCompatRefreshCycleThroughStopEnabled); break; + case "--cameraCompatAspectRatio": + runSetCameraCompatAspectRatio(pw); + break; default: getErrPrintWriter().println( "Error: Unrecognized letterbox style option: " + arg); @@ -1220,6 +1242,9 @@ public class WindowManagerShellCommand extends ShellCommand { mAppCompatConfiguration .resetCameraCompatRefreshCycleThroughStopEnabled(); break; + case "cameraCompatAspectRatio": + mAppCompatConfiguration.resetCameraCompatAspectRatio(); + break; default: getErrPrintWriter().println( "Error: Unrecognized letterbox style option: " + arg); @@ -1330,6 +1355,7 @@ public class WindowManagerShellCommand extends ShellCommand { mAppCompatConfiguration.resetUserAppAspectRatioFullscreenEnabled(); mAppCompatConfiguration.resetCameraCompatRefreshEnabled(); mAppCompatConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled(); + mAppCompatConfiguration.resetCameraCompatAspectRatio(); } } @@ -1619,6 +1645,11 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println(" Whether activity \"refresh\" in camera compatibility treatment should"); pw.println(" happen using the \"stopped -> resumed\" cycle rather than"); pw.println(" \"paused -> resumed\" cycle."); + pw.println(" --cameraCompatAspectRatio aspectRatio"); + pw.println(" Aspect ratio of letterbox for fixed-orientation camera apps, during "); + pw.println(" freeform camera compat mode. If aspectRatio <= " + + AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); + pw.println(" it will be ignored."); pw.println(" reset-letterbox-style [aspectRatio|cornerRadius|backgroundType"); pw.println(" |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha"); pw.println(" |horizontalPositionMultiplier|verticalPositionMultiplier"); @@ -1627,7 +1658,8 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println(" |isTranslucentLetterboxingEnabled|isUserAppAspectRatioSettingsEnabled"); pw.println(" |persistentPositionMultiplierForHorizontalReachability"); pw.println(" |persistentPositionMultiplierForVerticalReachability"); - pw.println(" |defaultPositionMultiplierForVerticalReachability]"); + pw.println(" |defaultPositionMultiplierForVerticalReachability"); + pw.println(" |cameraCompatAspectRatio]"); pw.println(" Resets overrides to default values for specified properties separated"); pw.println(" by space, e.g. 'reset-letterbox-style aspectRatio cornerRadius'."); pw.println(" If no arguments provided, all values will be reset."); diff --git a/services/profcollect/src/com/android/server/profcollect/Utils.java b/services/profcollect/src/com/android/server/profcollect/Utils.java index 850880256cfa..b4e254442a19 100644 --- a/services/profcollect/src/com/android/server/profcollect/Utils.java +++ b/services/profcollect/src/com/android/server/profcollect/Utils.java @@ -19,6 +19,7 @@ package com.android.server.profcollect; import static com.android.server.profcollect.ProfcollectForwardingService.LOG_TAG; import android.os.RemoteException; +import android.os.ServiceSpecificException; import android.provider.DeviceConfig; import android.util.Log; @@ -42,7 +43,7 @@ public final class Utils { BackgroundThread.get().getThreadHandler().post(() -> { try { mIProfcollect.trace_system(eventName); - } catch (RemoteException e) { + } catch (RemoteException | ServiceSpecificException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } }); @@ -56,7 +57,7 @@ public final class Utils { BackgroundThread.get().getThreadHandler().postDelayed(() -> { try { mIProfcollect.trace_system(eventName); - } catch (RemoteException e) { + } catch (RemoteException | ServiceSpecificException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } }, delayMs); @@ -73,10 +74,10 @@ public final class Utils { mIProfcollect.trace_process(eventName, processName, durationMs); - } catch (RemoteException e) { + } catch (RemoteException | ServiceSpecificException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } }); return true; } -}
\ No newline at end of file +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index 62e5b9a3dccc..45cd5719cd86 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -31,6 +31,12 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.media.AudioAttributes.USAGE_NOTIFICATION; import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_COOLDOWN; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_FLAG_SILENT; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_GROUP_ALERT; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_NOT_MUTED; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_OTHER_INSISTENT_PLAYING; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; @@ -106,6 +112,7 @@ import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.Notificat import com.android.internal.config.sysui.TestableFlagResolver; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.InstanceIdSequenceFake; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.IntPair; import com.android.server.UiServiceTestCase; import com.android.server.lights.LightsManager; @@ -1276,7 +1283,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyNeverBeep(); assertFalse(r.isInterruptive()); assertEquals(-1, r.getLastAudiblyAlertedMs()); - assertTrue(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS, + true)).isEqualTo(MUTE_REASON_FLAG_SILENT); } @Test @@ -1295,7 +1303,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyNeverBeep(); assertFalse(r.isInterruptive()); assertEquals(-1, r.getLastAudiblyAlertedMs()); - assertTrue(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS, + true)).isEqualTo(MUTE_REASON_GROUP_ALERT); } @Test @@ -1861,7 +1870,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepLooped(); NotificationRecord interrupter = getBeepyOtherNotification(); - assertTrue(mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS)); + assertThat( + mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS, + true)).isEqualTo(MUTE_REASON_OTHER_INSISTENT_PLAYING); mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS); verifyBeep(1); @@ -1879,16 +1890,16 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { ringtoneChannel.enableVibration(true); NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); mService.addNotification(ringtoneNotification); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); verifyBeepLooped(); verifyDelayedVibrateLooped(); Mockito.reset(mVibrator); Mockito.reset(mRingtonePlayer); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); // beep wasn't reset @@ -1907,8 +1918,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { ringtoneChannel.enableVibration(true); NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); mService.addNotification(ringtoneNotification); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); verifyBeepLooped(); verifyDelayedVibrateLooped(); @@ -1930,8 +1941,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { ringtoneChannel.enableVibration(true); NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); mService.addNotification(ringtoneNotification); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); verifyBeepLooped(); verifyNeverVibrate(); @@ -1951,14 +1962,15 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); ringtoneChannel.enableVibration(true); NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); verifyVibrateLooped(); NotificationRecord interrupter = getBuzzyOtherNotification(); - assertTrue(mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(interrupter, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_OTHER_INSISTENT_PLAYING); mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS); verifyVibrate(1); @@ -2260,10 +2272,13 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS, true)) + .isEqualTo(MUTE_REASON_COOLDOWN); - verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); + verify(mAccessibilityService, times(2)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r.getLastAudiblyAlertedMs()); } @@ -2305,8 +2320,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r3.getLastAudiblyAlertedMs()); @@ -2381,9 +2397,10 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg"); // update should beep at 0% volume - mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertEquals(-1, r2.getLastAudiblyAlertedMs()); - verifyBeepVolume(0.0f); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); // Use different package for next notifications NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, @@ -2392,8 +2409,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r3.getLastAudiblyAlertedMs()); @@ -2493,8 +2511,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // Regular notification: should beep at 0% volume NotificationRecord r = getBeepyNotification(); - mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); assertEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); @@ -2525,8 +2544,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); // Set important conversation mChannel.setImportantConversation(true); @@ -2751,9 +2771,10 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { Mockito.reset(mRingtonePlayer); // next update at 0% volume - mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); assertEquals(-1, summary.getLastAudiblyAlertedMs()); - verifyBeepVolume(0.0f); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); } @@ -2823,9 +2844,10 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertEquals(-1, r2.getLastAudiblyAlertedMs()); - verifyBeepVolume(0.0f); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); // Use different package for next notifications NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, @@ -2891,6 +2913,94 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } @Test + public void testBeepVolume_politeNotif_groupAlertSummary() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); + initAttentionHelper(flagResolver); + + // child should beep at 0% volume + NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(child.isInterruptive()); + assertEquals(-1, child.getLastAudiblyAlertedMs()); + Mockito.reset(mRingtonePlayer); + + // child should beep at 0% volume + child = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(child.isInterruptive()); + assertEquals(-1, child.getLastAudiblyAlertedMs()); + Mockito.reset(mRingtonePlayer); + + // summary 100% volume (GROUP_ALERT_SUMMARY) + NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertNotEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(1.0f); + Mockito.reset(mRingtonePlayer); + + // next update at 50% volume because only summary was tracked as alerting + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertNotEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.5f); + + verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); + } + + @Test + public void testBeepVolume_politeNotif_groupAlertChildren() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); + initAttentionHelper(flagResolver); + + // summary 0% volume (GROUP_ALERT_CHILDREN) + NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(summary.isInterruptive()); + assertEquals(-1, summary.getLastAudiblyAlertedMs()); + Mockito.reset(mRingtonePlayer); + + // child should beep at 100% volume + NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + assertNotEquals(-1, child.getLastAudiblyAlertedMs()); + verifyBeepVolume(1.0f); + Mockito.reset(mRingtonePlayer); + + // child should beep at 50% volume + child = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + assertNotEquals(-1, child.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.5f); + Mockito.reset(mRingtonePlayer); + + // child should beep at 0% volume + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertTrue(child.isInterruptive()); + assertEquals(-1, child.getLastAudiblyAlertedMs()); + + verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); + } + + @Test public void testVibrationIntensity_politeNotif() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); @@ -2914,8 +3024,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { Mockito.reset(vibratorHelper); // 2nd update should buzz at 0% intensity - mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); - verify(vibratorHelper, times(1)).scale(any(), eq(0.0f)); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + verifyNeverVibrate(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); } @Test @@ -3007,10 +3118,11 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS); - verifyBeepVolume(0.0f); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); - verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); + verify(mAccessibilityService, times(2)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r.getLastAudiblyAlertedMs()); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index 84c4f620f394..5709d884d427 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -1010,7 +1010,39 @@ public class ZenModeConfigTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_MODES_UI) - public void testConfigXml_manualRule_upgradeWhenExisting() throws Exception { + public void testConfigXml_manualRuleWithoutCondition_upgradeWhenExisting() throws Exception { + // prior to modes_ui, it's possible to have a non-null manual rule that doesn't have much + // data on it because it's meant to indicate that the manual rule is on by merely existing. + ZenModeConfig config = new ZenModeConfig(); + config.manualRule = new ZenModeConfig.ZenRule(); + config.manualRule.enabled = true; + config.manualRule.pkg = "android"; + config.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + config.manualRule.conditionId = null; + config.manualRule.enabler = "test"; + + // write out entire config xml + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ZenModeConfig fromXml = readConfigXml(bais); + + + // The result should be valid and contain a manual rule; the rule should have a non-null + // ZenPolicy and a condition whose state is true. The conditionId should be default. + assertThat(fromXml.isValid()).isTrue(); + assertThat(fromXml.manualRule).isNotNull(); + assertThat(fromXml.manualRule.zenPolicy).isNotNull(); + assertThat(fromXml.manualRule.condition).isNotNull(); + assertThat(fromXml.manualRule.condition.state).isEqualTo(STATE_TRUE); + assertThat(fromXml.manualRule.conditionId).isEqualTo(Uri.EMPTY); + assertThat(fromXml.manualRule.enabler).isEqualTo("test"); + assertThat(fromXml.isManualActive()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testConfigXml_manualRuleWithCondition_upgradeWhenExisting() throws Exception { // prior to modes_ui, it's possible to have a non-null manual rule that doesn't have much // data on it because it's meant to indicate that the manual rule is on by merely existing. ZenModeConfig config = new ZenModeConfig(); @@ -1029,6 +1061,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { // The result should have a manual rule; it should have a non-null ZenPolicy and a condition // whose state is true. The conditionId and enabler data should also be preserved. + assertThat(fromXml.isValid()).isTrue(); assertThat(fromXml.manualRule).isNotNull(); assertThat(fromXml.manualRule.zenPolicy).isNotNull(); assertThat(fromXml.manualRule.condition).isNotNull(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java index 5787780cef46..4cd75d5ba074 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java @@ -308,6 +308,8 @@ public class ActivityOptionsTest { // KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE case "android.activity.launchCookie": // KEY_LAUNCH_COOKIE case "android:activity.animAbortListener": // KEY_ANIM_ABORT_LISTENER + case "android.activity.allowPassThroughOnTouchOutside": + // KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE // Existing keys break; diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java index 92205f391f32..65736cbc519f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -186,7 +186,8 @@ class AppCompatActivityRobot { void setTopActivityCameraActive(boolean enabled) { doReturn(enabled).when(getTopDisplayRotationCompatPolicy()) - .isCameraActive(eq(mActivityStack.top()), /* mustBeFullscreen= */ eq(true)); + .isCameraRunningAndWindowingModeEligible(eq(mActivityStack.top()), + /* mustBeFullscreen= */ eq(true)); } void setTopActivityEligibleForOrientationOverride(boolean enabled) { diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java index dbcef10a6be2..a8ccf95e1bb5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -26,6 +26,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; @@ -35,6 +36,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static org.junit.Assert.assertEquals; @@ -247,8 +249,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertTrue(mActivity.info .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT)); - assertFalse(mCameraCompatFreeformPolicy - .shouldApplyFreeformTreatmentForCameraCompat(mActivity)); + assertFalse(mCameraCompatFreeformPolicy.isCameraCompatForFreeformEnabledForActivity( + mActivity)); } @Test @@ -256,8 +258,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mCameraCompatFreeformPolicy - .shouldApplyFreeformTreatmentForCameraCompat(mActivity)); + assertTrue(mCameraCompatFreeformPolicy.isCameraCompatForFreeformEnabledForActivity( + mActivity)); } @Test @@ -303,6 +305,49 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); } + @Test + public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() { + configureActivity(SCREEN_ORIENTATION_FULL_USER); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity); + + assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO, + mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity), + /* delta= */ 0.001); + } + + @Test + public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + final float configAspectRatio = 1.5f; + mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity); + + assertEquals(configAspectRatio, + mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity), + /* delta= */ 0.001); + } + + + @Test + public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + final float configAspectRatio = 1.5f; + mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio); + doReturn(true).when(mActivity.mAppCompatController.getAppCompatCameraOverrides()) + .isOverrideMinAspectRatioForCameraEnabled(); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity); + + assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO, + mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity), + /* delta= */ 0.001); + } + private void configureActivity(@ScreenOrientation int activityOrientation) { configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index 8cf593fd21db..35c9e3fb3aaf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -544,39 +544,35 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test - public void testIsCameraActiveWhenCallbackInvokedNoMultiWindow_returnTrue() { + public void testShouldCameraCompatControlOrientationWhenInvokedNoMultiWindow_returnTrue() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue( - mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true)); + assertTrue(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity)); } @Test - public void testIsCameraActiveWhenCallbackNotInvokedNoMultiWindow_returnFalse() { + public void testShouldCameraCompatControlOrientationWhenNotInvokedNoMultiWindow_returnFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertFalse( - mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true)); + assertFalse(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity)); } @Test - public void testIsCameraActiveWhenCallbackNotInvokedMultiWindow_returnFalse() { + public void testShouldCameraCompatControlOrientationWhenNotInvokedMultiWindow_returnFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); when(mActivity.inMultiWindowMode()).thenReturn(true); - assertFalse( - mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true)); + assertFalse(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity)); } @Test - public void testIsCameraActiveWhenCallbackInvokedMultiWindow_returnFalse() { + public void testShouldCameraCompatControlOrientationWhenInvokedMultiWindow_returnFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); when(mActivity.inMultiWindowMode()).thenReturn(true); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse( - mDisplayRotationCompatPolicy.isCameraActive(mActivity, /* mustBeFullscreen*/ true)); + assertFalse(mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(mActivity)); } private void configureActivity(@ScreenOrientation int activityOrientation) { diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 8fa4667c3b24..adc969c40e35 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -4849,6 +4849,39 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testUniversalResizeable() { + mWm.mConstants.mIgnoreActivityOrientationRequest = true; + setUpApp(mDisplayContent); + final float maxAspect = 1.8f; + final float minAspect = 1.5f; + prepareLimitedBounds(mActivity, maxAspect, minAspect, + ActivityInfo.SCREEN_ORIENTATION_LOCKED, true /* isUnresizable */); + + assertTrue(mActivity.isUniversalResizeable()); + assertTrue(mActivity.isResizeable()); + assertFalse(mActivity.shouldCreateAppCompatDisplayInsets()); + assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mActivity.getOverrideOrientation()); + assertEquals(mActivity.getTask().getBounds(), mActivity.getBounds()); + final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivity.mAppCompatController + .getAppCompatAspectRatioPolicy(); + assertEquals(0, aspectRatioPolicy.getMaxAspectRatio(), 0 /* delta */); + assertEquals(0, aspectRatioPolicy.getMinAspectRatio(), 0 /* delta */); + + // Compat override can still take effect. + final AppCompatAspectRatioOverrides aspectRatioOverrides = + mActivity.mAppCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + doReturn(true).when(aspectRatioOverrides).shouldOverrideMinAspectRatio(); + assertEquals(minAspect, aspectRatioPolicy.getMinAspectRatio(), 0 /* delta */); + + // User override can still take effect. + doReturn(true).when(aspectRatioOverrides).shouldApplyUserMinAspectRatioOverride(); + assertFalse(mActivity.isResizeable()); + assertEquals(maxAspect, aspectRatioPolicy.getMaxAspectRatio(), 0 /* delta */); + assertNotEquals(SCREEN_ORIENTATION_UNSPECIFIED, mActivity.getOverrideOrientation()); + } + + @Test public void testClearSizeCompat_resetOverrideConfig() { final int origDensity = 480; final int newDensity = 520; diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index cca0f8c4bc70..7f25ef25c9ac 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3017,6 +3017,14 @@ interface ITelephony { boolean setSatelliteListeningTimeoutDuration(in long timeoutMillis); /** + * This API can be used by only CTS to control ingoring cellular service state event. + * + * @param enabled Whether to enable boolean config. + * @return {@code true} if the value is set successfully, {@code false} otherwise. + */ + boolean setSatelliteIgnoreCellularServiceState(in boolean enabled); + + /** * This API can be used by only CTS to update satellite pointing UI app package and class names. * * @param packageName The package name of the satellite pointing UI app. diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml index 82de070921f0..8b65efdfb5f9 100644 --- a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml +++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml index 4ffb11ab92ae..3382c1e227b3 100644 --- a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml index 0fa4d07b2eca..e941e79faea3 100644 --- a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml index 4d9fefbc7d88..4e06dca17fe2 100644 --- a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml +++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml index b879c54dcab3..0cadd68597b6 100644 --- a/tests/FlickerTests/IME/AndroidTestTemplate.xml +++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- enable AOD --> <option name="set-secure-setting" key="doze_always_on" value="1" /> <!-- prevents the phone from restarting --> diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml index 04b312a896b9..f32e8bed85ef 100644 --- a/tests/FlickerTests/Notification/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml index 8acdabc2337d..68ae4f1f7f4f 100644 --- a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml index 91ece214aad5..ec186723b4a4 100644 --- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> |