diff options
191 files changed, 9423 insertions, 3757 deletions
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index 02be051d973a..52732d3eba78 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -72,6 +72,7 @@ import android.view.IRemoteAnimationRunner; import android.view.IWindowFocusObserver; import android.view.RemoteAnimationDefinition; import android.view.RemoteAnimationAdapter; +import android.window.BackAnimationAdaptor; import android.window.IWindowOrganizerController; import android.window.BackNavigationInfo; import android.window.SplashScreenView; @@ -356,5 +357,5 @@ interface IActivityTaskManager { * @param focusObserver a remote callback to nofify shell when the focused window lost focus. */ android.window.BackNavigationInfo startBackNavigation(in boolean requestAnimation, - in IWindowFocusObserver focusObserver); + in IWindowFocusObserver focusObserver, in BackAnimationAdaptor adaptor); } diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index fe81df0b6b3b..cc303fb1f413 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -70,7 +70,7 @@ public class AppWidgetHost { private final Handler mHandler; private final int mHostId; private final Callbacks mCallbacks; - private final SparseArray<AppWidgetHostView> mViews = new SparseArray<>(); + private final SparseArray<AppWidgetHostListener> mListeners = new SparseArray<>(); private InteractionHandler mInteractionHandler; static class Callbacks extends IAppWidgetHost.Stub { @@ -171,6 +171,15 @@ public class AppWidgetHost { this(context, hostId, null, context.getMainLooper()); } + @Nullable + private AppWidgetHostListener getListener(final int appWidgetId) { + AppWidgetHostListener tempListener = null; + synchronized (mListeners) { + tempListener = mListeners.get(appWidgetId); + } + return tempListener; + } + /** * @hide */ @@ -210,11 +219,11 @@ public class AppWidgetHost { return; } final int[] idsToUpdate; - synchronized (mViews) { - int N = mViews.size(); - idsToUpdate = new int[N]; - for (int i = 0; i < N; i++) { - idsToUpdate[i] = mViews.keyAt(i); + synchronized (mListeners) { + int n = mListeners.size(); + idsToUpdate = new int[n]; + for (int i = 0; i < n; i++) { + idsToUpdate[i] = mListeners.keyAt(i); } } List<PendingHostUpdate> updates; @@ -349,14 +358,11 @@ public class AppWidgetHost { if (sService == null) { return; } - synchronized (mViews) { - mViews.remove(appWidgetId); - try { - sService.deleteAppWidgetId(mContextOpPackageName, appWidgetId); - } - catch (RemoteException e) { - throw new RuntimeException("system server dead?", e); - } + removeListener(appWidgetId); + try { + sService.deleteAppWidgetId(mContextOpPackageName, appWidgetId); + } catch (RemoteException e) { + throw new RuntimeException("system server dead?", e); } } @@ -412,9 +418,7 @@ public class AppWidgetHost { AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget); view.setInteractionHandler(mInteractionHandler); view.setAppWidget(appWidgetId, appWidget); - synchronized (mViews) { - mViews.put(appWidgetId, view); - } + addListener(appWidgetId, view); RemoteViews views; try { views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId); @@ -439,24 +443,52 @@ public class AppWidgetHost { * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk. */ protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) { - AppWidgetHostView v; + AppWidgetHostListener v = getListener(appWidgetId); // Convert complex to dp -- we are getting the AppWidgetProviderInfo from the // AppWidgetService, which doesn't have our context, hence we need to do the // conversion here. appWidget.updateDimensions(mDisplayMetrics); - synchronized (mViews) { - v = mViews.get(appWidgetId); - } if (v != null) { - v.resetAppWidget(appWidget); + v.onUpdateProviderInfo(appWidget); } } + /** + * This interface specifies the actions to be performed on the app widget based on the calls + * from the service + * + * @hide + */ + public interface AppWidgetHostListener { + + /** + * This function is called when the service want to reset the app widget provider info + * @param appWidget The new app widget provider info + * + * @hide + */ + void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo appWidget); + + /** + * This function is called when the RemoteViews of the app widget is updated + * @param views The new RemoteViews to be set for the app widget + * + * @hide + */ + void updateAppWidget(@Nullable RemoteViews views); + + /** + * This function is called when the view ID is changed for the app widget + * @param viewId The new view ID to be be set for the widget + * + * @hide + */ + void onViewDataChanged(int viewId); + } + void dispatchOnAppWidgetRemoved(int appWidgetId) { - synchronized (mViews) { - mViews.remove(appWidgetId); - } + removeListener(appWidgetId); onAppWidgetRemoved(appWidgetId); } @@ -476,23 +508,43 @@ public class AppWidgetHost { // Does nothing } - void updateAppWidgetView(int appWidgetId, RemoteViews views) { - AppWidgetHostView v; - synchronized (mViews) { - v = mViews.get(appWidgetId); + /** + * Create an AppWidgetHostListener for the given widget. + * The AppWidgetHost retains a pointer to the newly-created listener. + * @param appWidgetId The ID of the app widget for which to add the listener + * @param listener The listener interface that deals with actions towards the widget view + * + * @hide + */ + public void addListener(int appWidgetId, @NonNull AppWidgetHostListener listener) { + synchronized (mListeners) { + mListeners.put(appWidgetId, listener); + } + } + + /** + * Delete the listener for the given widget + * @param appWidgetId The ID of the app widget for which the listener is to be deleted + + * @hide + */ + public void removeListener(int appWidgetId) { + synchronized (mListeners) { + mListeners.remove(appWidgetId); } + } + + void updateAppWidgetView(int appWidgetId, RemoteViews views) { + AppWidgetHostListener v = getListener(appWidgetId); if (v != null) { v.updateAppWidget(views); } } void viewDataChanged(int appWidgetId, int viewId) { - AppWidgetHostView v; - synchronized (mViews) { - v = mViews.get(appWidgetId); - } + AppWidgetHostListener v = getListener(appWidgetId); if (v != null) { - v.viewDataChanged(viewId); + v.onViewDataChanged(viewId); } } @@ -500,8 +552,8 @@ public class AppWidgetHost { * Clear the list of Views that have been created by this AppWidgetHost. */ protected void clearViews() { - synchronized (mViews) { - mViews.clear(); + synchronized (mListeners) { + mListeners.clear(); } } } diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index e3bca9c9aadb..fe10b7f8b3f4 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -66,7 +66,7 @@ import java.util.concurrent.Executor; * between updates, and will try recycling old views for each incoming * {@link RemoteViews}. */ -public class AppWidgetHostView extends FrameLayout { +public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppWidgetHostListener { static final String TAG = "AppWidgetHostView"; private static final String KEY_JAILED_ARRAY = "jail"; @@ -492,8 +492,11 @@ public class AppWidgetHostView extends FrameLayout { /** * Update the AppWidgetProviderInfo for this view, and reset it to the * initial layout. + * + * @hide */ - void resetAppWidget(AppWidgetProviderInfo info) { + @Override + public void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo info) { setAppWidget(mAppWidgetId, info); mViewMode = VIEW_MODE_NOINIT; updateAppWidget(null); @@ -503,6 +506,7 @@ public class AppWidgetHostView extends FrameLayout { * Process a set of {@link RemoteViews} coming in as an update from the * AppWidget provider. Will animate into these new views as needed */ + @Override public void updateAppWidget(RemoteViews remoteViews) { mLastInflatedRemoteViews = remoteViews; applyRemoteViews(remoteViews, true); @@ -693,8 +697,11 @@ public class AppWidgetHostView extends FrameLayout { /** * Process data-changed notifications for the specified view in the specified * set of {@link RemoteViews} views. + * + * @hide */ - void viewDataChanged(int viewId) { + @Override + public void onViewDataChanged(int viewId) { View v = findViewById(viewId); if ((v != null) && (v instanceof AdapterView<?>)) { AdapterView<?> adapterView = (AdapterView<?>) v; diff --git a/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java b/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java index be573721936b..d6f191e31182 100644 --- a/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java +++ b/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java @@ -37,6 +37,7 @@ import android.net.ipsec.ike.IkeSessionParams.IkeAuthPskConfig; import android.net.ipsec.ike.IkeSessionParams.IkeConfigRequest; import android.os.PersistableBundle; import android.util.ArraySet; +import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.vcn.util.PersistableBundleUtils; @@ -58,6 +59,8 @@ import java.util.Set; */ @VisibleForTesting(visibility = Visibility.PRIVATE) public final class IkeSessionParamsUtils { + private static final String TAG = IkeSessionParamsUtils.class.getSimpleName(); + private static final String SERVER_HOST_NAME_KEY = "SERVER_HOST_NAME_KEY"; private static final String SA_PROPOSALS_KEY = "SA_PROPOSALS_KEY"; private static final String LOCAL_ID_KEY = "LOCAL_ID_KEY"; @@ -72,6 +75,13 @@ public final class IkeSessionParamsUtils { private static final String NATT_KEEPALIVE_DELAY_SEC_KEY = "NATT_KEEPALIVE_DELAY_SEC_KEY"; private static final String IKE_OPTIONS_KEY = "IKE_OPTIONS_KEY"; + // TODO: b/243181760 Use the IKE API when they are exposed + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static final int IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION = 6; + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static final int IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES = 7; + private static final Set<Integer> IKE_OPTIONS = new ArraySet<>(); static { @@ -80,6 +90,26 @@ public final class IkeSessionParamsUtils { IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_MOBIKE); IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_FORCE_PORT_4500); IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_INITIAL_CONTACT); + IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY); + IKE_OPTIONS.add(IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION); + IKE_OPTIONS.add(IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES); + } + + /** + * Check if an IKE option is supported in the IPsec module installed on the device + * + * <p>This method ensures caller to safely access options that are added between dessert + * releases. + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static boolean isIkeOptionValid(int option) { + try { + new IkeSessionParams.Builder().addIkeOption(option); + return true; + } catch (IllegalArgumentException e) { + Log.d(TAG, "Option not supported; discarding: " + option); + return false; + } } /** Serializes an IkeSessionParams to a PersistableBundle. */ @@ -130,7 +160,7 @@ public final class IkeSessionParamsUtils { // IKE_OPTION is defined in IKE module and added in the IkeSessionParams final List<Integer> enabledIkeOptions = new ArrayList<>(); for (int option : IKE_OPTIONS) { - if (params.hasIkeOption(option)) { + if (isIkeOptionValid(option) && params.hasIkeOption(option)) { enabledIkeOptions.add(option); } } @@ -205,12 +235,16 @@ public final class IkeSessionParamsUtils { // Clear IKE Options that are by default enabled for (int option : IKE_OPTIONS) { - builder.removeIkeOption(option); + if (isIkeOptionValid(option)) { + builder.removeIkeOption(option); + } } final int[] optionArray = in.getIntArray(IKE_OPTIONS_KEY); for (int option : optionArray) { - builder.addIkeOption(option); + if (isIkeOptionValid(option)) { + builder.addIkeOption(option); + } } return builder.build(); diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 801d34d9d884..06c35b5bec5d 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -956,7 +956,16 @@ public abstract class BatteryStats implements Parcelable { public static final int NUM_WIFI_BATCHED_SCAN_BINS = 5; - public static final int NUM_USER_ACTIVITY_TYPES = PowerManager.USER_ACTIVITY_EVENT_MAX + 1; + /** + * Note that these must match the constants in android.os.PowerManager. + * Also, if the user activity types change, the BatteryStatsImpl.VERSION must + * also be bumped. + */ + static final String[] USER_ACTIVITY_TYPES = { + "other", "button", "touch", "accessibility", "attention" + }; + + public static final int NUM_USER_ACTIVITY_TYPES = USER_ACTIVITY_TYPES.length; public abstract void noteUserActivityLocked(int type); public abstract boolean hasUserActivity(); @@ -6168,7 +6177,7 @@ public abstract class BatteryStats implements Parcelable { } sb.append(val); sb.append(" "); - sb.append(PowerManager.userActivityEventToString(i)); + sb.append(Uid.USER_ACTIVITY_TYPES[i]); } } if (hasData) { diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 8a203e07ae6d..13ca2c34b27e 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -345,44 +345,6 @@ public final class PowerManager { public static final int USER_ACTIVITY_EVENT_DEVICE_STATE = 6; /** - * @hide - */ - public static final int USER_ACTIVITY_EVENT_MAX = USER_ACTIVITY_EVENT_DEVICE_STATE; - - /** - * @hide - */ - @IntDef(prefix = { "USER_ACTIVITY_EVENT_" }, value = { - USER_ACTIVITY_EVENT_OTHER, - USER_ACTIVITY_EVENT_BUTTON, - USER_ACTIVITY_EVENT_TOUCH, - USER_ACTIVITY_EVENT_ACCESSIBILITY, - USER_ACTIVITY_EVENT_ATTENTION, - USER_ACTIVITY_EVENT_FACE_DOWN, - USER_ACTIVITY_EVENT_DEVICE_STATE, - }) - @Retention(RetentionPolicy.SOURCE) - public @interface UserActivityEvent{} - - /** - * - * Convert the user activity event to a string for debugging purposes. - * @hide - */ - public static String userActivityEventToString(@UserActivityEvent int userActivityEvent) { - switch (userActivityEvent) { - case USER_ACTIVITY_EVENT_OTHER: return "other"; - case USER_ACTIVITY_EVENT_BUTTON: return "button"; - case USER_ACTIVITY_EVENT_TOUCH: return "touch"; - case USER_ACTIVITY_EVENT_ACCESSIBILITY: return "accessibility"; - case USER_ACTIVITY_EVENT_ATTENTION: return "attention"; - case USER_ACTIVITY_EVENT_FACE_DOWN: return "faceDown"; - case USER_ACTIVITY_EVENT_DEVICE_STATE: return "deviceState"; - default: return Integer.toString(userActivityEvent); - } - } - - /** * User activity flag: If already dimmed, extend the dim timeout * but do not brighten. This flag is useful for keeping the screen on * a little longer without causing a visible change such as when diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java index 91042bfa3402..a9c2ad1ce915 100644 --- a/core/java/android/service/notification/NotificationAssistantService.java +++ b/core/java/android/service/notification/NotificationAssistantService.java @@ -91,6 +91,22 @@ public abstract class NotificationAssistantService extends NotificationListenerS = "android.service.notification.NotificationAssistantService"; /** + * Activity Action: Show notification assistant detail setting page in NAS app. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + * @hide + */ + @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS = + "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS"; + + + /** * Data type: int, the feedback rating score provided by user. The score can be any integer * value depends on the experimental and feedback UX design. */ diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 0ef23bbfc8c9..bb26c46142d2 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -413,11 +413,6 @@ interface IWindowManager boolean hasNavigationBar(int displayId); /** - * Get the position of the nav bar - */ - int getNavBarPosition(int displayId); - - /** * Lock the device immediately with the specified options (can be null). */ @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index d63c25a09382..5236fe772a7b 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -176,7 +176,9 @@ public class InsetsSourceConsumer { // If we have a new leash, make sure visibility is up-to-date, even though we // didn't want to run an animation above. - applyRequestedVisibilityToControl(); + if (mController.getAnimationType(control.getType()) == ANIMATION_TYPE_NONE) { + applyRequestedVisibilityToControl(); + } // Remove the surface that owned by last control when it lost. if (!requestedVisible && lastControl == null) { diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index c198098cb6ff..c102ad3a3ace 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -349,6 +349,20 @@ public class InsetsState implements Parcelable { return insets; } + // TODO: Remove this once the task bar is treated as navigation bar. + public Insets calculateInsetsWithInternalTypes(Rect frame, @InternalInsetsType int[] types, + boolean ignoreVisibility) { + Insets insets = Insets.NONE; + for (int i = types.length - 1; i >= 0; i--) { + InsetsSource source = mSources[types[i]]; + if (source == null) { + continue; + } + insets = Insets.max(source.calculateInsets(frame, ignoreVisibility), insets); + } + return insets; + } + public Insets calculateInsets(Rect frame, @InsetsType int types, InsetsVisibilities overrideVisibilities) { Insets insets = Insets.NONE; diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java index 57a0330e3c18..5ed9d2f90a72 100644 --- a/core/java/android/view/WindowLayout.java +++ b/core/java/android/view/WindowLayout.java @@ -118,11 +118,11 @@ public class WindowLayout { } if (cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES) { if (displayFrame.width() < displayFrame.height()) { - displayCutoutSafeExceptMaybeBars.top = Integer.MIN_VALUE; - displayCutoutSafeExceptMaybeBars.bottom = Integer.MAX_VALUE; + displayCutoutSafeExceptMaybeBars.top = MIN_Y; + displayCutoutSafeExceptMaybeBars.bottom = MAX_Y; } else { - displayCutoutSafeExceptMaybeBars.left = Integer.MIN_VALUE; - displayCutoutSafeExceptMaybeBars.right = Integer.MAX_VALUE; + displayCutoutSafeExceptMaybeBars.left = MIN_X; + displayCutoutSafeExceptMaybeBars.right = MAX_X; } } final boolean layoutInsetDecor = (attrs.flags & FLAG_LAYOUT_INSET_DECOR) != 0; @@ -132,23 +132,23 @@ public class WindowLayout { final Insets systemBarsInsets = state.calculateInsets( displayFrame, WindowInsets.Type.systemBars(), requestedVisibilities); if (systemBarsInsets.left > 0) { - displayCutoutSafeExceptMaybeBars.left = Integer.MIN_VALUE; + displayCutoutSafeExceptMaybeBars.left = MIN_X; } if (systemBarsInsets.top > 0) { - displayCutoutSafeExceptMaybeBars.top = Integer.MIN_VALUE; + displayCutoutSafeExceptMaybeBars.top = MIN_Y; } if (systemBarsInsets.right > 0) { - displayCutoutSafeExceptMaybeBars.right = Integer.MAX_VALUE; + displayCutoutSafeExceptMaybeBars.right = MAX_X; } if (systemBarsInsets.bottom > 0) { - displayCutoutSafeExceptMaybeBars.bottom = Integer.MAX_VALUE; + displayCutoutSafeExceptMaybeBars.bottom = MAX_Y; } } if (type == TYPE_INPUT_METHOD) { final InsetsSource navSource = state.peekSource(ITYPE_NAVIGATION_BAR); if (navSource != null && navSource.calculateInsets(displayFrame, true).bottom > 0) { // The IME can always extend under the bottom cutout if the navbar is there. - displayCutoutSafeExceptMaybeBars.bottom = Integer.MAX_VALUE; + displayCutoutSafeExceptMaybeBars.bottom = MAX_Y; } } final boolean attachedInParent = attachedWindowFrame != null && !layoutInScreen; diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 2268bef2c1d9..450bb1e77ec8 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -4877,20 +4877,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Set the line break style for text wrapping. + * Sets the line-break style for text wrapping. * - * The line break style to indicates the line break strategies can be used when - * calculating the text wrapping. The line break style affects rule-based breaking. It - * specifies the strictness of line-breaking rules. - * There are several types for the line break style: - * {@link LineBreakConfig#LINE_BREAK_STYLE_LOOSE}, - * {@link LineBreakConfig#LINE_BREAK_STYLE_NORMAL} and - * {@link LineBreakConfig#LINE_BREAK_STYLE_STRICT}. The default values of the line break style - * is {@link LineBreakConfig#LINE_BREAK_STYLE_NONE}, indicating no breaking rule is specified. - * See <a href="https://www.w3.org/TR/css-text-3/#line-break-property"> - * the line-break property</a> + * <p>Line-break style specifies the line-break strategies that can be used + * for text wrapping. The line-break style affects rule-based line breaking + * by specifying the strictness of line-breaking rules. * - * @param lineBreakStyle the line break style for the text. + * <p>The following are types of line-break styles: + * <ul> + * <li>{@link LineBreakConfig#LINE_BREAK_STYLE_LOOSE} + * <li>{@link LineBreakConfig#LINE_BREAK_STYLE_NORMAL} + * <li>{@link LineBreakConfig#LINE_BREAK_STYLE_STRICT} + * </ul> + * + * <p>The default line-break style is + * {@link LineBreakConfig#LINE_BREAK_STYLE_NONE}, which specifies that no + * line-breaking rules are used. + * + * <p>See the + * <a href="https://www.w3.org/TR/css-text-3/#line-break-property" class="external"> + * line-break property</a> for more information. + * + * @param lineBreakStyle The line-break style for the text. */ public void setLineBreakStyle(@LineBreakConfig.LineBreakStyle int lineBreakStyle) { if (mLineBreakStyle != lineBreakStyle) { @@ -4904,17 +4912,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Set the line break word style for text wrapping. + * Sets the line-break word style for text wrapping. + * + * <p>The line-break word style affects dictionary-based line breaking by + * providing phrase-based line-breaking opportunities. Use + * {@link LineBreakConfig#LINE_BREAK_WORD_STYLE_PHRASE} to specify + * phrase-based line breaking. + * + * <p>The default line-break word style is + * {@link LineBreakConfig#LINE_BREAK_WORD_STYLE_NONE}, which specifies that + * no line-breaking word style is used. * - * The line break word style affects dictionary-based breaking and provide phrase-based - * breaking opportunities. The type for the line break word style is - * {@link LineBreakConfig#LINE_BREAK_WORD_STYLE_PHRASE}. The default values of the line break - * word style is {@link LineBreakConfig#LINE_BREAK_WORD_STYLE_NONE}, indicating no breaking rule - * is specified. - * See <a href="https://www.w3.org/TR/css-text-3/#word-break-property"> - * the word-break property</a> + * <p>See the + * <a href="https://www.w3.org/TR/css-text-3/#word-break-property" class="external"> + * word-break property</a> for more information. * - * @param lineBreakWordStyle the line break word style for the tet + * @param lineBreakWordStyle The line-break word style for the text. */ public void setLineBreakWordStyle(@LineBreakConfig.LineBreakWordStyle int lineBreakWordStyle) { if (mLineBreakWordStyle != lineBreakWordStyle) { @@ -4928,18 +4941,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Get the current line break style for text wrapping. + * Gets the current line-break style for text wrapping. * - * @return the current line break style to be used for text wrapping. + * @return The line-break style to be used for text wrapping. */ public @LineBreakConfig.LineBreakStyle int getLineBreakStyle() { return mLineBreakStyle; } /** - * Get the current line word break style for text wrapping. + * Gets the current line-break word style for text wrapping. * - * @return the current line break word style to be used for text wrapping. + * @return The line-break word style to be used for text wrapping. */ public @LineBreakConfig.LineBreakWordStyle int getLineBreakWordStyle() { return mLineBreakWordStyle; diff --git a/core/java/android/window/BackAnimationAdaptor.aidl b/core/java/android/window/BackAnimationAdaptor.aidl new file mode 100644 index 000000000000..1082d0ace1ae --- /dev/null +++ b/core/java/android/window/BackAnimationAdaptor.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +/** + * @hide + */ +parcelable BackAnimationAdaptor;
\ No newline at end of file diff --git a/core/java/android/window/BackAnimationAdaptor.java b/core/java/android/window/BackAnimationAdaptor.java new file mode 100644 index 000000000000..cf82046e7e55 --- /dev/null +++ b/core/java/android/window/BackAnimationAdaptor.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Object that describes how to run a remote back animation. + * + * @hide + */ +public class BackAnimationAdaptor implements Parcelable { + + private final IBackAnimationRunner mRunner; + @BackNavigationInfo.BackTargetType + private final int mSupportType; + + public BackAnimationAdaptor(IBackAnimationRunner runner, int supportType) { + mRunner = runner; + mSupportType = supportType; + } + + public BackAnimationAdaptor(Parcel in) { + mRunner = IBackAnimationRunner.Stub.asInterface(in.readStrongBinder()); + mSupportType = in.readInt(); + } + + public IBackAnimationRunner getRunner() { + return mRunner; + } + + @BackNavigationInfo.BackTargetType public int getSupportType() { + return mSupportType; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongInterface(mRunner); + dest.writeInt(mSupportType); + } + + public static final @android.annotation.NonNull Creator<BackAnimationAdaptor> CREATOR = + new Creator<BackAnimationAdaptor>() { + public BackAnimationAdaptor createFromParcel(Parcel in) { + return new BackAnimationAdaptor(in); + } + + public BackAnimationAdaptor[] newArray(int size) { + return new BackAnimationAdaptor[size]; + } + }; +} diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java index dd4901417671..941511ec33be 100644 --- a/core/java/android/window/BackNavigationInfo.java +++ b/core/java/android/window/BackNavigationInfo.java @@ -101,6 +101,8 @@ public final class BackNavigationInfo implements Parcelable { @Nullable private final IOnBackInvokedCallback mOnBackInvokedCallback; + private final boolean mIsPrepareRemoteAnimation; + /** * Create a new {@link BackNavigationInfo} instance. * @@ -117,6 +119,9 @@ public final class BackNavigationInfo implements Parcelable { * @param onBackNavigationDone The callback to be called once the client is done with the * back preview. * @param onBackInvokedCallback The back callback registered by the current top level window. + * @param isPrepareRemoteAnimation Return whether the core is preparing a back gesture + * animation, if true, the caller of startBackNavigation should + * be expected to receive an animation start callback. */ private BackNavigationInfo(@BackTargetType int type, @Nullable RemoteAnimationTarget departingAnimationTarget, @@ -124,7 +129,8 @@ public final class BackNavigationInfo implements Parcelable { @Nullable HardwareBuffer screenshotBuffer, @Nullable WindowConfiguration taskWindowConfiguration, @Nullable RemoteCallback onBackNavigationDone, - @Nullable IOnBackInvokedCallback onBackInvokedCallback) { + @Nullable IOnBackInvokedCallback onBackInvokedCallback, + boolean isPrepareRemoteAnimation) { mType = type; mDepartingAnimationTarget = departingAnimationTarget; mScreenshotSurface = screenshotSurface; @@ -132,6 +138,7 @@ public final class BackNavigationInfo implements Parcelable { mTaskWindowConfiguration = taskWindowConfiguration; mOnBackNavigationDone = onBackNavigationDone; mOnBackInvokedCallback = onBackInvokedCallback; + mIsPrepareRemoteAnimation = isPrepareRemoteAnimation; } private BackNavigationInfo(@NonNull Parcel in) { @@ -142,6 +149,7 @@ public final class BackNavigationInfo implements Parcelable { mTaskWindowConfiguration = in.readTypedObject(WindowConfiguration.CREATOR); mOnBackNavigationDone = in.readTypedObject(RemoteCallback.CREATOR); mOnBackInvokedCallback = IOnBackInvokedCallback.Stub.asInterface(in.readStrongBinder()); + mIsPrepareRemoteAnimation = in.readBoolean(); } @Override @@ -153,6 +161,7 @@ public final class BackNavigationInfo implements Parcelable { dest.writeTypedObject(mTaskWindowConfiguration, flags); dest.writeTypedObject(mOnBackNavigationDone, flags); dest.writeStrongInterface(mOnBackInvokedCallback); + dest.writeBoolean(mIsPrepareRemoteAnimation); } /** @@ -221,6 +230,10 @@ public final class BackNavigationInfo implements Parcelable { return mOnBackInvokedCallback; } + public boolean isPrepareRemoteAnimation() { + return mIsPrepareRemoteAnimation; + } + /** * Callback to be called when the back preview is finished in order to notify the server that * it can clean up the resources created for the animation. @@ -306,6 +319,8 @@ public final class BackNavigationInfo implements Parcelable { @Nullable private IOnBackInvokedCallback mOnBackInvokedCallback = null; + private boolean mPrepareAnimation; + /** * @see BackNavigationInfo#getType() */ @@ -366,12 +381,20 @@ public final class BackNavigationInfo implements Parcelable { } /** + * @param prepareAnimation Whether core prepare animation for shell. + */ + public Builder setPrepareAnimation(boolean prepareAnimation) { + mPrepareAnimation = prepareAnimation; + return this; + } + + /** * Builds and returns an instance of {@link BackNavigationInfo} */ public BackNavigationInfo build() { return new BackNavigationInfo(mType, mDepartingAnimationTarget, mScreenshotSurface, mScreenshotBuffer, mTaskWindowConfiguration, mOnBackNavigationDone, - mOnBackInvokedCallback); + mOnBackInvokedCallback, mPrepareAnimation); } } } diff --git a/core/java/android/window/IBackAnimationRunner.aidl b/core/java/android/window/IBackAnimationRunner.aidl new file mode 100644 index 000000000000..ca04b9d358b8 --- /dev/null +++ b/core/java/android/window/IBackAnimationRunner.aidl @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +import android.view.RemoteAnimationTarget; +import android.window.IBackNaviAnimationController; + +/** + * Interface that is used to callback from window manager to the process that runs a back gesture + * animation to start or cancel it. + * + * {@hide} + */ +oneway interface IBackAnimationRunner { + + /** + * Called when the system needs to cancel the current animation. This can be due to the + * wallpaper not drawing in time, or the handler not finishing the animation within a predefined + * amount of time. + * + */ + void onAnimationCancelled() = 1; + + /** + * Called when the system is ready for the handler to start animating all the visible tasks. + * + */ + void onAnimationStart(in IBackNaviAnimationController controller, in int type, + in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers, + in RemoteAnimationTarget[] nonApps) = 2; +} diff --git a/core/java/android/window/IBackNaviAnimationController.aidl b/core/java/android/window/IBackNaviAnimationController.aidl new file mode 100644 index 000000000000..bba223ea339b --- /dev/null +++ b/core/java/android/window/IBackNaviAnimationController.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.window; + +/** + * Interface to be invoked by the controlling process when a back animation has finished. + * + * @param trigger Whether the back gesture has passed the triggering threshold. + * {@hide} + */ +interface IBackNaviAnimationController { + void finish(in boolean triggerBack); +} diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index b263b08d6abc..dc1f612534e2 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -119,6 +119,12 @@ public final class TransitionInfo implements Parcelable { /** The container is going to show IME on its task after the transition. */ public static final int FLAG_WILL_IME_SHOWN = 1 << 11; + /** The container attaches owner profile thumbnail for cross profile animation. */ + public static final int FLAG_CROSS_PROFILE_OWNER_THUMBNAIL = 1 << 12; + + /** The container attaches work profile thumbnail for cross profile animation. */ + public static final int FLAG_CROSS_PROFILE_WORK_THUMBNAIL = 1 << 13; + /** @hide */ @IntDef(prefix = { "FLAG_" }, value = { FLAG_NONE, @@ -508,6 +514,11 @@ public final class TransitionInfo implements Parcelable { return mFlags; } + /** Whether the given change flags has included in this change. */ + public boolean hasFlags(@ChangeFlags int flags) { + return (mFlags & flags) != 0; + } + /** * @return the bounds of the container before the change. It may be empty if the container * is coming into existence. diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index e9e437fda8f2..0e1ed7bd0550 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -1283,9 +1283,6 @@ public class ResolverActivity extends Activity implements } if (target != null) { - if (intent != null && isLaunchingTargetInOtherProfile()) { - prepareIntentForCrossProfileLaunch(intent); - } safelyStartActivity(target); // Rely on the ActivityManager to pop up a dialog regarding app suspension @@ -1298,15 +1295,6 @@ public class ResolverActivity extends Activity implements return true; } - private void prepareIntentForCrossProfileLaunch(Intent intent) { - intent.fixUris(UserHandle.myUserId()); - } - - private boolean isLaunchingTargetInOtherProfile() { - return mMultiProfilePagerAdapter.getCurrentUserHandle().getIdentifier() - != UserHandle.myUserId(); - } - @VisibleForTesting public void safelyStartActivity(TargetInfo cti) { // We're dispatching intents that might be coming from legacy apps, so @@ -1513,9 +1501,6 @@ public class ResolverActivity extends Activity implements findViewById(R.id.button_open).setOnClickListener(v -> { Intent intent = otherProfileResolveInfo.getResolvedIntent(); - if (intent != null) { - prepareIntentForCrossProfileLaunch(intent); - } safelyStartActivityAsUser(otherProfileResolveInfo, inactiveAdapter.mResolverListController.getUserHandle()); finish(); diff --git a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java index 96cc5e1bd7d2..5f4a9cd5141e 100644 --- a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java +++ b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java @@ -172,12 +172,14 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable { @Override public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { + prepareIntentForCrossProfileLaunch(mResolvedIntent, userId); activity.startActivityAsCaller(mResolvedIntent, options, false, userId); return true; } @Override public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { + prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier()); activity.startActivityAsUser(mResolvedIntent, options, user); return false; } @@ -222,6 +224,13 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable { } }; + private static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) { + final int currentUserId = UserHandle.myUserId(); + if (targetUserId != currentUserId) { + intent.fixUris(currentUserId); + } + } + private DisplayResolveInfo(Parcel in) { mDisplayLabel = in.readCharSequence(); mExtendedInfo = in.readCharSequence(); diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index 7c08a7bbc826..6c689ff2b725 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -548,6 +548,12 @@ public final class SystemUiDeviceConfigFlags { */ public static final String TASK_MANAGER_SHOW_FOOTER_DOT = "task_manager_show_footer_dot"; + /** + * (boolean) Whether the task manager should show a stop button if the app is allowlisted + * by the user. + */ + public static final String TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = + "show_stop_button_for_user_allowlisted_apps"; /** * (boolean) Whether the clipboard overlay is enabled. diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 06473b5ee8de..98d4c5976adc 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -6144,8 +6144,7 @@ public class BatteryStatsImpl extends BatteryStats { } @GuardedBy("this") - public void noteUserActivityLocked(int uid, @PowerManager.UserActivityEvent int event, - long elapsedRealtimeMs, long uptimeMs) { + public void noteUserActivityLocked(int uid, int event, long elapsedRealtimeMs, long uptimeMs) { if (mOnBatteryInternal) { uid = mapUid(uid); getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs).noteUserActivityLocked(event); @@ -9957,14 +9956,14 @@ public class BatteryStatsImpl extends BatteryStats { } @Override - public void noteUserActivityLocked(@PowerManager.UserActivityEvent int event) { + public void noteUserActivityLocked(int type) { if (mUserActivityCounters == null) { initUserActivityLocked(); } - if (event >= 0 && event < NUM_USER_ACTIVITY_TYPES) { - mUserActivityCounters[event].stepAtomic(); + if (type >= 0 && type < NUM_USER_ACTIVITY_TYPES) { + mUserActivityCounters[type].stepAtomic(); } else { - Slog.w(TAG, "Unknown user activity event " + event + " was specified.", + Slog.w(TAG, "Unknown user activity type " + type + " was specified.", new Throwable()); } } diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 515ea5006667..004b5f6a3ea4 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -5484,22 +5484,22 @@ ignores some hyphen character related typographic features, e.g. kerning. --> <enum name="fullFast" value="4" /> </attr> - <!-- Indicates the line break strategies can be used when calculating the text wrapping. --> + <!-- Specifies the line-break strategies for text wrapping. --> <attr name="lineBreakStyle"> - <!-- No line break style specific. --> + <!-- No line-break rules are used for line breaking. --> <enum name="none" value="0" /> - <!-- Use the least restrictive rule for line-breaking. --> + <!-- The least restrictive line-break rules are used for line breaking. --> <enum name="loose" value="1" /> - <!-- Indicates breaking text with the most comment set of line-breaking rules. --> + <!-- The most common line-break rules are used for line breaking. --> <enum name="normal" value="2" /> - <!-- Indicates breaking text with the most strictest line-breaking rules. --> + <!-- The most strict line-break rules are used for line breaking. --> <enum name="strict" value="3" /> </attr> - <!-- Specify the phrase-based line break can be used when calculating the text wrapping.--> + <!-- Specifies the line-break word strategies for text wrapping.--> <attr name="lineBreakWordStyle"> - <!-- No line break word style specific. --> + <!-- No line-break word style is used for line breaking. --> <enum name="none" value="0" /> - <!-- Specify the phrase based breaking. --> + <!-- Line breaking is based on phrases, which results in text wrapping only on meaningful words. --> <enum name="phrase" value="1" /> </attr> <!-- Specify the type of auto-size. Note that this feature is not supported by EditText, diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index eef2ff627c79..d441b31a0996 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1562,8 +1562,7 @@ <bool name="config_enableIdleScreenBrightnessMode">false</bool> <!-- Array of desired screen brightness in nits corresponding to the lux values - in the config_autoBrightnessLevels array. As with config_screenBrightnessMinimumNits and - config_screenBrightnessMaximumNits, the display brightness is defined as the measured + in the config_autoBrightnessLevels array. The display brightness is defined as the measured brightness of an all-white image. If this is defined then: @@ -1584,7 +1583,7 @@ <array name="config_autoBrightnessDisplayValuesNitsIdle"> </array> - <!-- Array of output values for button backlight corresponding to the luX values + <!-- Array of output values for button backlight corresponding to the lux values in the config_autoBrightnessLevels array. This array should have size one greater than the size of the config_autoBrightnessLevels array. The brightness values must be between 0 and 255 and be non-decreasing. diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java index 2054b4fe9a35..8cf118c4b79a 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java @@ -18,8 +18,10 @@ package android.view; import static android.view.InsetsController.ANIMATION_TYPE_NONE; import static android.view.InsetsController.ANIMATION_TYPE_USER; +import static android.view.InsetsSourceConsumer.ShowResult.SHOW_IMMEDIATELY; import static android.view.InsetsState.ITYPE_IME; import static android.view.InsetsState.ITYPE_STATUS_BAR; +import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.statusBars; import static junit.framework.Assert.assertEquals; @@ -28,6 +30,7 @@ import static junit.framework.TestCase.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -75,6 +78,7 @@ public class InsetsSourceConsumerTest { private boolean mRemoveSurfaceCalled = false; private InsetsController mController; private InsetsState mState; + private ViewRootImpl mViewRoot; @Before public void setup() { @@ -86,10 +90,9 @@ public class InsetsSourceConsumerTest { instrumentation.runOnMainSync(() -> { final Context context = instrumentation.getTargetContext(); // cannot mock ViewRootImpl since it's final. - final ViewRootImpl viewRootImpl = new ViewRootImpl(context, - context.getDisplayNoVerify()); + mViewRoot = new ViewRootImpl(context, context.getDisplayNoVerify()); try { - viewRootImpl.setView(new TextView(context), new LayoutParams(), null); + mViewRoot.setView(new TextView(context), new LayoutParams(), null); } catch (BadTokenException e) { // activity isn't running, lets ignore BadTokenException. } @@ -97,7 +100,7 @@ public class InsetsSourceConsumerTest { mSpyInsetsSource = Mockito.spy(new InsetsSource(ITYPE_STATUS_BAR)); mState.addSource(mSpyInsetsSource); - mController = new InsetsController(new ViewRootInsetsControllerHost(viewRootImpl)); + mController = new InsetsController(new ViewRootInsetsControllerHost(mViewRoot)); mConsumer = new InsetsSourceConsumer(ITYPE_STATUS_BAR, mState, () -> mMockTransaction, mController) { @Override @@ -207,4 +210,40 @@ public class InsetsSourceConsumerTest { }); } + + @Test + public void testWontUpdateImeLeashVisibility_whenAnimation() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + InsetsState state = new InsetsState(); + ViewRootInsetsControllerHost host = new ViewRootInsetsControllerHost(mViewRoot); + InsetsController insetsController = new InsetsController(host, (controller, type) -> { + if (type == ITYPE_IME) { + return new InsetsSourceConsumer(ITYPE_IME, state, + () -> mMockTransaction, controller) { + @Override + public int requestShow(boolean fromController) { + return SHOW_IMMEDIATELY; + } + }; + } + return new InsetsSourceConsumer(type, controller.getState(), Transaction::new, + controller); + }, host.getHandler()); + InsetsSourceConsumer imeConsumer = insetsController.getSourceConsumer(ITYPE_IME); + + // Initial IME insets source control with its leash. + imeConsumer.setControl(new InsetsSourceControl(ITYPE_IME, mLeash, + false /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]); + reset(mMockTransaction); + + // Verify when the app requests controlling show IME animation, the IME leash + // visibility won't be updated when the consumer received the same leash in setControl. + insetsController.controlWindowInsetsAnimation(ime(), 0L, + null /* interpolator */, null /* cancellationSignal */, null /* listener */); + assertTrue(insetsController.getAnimationType(ITYPE_IME) == ANIMATION_TYPE_USER); + imeConsumer.setControl(new InsetsSourceControl(ITYPE_IME, mLeash, + true /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]); + verify(mMockTransaction, never()).show(mLeash); + }); + } } diff --git a/core/tests/coretests/src/android/window/BackNavigationTest.java b/core/tests/coretests/src/android/window/BackNavigationTest.java index bbbc4230903a..77d61d589015 100644 --- a/core/tests/coretests/src/android/window/BackNavigationTest.java +++ b/core/tests/coretests/src/android/window/BackNavigationTest.java @@ -92,7 +92,7 @@ public class BackNavigationTest { try { mInstrumentation.getUiAutomation().waitForIdle(500, 1000); BackNavigationInfo info = ActivityTaskManager.getService() - .startBackNavigation(true, null); + .startBackNavigation(true, null, null); assertNotNull("BackNavigationInfo is null", info); assertNotNull("OnBackInvokedCallback is null", info.getOnBackInvokedCallback()); info.getOnBackInvokedCallback().onBackInvoked(); diff --git a/data/etc/platform.xml b/data/etc/platform.xml index 6897c01844a8..9a1b8a90dbfd 100644 --- a/data/etc/platform.xml +++ b/data/etc/platform.xml @@ -171,10 +171,11 @@ <assign-permission name="android.permission.UPDATE_DEVICE_STATS" uid="audioserver" /> <assign-permission name="android.permission.UPDATE_APP_OPS_STATS" uid="audioserver" /> <assign-permission name="android.permission.PACKAGE_USAGE_STATS" uid="audioserver" /> - <assign-permission name="android.permission.INTERACT_ACROSS_USERS" uid="audioserver" /> + <assign-permission name="android.permission.INTERACT_ACROSS_USERS_FULL" uid="audioserver" /> <assign-permission name="android.permission.OBSERVE_SENSOR_PRIVACY" uid="audioserver" /> <assign-permission name="android.permission.MODIFY_AUDIO_SETTINGS" uid="cameraserver" /> + <assign-permission name="android.permission.INTERACT_ACROSS_USERS_FULL" uid="cameraserver" /> <assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="cameraserver" /> <assign-permission name="android.permission.WAKE_LOCK" uid="cameraserver" /> <assign-permission name="android.permission.UPDATE_DEVICE_STATS" uid="cameraserver" /> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 4976784416ad..c2074da55c9a 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -2041,12 +2041,6 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "-108248992": { - "message": "Defer transition ready for TaskFragmentTransaction=%s", - "level": "VERBOSE", - "group": "WM_DEBUG_WINDOW_TRANSITIONS", - "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java" - }, "-106400104": { "message": "Preload recents with %s", "level": "DEBUG", @@ -2095,12 +2089,6 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/TaskFragment.java" }, - "-79016993": { - "message": "Continue transition ready for TaskFragmentTransaction=%s", - "level": "VERBOSE", - "group": "WM_DEBUG_WINDOW_TRANSITIONS", - "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java" - }, "-70719599": { "message": "Unregister remote animations for organizer=%s uid=%d pid=%d", "level": "VERBOSE", @@ -2647,6 +2635,12 @@ "group": "WM_DEBUG_ANIM", "at": "com\/android\/server\/wm\/WindowContainer.java" }, + "390947100": { + "message": "Screenshotting %s [%s]", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "397382873": { "message": "Moving to PAUSED: %s %s", "level": "VERBOSE", @@ -3079,6 +3073,12 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, + "851368695": { + "message": "Deferred transition id=%d has been continued before the TaskFragmentTransaction=%s is finished", + "level": "WARN", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java" + }, "872933199": { "message": "Changing focus from %s to %s displayId=%d Callers=%s", "level": "DEBUG", @@ -3271,6 +3271,12 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "1046228706": { + "message": "Defer transition id=%d for TaskFragmentTransaction=%s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java" + }, "1046922686": { "message": "requestScrollCapture: caught exception dispatching callback: %s", "level": "WARN", @@ -3313,6 +3319,12 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/WallpaperAnimationAdapter.java" }, + "1075460705": { + "message": "Continue transition id=%d for TaskFragmentTransaction=%s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/TaskFragmentOrganizerController.java" + }, "1087494661": { "message": "Clear window stuck on animatingExit status: %s", "level": "WARN", @@ -4171,6 +4183,12 @@ "group": "WM_DEBUG_FOCUS_LIGHT", "at": "com\/android\/server\/wm\/InputMonitor.java" }, + "2004282287": { + "message": "Override sync-method for %s because seamless rotating", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "2010476671": { "message": "Animation done in %s: reportedVisible=%b okToDisplay=%b okToAnimate=%b startingDisplayed=%b", "level": "VERBOSE", diff --git a/ktfmt_includes.txt b/ktfmt_includes.txt index 96da8c9c803b..c7062e093135 100644 --- a/ktfmt_includes.txt +++ b/ktfmt_includes.txt @@ -6,4 +6,12 @@ packages/SystemUI/src/com/android/systemui/keyguard/data packages/SystemUI/src/com/android/systemui/keyguard/dagger packages/SystemUI/src/com/android/systemui/keyguard/domain packages/SystemUI/src/com/android/systemui/keyguard/shared -packages/SystemUI/src/com/android/systemui/keyguard/ui
\ No newline at end of file +packages/SystemUI/src/com/android/systemui/keyguard/ui +packages/SystemUI/src/com/android/systemui/qs/footer +packages/SystemUI/src/com/android/systemui/security +packages/SystemUI/src/com/android/systemui/common/ +packages/SystemUI/tests/utils/src/com/android/systemui/qs/ +packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt +packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeUserInfoController.kt +packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/MockUserSwitcherControllerWrapper.kt +packages/SystemUI/tests/src/com/android/systemui/qs/footer/
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index 6bfb16a3c22d..f24401f0cd53 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -20,21 +20,26 @@ import static android.view.Display.DEFAULT_DISPLAY; import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT; import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED; +import static androidx.window.util.ExtensionHelper.isZero; import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; -import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityClient; import android.app.Application; import android.app.WindowConfiguration; +import android.content.ComponentCallbacks; import android.content.Context; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.util.ArrayMap; +import android.window.WindowContext; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiContext; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; @@ -58,11 +63,14 @@ import java.util.function.Consumer; public class WindowLayoutComponentImpl implements WindowLayoutComponent { private static final String TAG = "SampleExtension"; - private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = + private final Map<Context, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = new ArrayMap<>(); private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; + private final Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners = + new ArrayMap<>(); + public WindowLayoutComponentImpl(@NonNull Context context) { ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); @@ -78,14 +86,42 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * @param activity hosting a {@link android.view.Window} * @param consumer interested in receiving updates to {@link WindowLayoutInfo} */ + @Override public void addWindowLayoutInfoListener(@NonNull Activity activity, @NonNull Consumer<WindowLayoutInfo> consumer) { + addWindowLayoutInfoListener((Context) activity, consumer); + } + + /** + * Similar to {@link #addWindowLayoutInfoListener(Activity, Consumer)}, but takes a UI Context + * as a parameter. + */ + // TODO(b/204073440): Add @Override to hook the API in WM extensions library. + public void addWindowLayoutInfoListener(@NonNull @UiContext Context context, + @NonNull Consumer<WindowLayoutInfo> consumer) { + if (mWindowLayoutChangeListeners.containsKey(context) + || mWindowLayoutChangeListeners.containsValue(consumer)) { + // Early return if the listener or consumer has been registered. + return; + } + if (!context.isUiContext()) { + throw new IllegalArgumentException("Context must be a UI Context, which should be" + + " an Activity or a WindowContext"); + } mFoldingFeatureProducer.getData((features) -> { // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer. - WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(activity, features); + WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features); consumer.accept(newWindowLayout); }); - mWindowLayoutChangeListeners.put(activity, consumer); + mWindowLayoutChangeListeners.put(context, consumer); + + if (context instanceof WindowContext) { + final IBinder windowContextToken = context.getWindowContextToken(); + final WindowContextConfigListener listener = + new WindowContextConfigListener(windowContextToken); + context.registerComponentCallbacks(listener); + mWindowContextConfigListeners.put(windowContextToken, listener); + } } /** @@ -93,18 +129,30 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo} */ + @Override public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) { + for (Context context : mWindowLayoutChangeListeners.keySet()) { + if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) { + continue; + } + if (context instanceof WindowContext) { + final IBinder token = context.getWindowContextToken(); + context.unregisterComponentCallbacks(mWindowContextConfigListeners.get(token)); + mWindowContextConfigListeners.remove(token); + } + break; + } mWindowLayoutChangeListeners.values().remove(consumer); } @NonNull - Set<Activity> getActivitiesListeningForLayoutChanges() { + Set<Context> getContextsListeningForLayoutChanges() { return mWindowLayoutChangeListeners.keySet(); } private boolean isListeningForLayoutChanges(IBinder token) { - for (Activity activity: getActivitiesListeningForLayoutChanges()) { - if (token.equals(activity.getWindow().getAttributes().token)) { + for (Context context: getContextsListeningForLayoutChanges()) { + if (token.equals(Context.getToken(context))) { return true; } } @@ -138,10 +186,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { - for (Activity activity : getActivitiesListeningForLayoutChanges()) { + for (Context context : getContextsListeningForLayoutChanges()) { // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer. - Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(activity); - WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(activity, storedFeatures); + Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(context); + WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, storedFeatures); layoutConsumer.accept(newWindowLayout); } } @@ -149,11 +197,12 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { /** * Translates the {@link DisplayFeature} into a {@link WindowLayoutInfo} when a * valid state is found. - * @param activity a proxy for the {@link android.view.Window} that contains the + * @param context a proxy for the {@link android.view.Window} that contains the + * {@link DisplayFeature}. */ - private WindowLayoutInfo getWindowLayoutInfo( - @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) { - List<DisplayFeature> displayFeatureList = getDisplayFeatures(activity, storedFeatures); + private WindowLayoutInfo getWindowLayoutInfo(@NonNull @UiContext Context context, + List<CommonFoldingFeature> storedFeatures) { + List<DisplayFeature> displayFeatureList = getDisplayFeatures(context, storedFeatures); return new WindowLayoutInfo(displayFeatureList); } @@ -170,18 +219,18 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * bounds are not valid, constructing a {@link FoldingFeature} will throw an * {@link IllegalArgumentException} since this can cause negative UI effects down stream. * - * @param activity a proxy for the {@link android.view.Window} that contains the + * @param context a proxy for the {@link android.view.Window} that contains the * {@link DisplayFeature}. * are within the {@link android.view.Window} of the {@link Activity} */ private List<DisplayFeature> getDisplayFeatures( - @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) { + @NonNull @UiContext Context context, List<CommonFoldingFeature> storedFeatures) { List<DisplayFeature> features = new ArrayList<>(); - if (!shouldReportDisplayFeatures(activity)) { + if (!shouldReportDisplayFeatures(context)) { return features; } - int displayId = activity.getDisplay().getDisplayId(); + int displayId = context.getDisplay().getDisplayId(); for (CommonFoldingFeature baseFeature : storedFeatures) { Integer state = convertToExtensionState(baseFeature.getState()); if (state == null) { @@ -189,9 +238,9 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } Rect featureRect = baseFeature.getRect(); rotateRectToDisplayRotation(displayId, featureRect); - transformToWindowSpaceRect(activity, featureRect); + transformToWindowSpaceRect(context, featureRect); - if (!isRectZero(featureRect)) { + if (!isZero(featureRect)) { // TODO(b/228641877): Remove guarding when fixed. features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); } @@ -203,15 +252,21 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * Checks whether display features should be reported for the activity. * TODO(b/238948678): Support reporting display features in all windowing modes. */ - private boolean shouldReportDisplayFeatures(@NonNull Activity activity) { - int displayId = activity.getDisplay().getDisplayId(); + private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) { + int displayId = context.getDisplay().getDisplayId(); if (displayId != DEFAULT_DISPLAY) { // Display features are not supported on secondary displays. return false; } - final int taskWindowingMode = ActivityClient.getInstance().getTaskWindowingMode( - activity.getActivityToken()); - if (taskWindowingMode == -1) { + final int windowingMode; + if (context instanceof Activity) { + windowingMode = ActivityClient.getInstance().getTaskWindowingMode( + context.getActivityToken()); + } else { + windowingMode = context.getResources().getConfiguration().windowConfiguration + .getWindowingMode(); + } + if (windowingMode == -1) { // If we cannot determine the task windowing mode for any reason, it is likely that we // won't be able to determine its position correctly as well. DisplayFeatures' bounds // in this case can't be computed correctly, so we should skip. @@ -219,36 +274,43 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } // It is recommended not to report any display features in multi-window mode, since it // won't be possible to synchronize the display feature positions with window movement. - return !WindowConfiguration.inMultiWindowMode(taskWindowingMode); + return !WindowConfiguration.inMultiWindowMode(windowingMode); } - /** - * Returns {@link true} if a {@link Rect} has zero width and zero height, - * {@code false} otherwise. - */ - private boolean isRectZero(Rect rect) { - return rect.width() == 0 && rect.height() == 0; + private void onDisplayFeaturesChangedIfListening(@NonNull IBinder token) { + if (isListeningForLayoutChanges(token)) { + mFoldingFeatureProducer.getData( + WindowLayoutComponentImpl.this::onDisplayFeaturesChanged); + } } private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { super.onActivityCreated(activity, savedInstanceState); - onDisplayFeaturesChangedIfListening(activity); + onDisplayFeaturesChangedIfListening(activity.getActivityToken()); } @Override public void onActivityConfigurationChanged(Activity activity) { super.onActivityConfigurationChanged(activity); - onDisplayFeaturesChangedIfListening(activity); + onDisplayFeaturesChangedIfListening(activity.getActivityToken()); + } + } + + private final class WindowContextConfigListener implements ComponentCallbacks { + final IBinder mToken; + + WindowContextConfigListener(IBinder token) { + mToken = token; } - private void onDisplayFeaturesChangedIfListening(Activity activity) { - IBinder token = activity.getWindow().getAttributes().token; - if (token == null || isListeningForLayoutChanges(token)) { - mFoldingFeatureProducer.getData( - WindowLayoutComponentImpl.this::onDisplayFeaturesChanged); - } + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + onDisplayFeaturesChangedIfListening(mToken); } + + @Override + public void onLowMemory() {} } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java index 0da44ac36a6e..cbaa27712015 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java @@ -16,6 +16,7 @@ package androidx.window.util; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import java.util.LinkedHashSet; @@ -25,25 +26,45 @@ import java.util.function.Consumer; /** * Base class that provides the implementation for the callback mechanism of the - * {@link DataProducer} API. + * {@link DataProducer} API. This class is thread safe for adding, removing, and notifying + * consumers. * * @param <T> The type of data this producer returns through {@link DataProducer#getData}. */ public abstract class BaseDataProducer<T> implements DataProducer<T> { + + private final Object mLock = new Object(); + @GuardedBy("mLock") private final Set<Consumer<T>> mCallbacks = new LinkedHashSet<>(); + /** + * Adds a callback to the set of callbacks listening for data. Data is delivered through + * {@link BaseDataProducer#notifyDataChanged(Object)}. This method is thread safe. Callers + * should ensure that callbacks are thread safe. + * @param callback that will receive data from the producer. + */ @Override public final void addDataChangedCallback(@NonNull Consumer<T> callback) { - mCallbacks.add(callback); - Optional<T> currentData = getCurrentData(); - currentData.ifPresent(callback); - onListenersChanged(mCallbacks); + synchronized (mLock) { + mCallbacks.add(callback); + Optional<T> currentData = getCurrentData(); + currentData.ifPresent(callback); + onListenersChanged(mCallbacks); + } } + /** + * Removes a callback to the set of callbacks listening for data. This method is thread safe + * for adding. + * @param callback that was registered in + * {@link BaseDataProducer#addDataChangedCallback(Consumer)}. + */ @Override public final void removeDataChangedCallback(@NonNull Consumer<T> callback) { - mCallbacks.remove(callback); - onListenersChanged(mCallbacks); + synchronized (mLock) { + mCallbacks.remove(callback); + onListenersChanged(mCallbacks); + } } protected void onListenersChanged(Set<Consumer<T>> callbacks) {} @@ -56,11 +77,14 @@ public abstract class BaseDataProducer<T> implements DataProducer<T> { /** * Called to notify all registered consumers that the data provided - * by {@link DataProducer#getData} has changed. + * by {@link DataProducer#getData} has changed. Calls to this are thread save but callbacks need + * to ensure thread safety. */ protected void notifyDataChanged(T value) { - for (Consumer<T> callback : mCallbacks) { - callback.accept(value); + synchronized (mLock) { + for (Consumer<T> callback : mCallbacks) { + callback.accept(value); + } } } }
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java index 2a593f15a9de..31bf96313a95 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java @@ -21,14 +21,15 @@ import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; -import android.app.Activity; +import android.content.Context; import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; import android.view.DisplayInfo; import android.view.Surface; +import android.view.WindowManager; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import androidx.annotation.UiContext; /** * Util class for both Sidecar and Extensions. @@ -86,12 +87,9 @@ public final class ExtensionHelper { } /** Transforms rectangle from absolute coordinate space to the window coordinate space. */ - public static void transformToWindowSpaceRect(Activity activity, Rect inOutRect) { - Rect windowRect = getWindowBounds(activity); - if (windowRect == null) { - inOutRect.setEmpty(); - return; - } + public static void transformToWindowSpaceRect(@NonNull @UiContext Context context, + Rect inOutRect) { + Rect windowRect = getWindowBounds(context); if (!Rect.intersects(inOutRect, windowRect)) { inOutRect.setEmpty(); return; @@ -103,9 +101,9 @@ public final class ExtensionHelper { /** * Gets the current window bounds in absolute coordinates. */ - @Nullable - private static Rect getWindowBounds(@NonNull Activity activity) { - return activity.getWindowManager().getCurrentWindowMetrics().getBounds(); + @NonNull + private static Rect getWindowBounds(@NonNull @UiContext Context context) { + return context.getSystemService(WindowManager.class).getCurrentWindowMetrics().getBounds(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index d3e46f82efe5..33ecdd88fad3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -16,6 +16,9 @@ package com.android.wm.shell.back; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; + import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; @@ -27,8 +30,6 @@ import android.app.WindowConfiguration; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; -import android.graphics.Point; -import android.graphics.PointF; import android.hardware.HardwareBuffer; import android.hardware.input.InputManager; import android.net.Uri; @@ -47,8 +48,11 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.window.BackAnimationAdaptor; import android.window.BackEvent; import android.window.BackNavigationInfo; +import android.window.IBackAnimationRunner; +import android.window.IBackNaviAnimationController; import android.window.IOnBackInvokedCallback; import com.android.internal.annotations.VisibleForTesting; @@ -76,22 +80,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private static final int PROGRESS_THRESHOLD = SystemProperties .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); + // TODO (b/241808055) Find a appropriate time to remove during refactor + private static final boolean USE_TRANSITION = + SystemProperties.getInt("persist.wm.debug.predictive_back_ani_trans", 1) != 0; /** * Max duration to wait for a transition to finish before accepting another gesture start * request. */ private static final long MAX_TRANSITION_DURATION = 2000; - /** - * Location of the initial touch event of the back gesture. - */ - private final PointF mInitTouchLocation = new PointF(); - - /** - * Raw delta between {@link #mInitTouchLocation} and the last touch location. - */ - private final Point mTouchEventDelta = new Point(); - /** True when a back gesture is ongoing */ private boolean mBackGestureStarted = false; @@ -119,6 +116,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTransitionInProgress = false; }; + private RemoteAnimationTarget mAnimationTarget; + IBackAnimationRunner mIBackAnimationRunner; + private IBackNaviAnimationController mBackAnimationController; + private BackAnimationAdaptor mBackAnimationAdaptor; + + private boolean mWaitingAnimationStart; + private final TouchTracker mTouchTracker = new TouchTracker(); + private final CachingBackDispatcher mCachingBackDispatcher = new CachingBackDispatcher(); + @VisibleForTesting final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() { @Override @@ -137,6 +143,92 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } }; + /** + * Helper class to record the touch location for gesture start and latest. + */ + private static class TouchTracker { + /** + * Location of the latest touch event + */ + private float mLatestTouchX; + private float mLatestTouchY; + private int mSwipeEdge; + + /** + * Location of the initial touch event of the back gesture. + */ + private float mInitTouchX; + private float mInitTouchY; + + void update(float touchX, float touchY, int swipeEdge) { + mLatestTouchX = touchX; + mLatestTouchY = touchY; + mSwipeEdge = swipeEdge; + } + + void setGestureStartLocation(float touchX, float touchY) { + mInitTouchX = touchX; + mInitTouchY = touchY; + } + + int getDeltaFromGestureStart(float touchX) { + return Math.round(touchX - mInitTouchX); + } + + void reset() { + mInitTouchX = 0; + mInitTouchY = 0; + } + } + + /** + * Cache the temporary callback and trigger result if gesture was finish before received + * BackAnimationRunner#onAnimationStart/cancel, so there can continue play the animation. + */ + private class CachingBackDispatcher { + private IOnBackInvokedCallback mOnBackCallback; + private boolean mTriggerBack; + // Whether we are waiting to receive onAnimationStart + private boolean mWaitingAnimation; + + void startWaitingAnimation() { + mWaitingAnimation = true; + } + + boolean set(IOnBackInvokedCallback callback, boolean triggerBack) { + if (mWaitingAnimation) { + mOnBackCallback = callback; + mTriggerBack = triggerBack; + return true; + } + return false; + } + + boolean consume() { + boolean consumed = false; + if (mWaitingAnimation && mOnBackCallback != null) { + if (mTriggerBack) { + final BackEvent backFinish = new BackEvent( + mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 1, + mTouchTracker.mSwipeEdge, mAnimationTarget); + dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); + dispatchOnBackInvoked(mOnBackCallback); + } else { + final BackEvent backFinish = new BackEvent( + mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 0, + mTouchTracker.mSwipeEdge, mAnimationTarget); + dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); + dispatchOnBackCancelled(mOnBackCallback); + } + startTransition(); + consumed = true; + } + mOnBackCallback = null; + mWaitingAnimation = false; + return consumed; + } + } + public BackAnimationController( @NonNull ShellInit shellInit, @NonNull @ShellMainThread ShellExecutor shellExecutor, @@ -272,6 +364,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @VisibleForTesting void setBackToLauncherCallback(IOnBackInvokedCallback callback) { mBackToLauncherCallback = callback; + if (USE_TRANSITION) { + createAdaptor(); + } } private void clearBackToLauncherCallback() { @@ -280,15 +375,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @VisibleForTesting void onBackToLauncherAnimationFinished() { - if (mBackNavigationInfo != null) { - IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); - if (mTriggerBack) { + final boolean triggerBack = mTriggerBack; + IOnBackInvokedCallback callback = mBackNavigationInfo != null + ? mBackNavigationInfo.getOnBackInvokedCallback() : null; + // Make sure the notification sequence should be controller > client. + finishAnimation(); + if (callback != null) { + if (triggerBack) { dispatchOnBackInvoked(callback); } else { dispatchOnBackCancelled(callback); } } - finishAnimation(); } /** @@ -300,6 +398,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mTransitionInProgress) { return; } + + mTouchTracker.update(touchX, touchY, swipeEdge); if (keyAction == MotionEvent.ACTION_DOWN) { if (!mBackGestureStarted) { mShouldStartOnNextMoveEvent = true; @@ -330,13 +430,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont finishAnimation(); } - mInitTouchLocation.set(touchX, touchY); + mTouchTracker.setGestureStartLocation(touchX, touchY); mBackGestureStarted = true; try { boolean requestAnimation = mEnableAnimations.get(); - mBackNavigationInfo = - mActivityTaskManager.startBackNavigation(requestAnimation, mFocusObserver); + mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation, + mFocusObserver, mBackAnimationAdaptor); onBackNavigationInfoReceived(mBackNavigationInfo); } catch (RemoteException remoteException) { Log.e(TAG, "Failed to initAnimation", remoteException); @@ -352,6 +452,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } int backType = backNavigationInfo.getType(); IOnBackInvokedCallback targetCallback = null; + final boolean dispatchToLauncher = shouldDispatchToLauncher(backType); if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { HardwareBuffer hardwareBuffer = backNavigationInfo.getScreenshotHardwareBuffer(); if (hardwareBuffer != null) { @@ -359,12 +460,17 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont backNavigationInfo.getTaskWindowConfiguration()); } mTransaction.apply(); - } else if (shouldDispatchToLauncher(backType)) { + } else if (dispatchToLauncher) { targetCallback = mBackToLauncherCallback; + if (USE_TRANSITION) { + mCachingBackDispatcher.startWaitingAnimation(); + } } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); } - dispatchOnBackStarted(targetCallback); + if (!USE_TRANSITION || !dispatchToLauncher) { + dispatchOnBackStarted(targetCallback); + } } /** @@ -403,24 +509,33 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mBackGestureStarted || mBackNavigationInfo == null) { return; } - int deltaX = Math.round(touchX - mInitTouchLocation.x); + int deltaX = mTouchTracker.getDeltaFromGestureStart(touchX); float progressThreshold = PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold; float progress = Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1); - int backType = mBackNavigationInfo.getType(); - RemoteAnimationTarget animationTarget = mBackNavigationInfo.getDepartingAnimationTarget(); - - BackEvent backEvent = new BackEvent( - touchX, touchY, progress, swipeEdge, animationTarget); - IOnBackInvokedCallback targetCallback = null; - if (shouldDispatchToLauncher(backType)) { - targetCallback = mBackToLauncherCallback; - } else if (backType == BackNavigationInfo.TYPE_CROSS_TASK - || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { - // TODO(208427216) Run the actual animation - } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { - targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); + if (USE_TRANSITION) { + if (mBackAnimationController != null && mAnimationTarget != null) { + final BackEvent backEvent = new BackEvent( + touchX, touchY, progress, swipeEdge, mAnimationTarget); + dispatchOnBackProgressed(mBackToLauncherCallback, backEvent); + } + } else { + int backType = mBackNavigationInfo.getType(); + RemoteAnimationTarget animationTarget = + mBackNavigationInfo.getDepartingAnimationTarget(); + + BackEvent backEvent = new BackEvent( + touchX, touchY, progress, swipeEdge, animationTarget); + IOnBackInvokedCallback targetCallback = null; + if (shouldDispatchToLauncher(backType)) { + targetCallback = mBackToLauncherCallback; + } else if (backType == BackNavigationInfo.TYPE_CROSS_TASK + || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { + // TODO(208427216) Run the actual animation + } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { + targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); + } + dispatchOnBackProgressed(targetCallback, backEvent); } - dispatchOnBackProgressed(targetCallback, backEvent); } private void injectBackKey() { @@ -474,6 +589,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont IOnBackInvokedCallback targetCallback = shouldDispatchToLauncher ? mBackToLauncherCallback : mBackNavigationInfo.getOnBackInvokedCallback(); + if (mCachingBackDispatcher.set(targetCallback, mTriggerBack)) { + return; + } if (shouldDispatchToLauncher) { startTransition(); } @@ -493,7 +611,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont && mBackToLauncherCallback != null && mEnableAnimations.get() && mBackNavigationInfo != null - && mBackNavigationInfo.getDepartingAnimationTarget() != null; + && ((USE_TRANSITION && mBackNavigationInfo.isPrepareRemoteAnimation()) + || mBackNavigationInfo.getDepartingAnimationTarget() != null); } private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) { @@ -558,8 +677,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void finishAnimation() { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishAnimation()"); - mTouchEventDelta.set(0, 0); - mInitTouchLocation.set(0, 0); + mTouchTracker.reset(); BackNavigationInfo backNavigationInfo = mBackNavigationInfo; boolean triggerBack = mTriggerBack; mBackNavigationInfo = null; @@ -569,19 +687,33 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } - RemoteAnimationTarget animationTarget = backNavigationInfo.getDepartingAnimationTarget(); - if (animationTarget != null) { - if (animationTarget.leash != null && animationTarget.leash.isValid()) { - mTransaction.remove(animationTarget.leash); + if (!USE_TRANSITION) { + RemoteAnimationTarget animationTarget = backNavigationInfo + .getDepartingAnimationTarget(); + if (animationTarget != null) { + if (animationTarget.leash != null && animationTarget.leash.isValid()) { + mTransaction.remove(animationTarget.leash); + } } + SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface(); + if (screenshotSurface != null && screenshotSurface.isValid()) { + mTransaction.remove(screenshotSurface); + } + mTransaction.apply(); } - SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface(); - if (screenshotSurface != null && screenshotSurface.isValid()) { - mTransaction.remove(screenshotSurface); - } - mTransaction.apply(); stopTransition(); backNavigationInfo.onBackNavigationFinished(triggerBack); + if (USE_TRANSITION) { + final IBackNaviAnimationController controller = mBackAnimationController; + if (controller != null) { + try { + controller.finish(triggerBack); + } catch (RemoteException r) { + // Oh no! + } + } + mBackAnimationController = null; + } } private void startTransition() { @@ -599,4 +731,50 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor.removeCallbacks(mResetTransitionRunnable); mTransitionInProgress = false; } + + private void createAdaptor() { + mIBackAnimationRunner = new IBackAnimationRunner.Stub() { + @Override + public void onAnimationCancelled() { + // no op for now + } + @Override // Binder interface + public void onAnimationStart(IBackNaviAnimationController controller, int type, + RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps) { + mShellExecutor.execute(() -> { + mBackAnimationController = controller; + for (int i = 0; i < apps.length; i++) { + final RemoteAnimationTarget target = apps[i]; + if (MODE_CLOSING == target.mode) { + mAnimationTarget = target; + } else if (MODE_OPENING == target.mode) { + // TODO Home activity should handle the visibility for itself + // once it finish relayout for orientation change + SurfaceControl.Transaction tx = + new SurfaceControl.Transaction(); + tx.setAlpha(target.leash, 1); + tx.apply(); + } + } + // TODO animation target should be passed at onBackStarted + dispatchOnBackStarted(mBackToLauncherCallback); + // TODO This is Workaround for LauncherBackAnimationController, there will need + // to dispatch onBackProgressed twice(startBack & updateBackProgress) to + // initialize the animation data, for now that would happen when onMove + // called, but there will no expected animation if the down -> up gesture + // happen very fast which ACTION_MOVE only happen once. + final BackEvent backInit = new BackEvent( + mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 0, + mTouchTracker.mSwipeEdge, mAnimationTarget); + dispatchOnBackProgressed(mBackToLauncherCallback, backInit); + if (!mCachingBackDispatcher.consume()) { + dispatchOnBackProgressed(mBackToLauncherCallback, backInit); + } + }); + } + }; + mBackAnimationAdaptor = new BackAnimationAdaptor(mIBackAnimationRunner, + BackNavigationInfo.TYPE_RETURN_TO_HOME); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 83ba909e712d..b7959eb629c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -80,10 +80,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public static final int PARALLAX_DISMISSING = 1; public static final int PARALLAX_ALIGN_CENTER = 2; - private static final int FLING_RESIZE_DURATION = 250; - private static final int FLING_SWITCH_DURATION = 350; - private static final int FLING_ENTER_DURATION = 350; - private static final int FLING_EXIT_DURATION = 350; + private static final int FLING_ANIMATION_DURATION = 250; private final int mDividerWindowWidth; private final int mDividerInsets; @@ -96,9 +93,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final Rect mBounds1 = new Rect(); // Bounds2 final position should be always at bottom or right private final Rect mBounds2 = new Rect(); - // The temp bounds outside of display bounds for side stage when split screen inactive to avoid - // flicker next time active split screen. - private final Rect mInvisibleBounds = new Rect(); private final Rect mWinBounds1 = new Rect(); private final Rect mWinBounds2 = new Rect(); private final SplitLayoutHandler mSplitLayoutHandler; @@ -147,10 +141,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange resetDividerPosition(); mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide); - - mInvisibleBounds.set(mRootBounds); - mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0, - isLandscape() ? 0 : mRootBounds.bottom); } private int getDividerInsets(Resources resources, Display display) { @@ -249,12 +239,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange rect.offset(-mRootBounds.left, -mRootBounds.top); } - /** Gets bounds size equal to root bounds but outside of screen, used for position side stage - * when split inactive to avoid flicker when next time active. */ - public void getInvisibleBounds(Rect rect) { - rect.set(mInvisibleBounds); - } - /** Returns leash of the current divider bar. */ @Nullable public SurfaceControl getDividerLeash() { @@ -300,10 +284,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); initDividerPosition(mTempRect); - mInvisibleBounds.set(mRootBounds); - mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0, - isLandscape() ? 0 : mRootBounds.bottom); - return true; } @@ -425,13 +405,6 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mFreezeDividerWindow = freezeDividerWindow; } - /** Update current layout as divider put on start or end position. */ - public void setDividerAtBorder(boolean start) { - final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position - : mDividerSnapAlgorithm.getDismissEndTarget().position; - setDividePosition(pos, false /* applyLayoutChange */); - } - /** * Updates bounds with the passing position. Usually used to update recording bounds while * performing animation or dragging divider bar to resize the splits. @@ -476,17 +449,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { switch (snapTarget.flag) { case FLAG_DISMISS_START: - flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + flingDividePosition(currentPosition, snapTarget.position, () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */, EXIT_REASON_DRAG_DIVIDER)); break; case FLAG_DISMISS_END: - flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + flingDividePosition(currentPosition, snapTarget.position, () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */, EXIT_REASON_DRAG_DIVIDER)); break; default: - flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + flingDividePosition(currentPosition, snapTarget.position, () -> setDividePosition(snapTarget.position, true /* applyLayoutChange */)); break; } @@ -520,11 +493,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange final Rect insets = stableInsets != null ? stableInsets : getDisplayInsets(context); // Make split axis insets value same as the larger one to avoid bounds1 and bounds2 - // have difference after split switching for solving issues on non-resizable app case. - if (isLandscape) { - final int largerInsets = Math.max(insets.left, insets.right); - insets.set(largerInsets, insets.top, largerInsets, insets.bottom); - } else { + // have difference for avoiding size-compat mode when switching unresizable apps in + // landscape while they are letterboxed. + if (!isLandscape) { final int largerInsets = Math.max(insets.top, insets.bottom); insets.set(insets.left, largerInsets, insets.right, largerInsets); } @@ -543,20 +514,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void flingDividerToDismiss(boolean toEnd, int reason) { final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position : mDividerSnapAlgorithm.getDismissStartTarget().position; - flingDividePosition(getDividePosition(), target, FLING_EXIT_DURATION, + flingDividePosition(getDividePosition(), target, () -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason)); } - /** Fling divider from current position to center position. */ - public void flingDividerToCenter() { - final int pos = mDividerSnapAlgorithm.getMiddleTarget().position; - flingDividePosition(getDividePosition(), pos, FLING_ENTER_DURATION, - () -> setDividePosition(pos, true /* applyLayoutChange */)); - } - @VisibleForTesting - void flingDividePosition(int from, int to, int duration, - @Nullable Runnable flingFinishedCallback) { + void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) { if (from == to) { // No animation run, still callback to stop resizing. mSplitLayoutHandler.onLayoutSizeChanged(this); @@ -566,7 +529,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } ValueAnimator animator = ValueAnimator .ofInt(from, to) - .setDuration(duration); + .setDuration(FLING_ANIMATION_DURATION); animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); animator.addUpdateListener( animation -> updateDivideBounds((int) animation.getAnimatedValue())); @@ -623,7 +586,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange AnimatorSet set = new AnimatorSet(); set.playTogether(animator1, animator2, animator3); - set.setDuration(FLING_SWITCH_DURATION); + set.setDuration(FLING_ANIMATION_DURATION); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java index 2fdd12185551..e91987dab972 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java @@ -316,11 +316,13 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { true /* onTop */); wct.reorder(rootToken, mEnabled /* onTop */); mSyncQueue.queue(wct); - final SurfaceControl rootLeash = mLaunchRootLeash; - mSyncQueue.runInSync(t -> { - t.setPosition(rootLeash, taskBounds.left, taskBounds.top); - t.setWindowCrop(rootLeash, taskBounds.width(), taskBounds.height()); - }); + if (mEnabled) { + final SurfaceControl rootLeash = mLaunchRootLeash; + mSyncQueue.runInSync(t -> { + t.setPosition(rootLeash, taskBounds.left, taskBounds.top); + t.setWindowCrop(rootLeash, taskBounds.width(), taskBounds.height()); + }); + } } private Rect calculateBounds() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 21fc01e554c8..7e83d2fa0a0b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -25,7 +25,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; -import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; @@ -489,6 +488,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); + // If split still not active, apply windows bounds first to avoid surface reset to + // wrong pos by SurfaceAnimator from wms. + // TODO(b/223325631): check is it still necessary after improve enter transition done. + if (!mMainStage.isActive()) { + updateWindowBounds(mSplitLayout, wct); + } + wct.sendPendingIntent(intent, fillInIntent, options); mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); } @@ -635,7 +641,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.startTask(sideTaskId, sideOptions); } // Using legacy transitions, so we can't use blast sync since it conflicts. - mSyncQueue.queue(wct); + mTaskOrganizer.applyTransaction(wct); mSyncQueue.runInSync(t -> { setDividerVisibility(true, t); updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); @@ -887,13 +893,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mShouldUpdateRecents = false; mIsDividerRemoteAnimating = false; - mSplitLayout.getInvisibleBounds(mTempRect1); if (childrenToTop == null) { mSideStage.removeAllTasks(wct, false /* toTop */); mMainStage.deactivate(wct, false /* toTop */); wct.reorder(mRootTaskInfo.token, false /* onTop */); - wct.setForceTranslucent(mRootTaskInfo.token, true); - wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); onTransitionAnimationComplete(); } else { // Expand to top side split as full screen for fading out decor animation and dismiss @@ -904,32 +907,27 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ? mSideStage : mMainStage; tempFullStage.resetBounds(wct); wct.setSmallestScreenWidthDp(tempFullStage.mRootTaskInfo.token, - SMALLEST_SCREEN_WIDTH_DP_UNDEFINED); + mRootTaskInfo.configuration.smallestScreenWidthDp); dismissStage.dismiss(wct, false /* toTop */); } mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { t.setWindowCrop(mMainStage.mRootLeash, null) .setWindowCrop(mSideStage.mRootLeash, null); + t.setPosition(mMainStage.mRootLeash, 0, 0) + .setPosition(mSideStage.mRootLeash, 0, 0); t.hide(mMainStage.mDimLayer).hide(mSideStage.mDimLayer); setDividerVisibility(false, t); - if (childrenToTop == null) { - t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right); - } else { - // In this case, exit still under progress, fade out the split decor after first WCT - // done and do remaining WCT after animation finished. + // In this case, exit still under progress, fade out the split decor after first WCT + // done and do remaining WCT after animation finished. + if (childrenToTop != null) { childrenToTop.fadeOutDecor(() -> { WindowContainerTransaction finishedWCT = new WindowContainerTransaction(); mIsExiting = false; childrenToTop.dismiss(finishedWCT, true /* toTop */); finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */); - finishedWCT.setForceTranslucent(mRootTaskInfo.token, true); - finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); - mSyncQueue.queue(finishedWCT); - mSyncQueue.runInSync(at -> { - at.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right); - }); + mTaskOrganizer.applyTransaction(finishedWCT); onTransitionAnimationComplete(); }); } @@ -998,7 +996,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, true /* includingTopTask */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); } void finishEnterSplitScreen(SurfaceControl.Transaction t) { @@ -1224,13 +1221,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Make the stages adjacent to each other so they occlude what's behind them. wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - wct.setForceTranslucent(mRootTaskInfo.token, true); - mSplitLayout.getInvisibleBounds(mTempRect1); - wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top); - }); + mTaskOrganizer.applyTransaction(wct); } private void onRootTaskVanished() { @@ -1386,17 +1377,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // TODO (b/238697912) : Add the validation to prevent entering non-recovered status final WindowContainerTransaction wct = new WindowContainerTransaction(); mSplitLayout.init(); - mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); - mMainStage.activate(wct, true /* includingTopTask */); - updateWindowBounds(mSplitLayout, wct); - wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + prepareEnterSplitScreen(wct); mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); - - mSplitLayout.flingDividerToCenter(); - }); + mSyncQueue.runInSync(t -> + updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */)); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { mShouldUpdateRecents = true; @@ -1838,7 +1822,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // properly for the animation itself. mSplitLayout.release(); mSplitLayout.resetDividerPosition(); - mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; } } 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 6c659667a4a7..cff60f5e5b6c 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 @@ -44,6 +44,8 @@ import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; +import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; +import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; @@ -903,11 +905,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private void attachThumbnail(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, TransitionInfo.Change change, TransitionInfo.AnimationOptions options, float cornerRadius) { - final boolean isTask = change.getTaskInfo() != null; final boolean isOpen = Transitions.isOpeningType(change.getMode()); final boolean isClose = Transitions.isClosingType(change.getMode()); if (isOpen) { - if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS && isTask) { + if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) { attachCrossProfileThumbnailAnimation(animations, finishCallback, change, cornerRadius); } else if (options.getType() == ANIM_THUMBNAIL_SCALE_UP) { @@ -922,8 +923,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull Runnable finishCallback, TransitionInfo.Change change, float cornerRadius) { final Rect bounds = change.getEndAbsBounds(); // Show the right drawable depending on the user we're transitioning to. - final Drawable thumbnailDrawable = change.getTaskInfo().userId == mCurrentUserId - ? mContext.getDrawable(R.drawable.ic_account_circle) : mEnterpriseThumbnailDrawable; + final Drawable thumbnailDrawable = change.hasFlags(FLAG_CROSS_PROFILE_OWNER_THUMBNAIL) + ? mContext.getDrawable(R.drawable.ic_account_circle) + : change.hasFlags(FLAG_CROSS_PROFILE_WORK_THUMBNAIL) + ? mEnterpriseThumbnailDrawable : null; + if (thumbnailDrawable == null) { + return; + } final HardwareBuffer thumbnail = mTransitionAnimation.createCrossProfileAppsThumbnail( thumbnailDrawable, bounds); if (thumbnail == null) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java index da95c77d2b89..fe8b305093d7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java @@ -48,9 +48,10 @@ public class TestShellExecutor implements ShellExecutor { } public void flushAll() { - for (Runnable r : mRunnables) { + final ArrayList<Runnable> tmpRunnable = new ArrayList<>(mRunnables); + mRunnables.clear(); + for (Runnable r : tmpRunnable) { r.run(); } - mRunnables.clear(); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 5b3b8fd7ad71..90a377309edd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -54,6 +54,7 @@ import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.BackEvent; import android.window.BackNavigationInfo; +import android.window.IBackNaviAnimationController; import android.window.IOnBackInvokedCallback; import androidx.test.filters.SmallTest; @@ -98,6 +99,9 @@ public class BackAnimationControllerTest extends ShellTestCase { @Mock private IOnBackInvokedCallback mIOnBackInvokedCallback; + @Mock + private IBackNaviAnimationController mIBackNaviAnimationController; + private BackAnimationController mController; private int mEventTime = 0; @@ -127,7 +131,7 @@ public class BackAnimationControllerTest extends ShellTestCase { SurfaceControl screenshotSurface, HardwareBuffer hardwareBuffer, int backType, - IOnBackInvokedCallback onBackInvokedCallback) { + IOnBackInvokedCallback onBackInvokedCallback, boolean prepareAnimation) { BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder() .setType(backType) .setDepartingAnimationTarget(topAnimationTarget) @@ -135,7 +139,8 @@ public class BackAnimationControllerTest extends ShellTestCase { .setScreenshotBuffer(hardwareBuffer) .setTaskWindowConfiguration(new WindowConfiguration()) .setOnBackNavigationDone(new RemoteCallback((bundle) -> {})) - .setOnBackInvokedCallback(onBackInvokedCallback); + .setOnBackInvokedCallback(onBackInvokedCallback) + .setPrepareAnimation(prepareAnimation); createNavigationInfo(builder); } @@ -143,7 +148,7 @@ public class BackAnimationControllerTest extends ShellTestCase { private void createNavigationInfo(BackNavigationInfo.Builder builder) { try { doReturn(builder.build()).when(mActivityTaskManager) - .startBackNavigation(anyBoolean(), any()); + .startBackNavigation(anyBoolean(), any(), any()); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } @@ -175,7 +180,7 @@ public class BackAnimationControllerTest extends ShellTestCase { SurfaceControl screenshotSurface = new SurfaceControl(); HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class); createNavigationInfo(createAnimationTarget(), screenshotSurface, hardwareBuffer, - BackNavigationInfo.TYPE_CROSS_ACTIVITY, null); + BackNavigationInfo.TYPE_CROSS_ACTIVITY, null, true); doMotionEvent(MotionEvent.ACTION_DOWN, 0); verify(mTransaction).setBuffer(screenshotSurface, hardwareBuffer); verify(mTransaction).setVisibility(screenshotSurface, true); @@ -188,7 +193,7 @@ public class BackAnimationControllerTest extends ShellTestCase { HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class); RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, screenshotSurface, hardwareBuffer, - BackNavigationInfo.TYPE_CROSS_ACTIVITY, null); + BackNavigationInfo.TYPE_CROSS_ACTIVITY, null, true); doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); // b/207481538, we check that the surface is not moved for now, we can re-enable this once @@ -222,15 +227,16 @@ public class BackAnimationControllerTest extends ShellTestCase { mController.setBackToLauncherCallback(mIOnBackInvokedCallback); RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true); doMotionEvent(MotionEvent.ACTION_DOWN, 0); // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); verify(mIOnBackInvokedCallback).onBackStarted(); ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); - verify(mIOnBackInvokedCallback).onBackProgressed(backEventCaptor.capture()); + verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture()); assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget()); // Check that back invocation is dispatched. @@ -255,7 +261,7 @@ public class BackAnimationControllerTest extends ShellTestCase { IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class); ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback); + BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback, false); triggerBackGesture(); @@ -273,9 +279,10 @@ public class BackAnimationControllerTest extends ShellTestCase { mController.setBackToLauncherCallback(mIOnBackInvokedCallback); RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true); triggerBackGesture(); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); // Check that back invocation is dispatched. verify(mIOnBackInvokedCallback).onBackInvoked(); @@ -294,6 +301,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Verify that we start accepting gestures again once transition finishes. doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); verify(mIOnBackInvokedCallback).onBackStarted(); } @@ -302,15 +310,17 @@ public class BackAnimationControllerTest extends ShellTestCase { mController.setBackToLauncherCallback(mIOnBackInvokedCallback); RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true); triggerBackGesture(); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); reset(mIOnBackInvokedCallback); // Simulate transition timeout. mShellExecutor.flushAll(); doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); verify(mIOnBackInvokedCallback).onBackStarted(); } @@ -321,11 +331,12 @@ public class BackAnimationControllerTest extends ShellTestCase { RemoteAnimationTarget animationTarget = createAnimationTarget(); createNavigationInfo(animationTarget, null, null, - BackNavigationInfo.TYPE_RETURN_TO_HOME, null); + BackNavigationInfo.TYPE_RETURN_TO_HOME, null, true); doMotionEvent(MotionEvent.ACTION_DOWN, 0); // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); + simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); verify(mIOnBackInvokedCallback).onBackStarted(); // Check that back invocation is dispatched. @@ -349,4 +360,14 @@ public class BackAnimationControllerTest extends ShellTestCase { BackEvent.EDGE_LEFT); mEventTime += 10; } + + private void simulateRemoteAnimationStart(int type, RemoteAnimationTarget animationTarget) + throws RemoteException { + if (mController.mIBackAnimationRunner != null) { + final RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget}; + mController.mIBackAnimationRunner.onAnimationStart(mIBackNaviAnimationController, type, + targets, null, null); + mShellExecutor.flushAll(); + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 695550dd8fa5..95725bbfd855 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -159,8 +159,7 @@ public class SplitLayoutTests extends ShellTestCase { } private void waitDividerFlingFinished() { - verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), anyInt(), - mRunnableCaptor.capture()); + verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture()); mRunnableCaptor.getValue().run(); } diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml index 35d13230a3b7..2aa26e321a91 100644 --- a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml +++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml @@ -20,6 +20,11 @@ android:layout_height="wrap_content" android:layout_width="match_parent" android:background="?android:attr/colorBackground" + android:minHeight="?android:attr/listPreferredItemHeight" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingTop="@dimen/settingslib_switchbar_margin" + android:paddingBottom="@dimen/settingslib_switchbar_margin" android:orientation="vertical"> <LinearLayout @@ -27,7 +32,6 @@ android:minHeight="@dimen/settingslib_min_switch_bar_height" android:layout_height="wrap_content" android:layout_width="match_parent" - android:layout_margin="@dimen/settingslib_switchbar_margin" android:paddingStart="@dimen/settingslib_switchbar_padding_left" android:paddingEnd="@dimen/settingslib_switchbar_padding_right"> diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java index d86bd014988e..24037caf4e6c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java @@ -16,6 +16,8 @@ package com.android.settingslib.widget; +import static android.graphics.text.LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE; + import static com.google.common.truth.Truth.assertThat; import android.content.Context; @@ -97,4 +99,14 @@ public class MainSwitchBarTest { assertThat(mBar.getVisibility()).isEqualTo(View.GONE); } + + @Test + public void setTitle_shouldSetCorrectLineBreakStyle() { + final String title = "title"; + + mBar.setTitle(title); + final TextView textView = ((TextView) mBar.findViewById(R.id.switch_text)); + + assertThat(textView.getLineBreakWordStyle()).isEqualTo(LINE_BREAK_WORD_STYLE_PHRASE); + } } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 86d2eb85479f..36fbf8f850d3 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -210,6 +210,8 @@ public class SecureSettings { Settings.Secure.LOCKSCREEN_SHOW_WALLET, Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER, Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, - Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON + Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON, + Settings.Secure.ASSIST_TOUCH_GESTURE_ENABLED, + Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED }; } diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index cd3a72271603..ff4c748f4946 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -230,6 +230,7 @@ android_library { libs: [ "android.test.runner", "android.test.base", + "android.test.mock", ], kotlincflags: ["-Xjvm-default=enable"], aaptflags: [ diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 8ddd430dadbc..7d4dcf88542b 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -311,8 +311,7 @@ class ActivityLaunchAnimator( @JvmStatic fun fromView(view: View, cujType: Int? = null): Controller? { if (view.parent !is ViewGroup) { - // TODO(b/192194319): Throw instead of just logging. - Log.wtf( + Log.e( TAG, "Skipping animation as view $view is not attached to a ViewGroup", Exception() diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index 8f9ced6956ca..eac5d275092a 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -113,6 +113,19 @@ constructor( } val animateFrom = animatedParent?.dialogContentWithBackground ?: view + if (animatedParent == null && animateFrom !is LaunchableView) { + // Make sure the View we launch from implements LaunchableView to avoid visibility + // issues. Given that we don't own dialog decorViews so we can't enforce it for launches + // from a dialog. + // TODO(b/243636422): Throw instead of logging to enforce this. + Log.w( + TAG, + "A dialog was launched from a View that does not implement LaunchableView. This " + + "can lead to subtle bugs where the visibility of the View we are " + + "launching from is not what we expected." + ) + } + // Make sure we don't run the launch animation from the same view twice at the same time. if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) { Log.e(TAG, "Not running dialog launch animation as there is already one running") @@ -156,9 +169,14 @@ constructor( openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground ?: throw IllegalStateException( "The animateFrom dialog was not animated using " + - "DialogLaunchAnimator.showFrom(View|Dialog)") + "DialogLaunchAnimator.showFrom(View|Dialog)" + ) showFromView( - dialog, view, animateBackgroundBoundsChange = animateBackgroundBoundsChange, cuj = cuj) + dialog, + view, + animateBackgroundBoundsChange = animateBackgroundBoundsChange, + cuj = cuj + ) } /** @@ -197,7 +215,7 @@ constructor( // bouncer. if ( !dialog.isShowing || - (!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock()) + (!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock()) ) { return null } @@ -556,11 +574,12 @@ private class AnimatedDialog( window.setDecorFitsSystemWindows(false) val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup) viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets -> - val type = if (wasFittingNavigationBars) { - WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars() - } else { - WindowInsets.Type.displayCutout() - } + val type = + if (wasFittingNavigationBars) { + WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars() + } else { + WindowInsets.Type.displayCutout() + } val insets = windowInsets.getInsets(type) view.setPadding(insets.left, insets.top, insets.right, insets.bottom) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt new file mode 100644 index 000000000000..8ce372dbb278 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import android.view.View + +/** A piece of UI that can be expanded into a Dialog or an Activity. */ +interface Expandable { + /** + * Create an [ActivityLaunchAnimator.Controller] that can be used to expand this [Expandable] + * into an Activity, or return `null` if this [Expandable] should not be animated (e.g. if it is + * currently not attached or visible). + * + * @param cujType the CUJ type from the [com.android.internal.jank.InteractionJankMonitor] + * associated to the launch that will use this controller. + */ + fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller? + + // TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here. + + companion object { + /** + * Create an [Expandable] that will animate [view] when expanded. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be properly + * animated. + */ + fun fromView(view: View): Expandable { + return object : Expandable { + override fun activityLaunchController( + cujType: Int?, + ): ActivityLaunchAnimator.Controller? { + return ActivityLaunchAnimator.Controller.fromView(view, cujType) + } + } + } + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt index 7499302c06b2..67b59e0e9928 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchableView.kt @@ -16,15 +16,79 @@ package com.android.systemui.animation +import android.view.View + /** A view that can expand/launch into an app or a dialog. */ interface LaunchableView { /** - * Set whether this view should block/prevent all visibility changes. This ensures that this - * view remains invisible during the launch animation given that it is ghosted and already drawn + * Set whether this view should block/postpone all visibility changes. This ensures that this + * view: + * - remains invisible during the launch animation given that it is ghosted and already drawn * somewhere else. + * - remains invisible as long as a dialog expanded from it is shown. + * - restores its expected visibility once the dialog expanded from it is dismissed. * * Note that when this is set to true, both the [normal][android.view.View.setVisibility] and * [transition][android.view.View.setTransitionVisibility] visibility changes must be blocked. + * + * @param block whether we should block/postpone all calls to `setVisibility` and + * `setTransitionVisibility`. */ fun setShouldBlockVisibilityChanges(block: Boolean) } + +/** A delegate that can be used by views to make the implementation of [LaunchableView] easier. */ +class LaunchableViewDelegate( + private val view: View, + + /** + * The lambda that should set the actual visibility of [view], usually by calling + * super.setVisibility(visibility). + */ + private val superSetVisibility: (Int) -> Unit, + + /** + * The lambda that should set the actual transition visibility of [view], usually by calling + * super.setTransitionVisibility(visibility). + */ + private val superSetTransitionVisibility: (Int) -> Unit, +) { + private var blockVisibilityChanges = false + private var lastVisibility = view.visibility + + /** Call this when [LaunchableView.setShouldBlockVisibilityChanges] is called. */ + fun setShouldBlockVisibilityChanges(block: Boolean) { + if (block == blockVisibilityChanges) { + return + } + + blockVisibilityChanges = block + if (block) { + lastVisibility = view.visibility + } else { + superSetVisibility(lastVisibility) + } + } + + /** Call this when [View.setVisibility] is called. */ + fun setVisibility(visibility: Int) { + if (blockVisibilityChanges) { + lastVisibility = visibility + return + } + + superSetVisibility(visibility) + } + + /** Call this when [View.setTransitionVisibility] is called. */ + fun setTransitionVisibility(visibility: Int) { + if (blockVisibilityChanges) { + // View.setTransitionVisibility just sets the visibility flag, so we don't have to save + // the transition visibility separately from the normal visibility. + lastVisibility = visibility + return + } + + superSetTransitionVisibility(visibility) + } +} diff --git a/packages/SystemUI/compose/gallery/Android.bp b/packages/SystemUI/compose/gallery/Android.bp index b0f5cc112120..5a7a1e1807a3 100644 --- a/packages/SystemUI/compose/gallery/Android.bp +++ b/packages/SystemUI/compose/gallery/Android.bp @@ -54,6 +54,11 @@ android_library { "testables", "truth-prebuilt", "androidx.test.uiautomator", + "kotlinx_coroutines_test", + ], + + libs: [ + "android.test.mock", ], kotlincflags: ["-Xjvm-default=all"], diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/qs/footer/Fakes.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/qs/footer/Fakes.kt new file mode 100644 index 000000000000..11477f9d833b --- /dev/null +++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/qs/footer/Fakes.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer + +import android.content.Context +import android.os.UserHandle +import android.view.View +import com.android.internal.util.UserIcons +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.classifier.FalsingManagerFake +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.globalactions.GlobalActionsDialogLite +import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel +import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor +import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel +import com.android.systemui.util.mockito.mock +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** A list of fake [FooterActionsViewModel] to be used in screenshot tests and the gallery. */ +fun fakeFooterActionsViewModels( + @Application context: Context, +): List<FooterActionsViewModel> { + return listOf( + fakeFooterActionsViewModel(context), + fakeFooterActionsViewModel(context, showPowerButton = false, isGuestUser = true), + fakeFooterActionsViewModel(context, showUserSwitcher = false), + fakeFooterActionsViewModel(context, showUserSwitcher = false, foregroundServices = 4), + fakeFooterActionsViewModel( + context, + foregroundServices = 4, + hasNewForegroundServices = true, + userId = 1, + ), + fakeFooterActionsViewModel( + context, + securityText = "Security", + foregroundServices = 4, + showUserSwitcher = false, + ), + fakeFooterActionsViewModel( + context, + securityText = "Security (not clickable)", + securityClickable = false, + foregroundServices = 4, + hasNewForegroundServices = true, + userId = 2, + ), + ) +} + +private fun fakeFooterActionsViewModel( + @Application context: Context, + securityText: String? = null, + securityClickable: Boolean = true, + foregroundServices: Int = 0, + hasNewForegroundServices: Boolean = false, + showUserSwitcher: Boolean = true, + showPowerButton: Boolean = true, + userId: Int = UserHandle.USER_OWNER, + isGuestUser: Boolean = false, +): FooterActionsViewModel { + val interactor = + FakeFooterActionsInteractor( + securityButtonConfig = + flowOf( + securityText?.let { text -> + SecurityButtonConfig( + icon = Icon.Resource(R.drawable.ic_info_outline), + text = text, + isClickable = securityClickable, + ) + } + ), + foregroundServicesCount = flowOf(foregroundServices), + hasNewForegroundServices = flowOf(hasNewForegroundServices), + userSwitcherStatus = + flowOf( + if (showUserSwitcher) { + UserSwitcherStatusModel.Enabled( + currentUserName = "foo", + currentUserImage = + UserIcons.getDefaultUserIcon( + context.resources, + userId, + /* light= */ false, + ), + isGuestUser = isGuestUser, + ) + } else { + UserSwitcherStatusModel.Disabled + } + ), + deviceMonitoringDialogRequests = flowOf(), + ) + + return FooterActionsViewModel( + context, + interactor, + FalsingManagerFake(), + globalActionsDialogLite = mock(), + showPowerButton = showPowerButton, + ) +} + +private class FakeFooterActionsInteractor( + override val securityButtonConfig: Flow<SecurityButtonConfig?> = flowOf(null), + override val foregroundServicesCount: Flow<Int> = flowOf(0), + override val hasNewForegroundServices: Flow<Boolean> = flowOf(false), + override val userSwitcherStatus: Flow<UserSwitcherStatusModel> = + flowOf(UserSwitcherStatusModel.Disabled), + override val deviceMonitoringDialogRequests: Flow<Unit> = flowOf(), + private val onShowDeviceMonitoringDialogFromView: (View) -> Unit = {}, + private val onShowDeviceMonitoringDialog: (Context) -> Unit = {}, + private val onShowForegroundServicesDialog: (View) -> Unit = {}, + private val onShowPowerMenuDialog: (GlobalActionsDialogLite, View) -> Unit = { _, _ -> }, + private val onShowSettings: (Expandable) -> Unit = {}, + private val onShowUserSwitcher: (View) -> Unit = {}, +) : FooterActionsInteractor { + override fun showDeviceMonitoringDialog(view: View) { + onShowDeviceMonitoringDialogFromView(view) + } + + override fun showDeviceMonitoringDialog(quickSettingsContext: Context) { + onShowDeviceMonitoringDialog(quickSettingsContext) + } + + override fun showForegroundServicesDialog(view: View) { + onShowForegroundServicesDialog(view) + } + + override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) { + onShowPowerMenuDialog(globalActionsDialogLite, view) + } + + override fun showSettings(expandable: Expandable) { + onShowSettings(expandable) + } + + override fun showUserSwitcher(view: View) { + onShowUserSwitcher(view) + } +} diff --git a/packages/SystemUI/res-keyguard/layout/fgs_footer.xml b/packages/SystemUI/res-keyguard/layout/fgs_footer.xml index 6757acf7014a..ee588f997ab8 100644 --- a/packages/SystemUI/res-keyguard/layout/fgs_footer.xml +++ b/packages/SystemUI/res-keyguard/layout/fgs_footer.xml @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> +<!-- TODO(b/242040009): Remove this file. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="0dp" diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml index a7e61029bfdb..1ce106ed2156 100644 --- a/packages/SystemUI/res-keyguard/layout/footer_actions.xml +++ b/packages/SystemUI/res-keyguard/layout/footer_actions.xml @@ -16,16 +16,17 @@ --> <!-- Action buttons for footer in QS/QQS, containing settings button, power off button etc --> +<!-- TODO(b/242040009): Clean up this file. --> <com.android.systemui.qs.FooterActionsView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:layout_width="match_parent" android:layout_height="@dimen/footer_actions_height" android:elevation="@dimen/qs_panel_elevation" - android:paddingTop="8dp" + android:paddingTop="@dimen/qs_footer_actions_top_padding" android:paddingBottom="@dimen/qs_footer_actions_bottom_padding" android:background="@drawable/qs_footer_actions_background" - android:gravity="center_vertical" + android:gravity="center_vertical|end" android:layout_gravity="bottom" > diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml new file mode 100644 index 000000000000..fad41c822ec0 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<com.android.systemui.statusbar.AlphaOptimizedFrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/qs_footer_action_button_size" + android:layout_height="@dimen/qs_footer_action_button_size" + android:visibility="gone"> + <ImageView + android:id="@+id/icon" + android:layout_width="@dimen/qs_footer_icon_size" + android:layout_height="@dimen/qs_footer_icon_size" + android:layout_gravity="center" + android:scaleType="centerInside" /> +</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml new file mode 100644 index 000000000000..a7ffe9ca256f --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<com.android.systemui.statusbar.AlphaOptimizedFrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/qs_footer_action_button_size" + android:layout_height="@dimen/qs_footer_action_button_size" + android:background="@drawable/qs_footer_action_circle" + android:visibility="gone"> + <TextView + android:id="@+id/number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.QS.SecurityFooter" + android:layout_gravity="center" + android:textColor="?android:attr/textColorPrimary" + android:textSize="18sp"/> + <ImageView + android:id="@+id/new_dot" + android:layout_width="12dp" + android:layout_height="12dp" + android:scaleType="fitCenter" + android:layout_gravity="bottom|end" + android:src="@drawable/fgs_dot" + android:contentDescription="@string/fgs_dot_content_description" /> +</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml new file mode 100644 index 000000000000..fc18132d4dc3 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<com.android.systemui.common.ui.view.LaunchableLinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="0dp" + android:layout_height="@dimen/qs_security_footer_single_line_height" + android:layout_weight="1" + android:orientation="horizontal" + android:paddingHorizontal="@dimen/qs_footer_padding" + android:gravity="center_vertical" + android:layout_marginEnd="@dimen/qs_footer_action_inset" + android:background="@drawable/qs_security_footer_background" + android:visibility="gone"> + <ImageView + android:id="@+id/icon" + android:layout_width="@dimen/qs_footer_icon_size" + android:layout_height="@dimen/qs_footer_icon_size" + android:gravity="start" + android:layout_marginEnd="12dp" + android:contentDescription="@null" + android:src="@drawable/ic_info_outline" + android:tint="?android:attr/textColorSecondary" /> + + <TextView + android:id="@+id/text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:maxLines="1" + android:ellipsize="end" + android:textAppearance="@style/TextAppearance.QS.SecurityFooter" + android:textColor="?android:attr/textColorSecondary"/> + + <ImageView + android:id="@+id/new_dot" + android:layout_width="12dp" + android:layout_height="12dp" + android:scaleType="fitCenter" + android:src="@drawable/fgs_dot" + android:contentDescription="@string/fgs_dot_content_description" + /> + + <ImageView + android:id="@+id/chevron_icon" + android:layout_width="@dimen/qs_footer_icon_size" + android:layout_height="@dimen/qs_footer_icon_size" + android:layout_marginStart="8dp" + android:contentDescription="@null" + android:src="@*android:drawable/ic_chevron_end" + android:autoMirrored="true" + android:tint="?android:attr/textColorSecondary" /> +</com.android.systemui.common.ui.view.LaunchableLinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml index 3bf44a4b85a4..6ba88a66977a 100644 --- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml +++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<TextClock +<com.android.systemui.dreams.complication.DoubleShadowTextClock xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/time_view" android:layout_width="wrap_content" @@ -23,8 +23,6 @@ android:textColor="@android:color/white" android:format12Hour="@string/dream_time_complication_12_hr_time_format" android:format24Hour="@string/dream_time_complication_24_hr_time_format" - android:shadowColor="@color/keyguard_shadow_color" - android:shadowRadius="?attr/shadowRadius" android:fontFeatureSettings="pnum, lnum" android:letterSpacing="0.02" android:textSize="@dimen/dream_overlay_complication_clock_time_text_size"/> diff --git a/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml b/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml new file mode 100644 index 000000000000..65cf81ea416b --- /dev/null +++ b/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. +--> + +<!-- This XML is served to be overridden by other OEMs/device types. --> +<com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:systemui="http://schemas.android.com/apk/res-auto" + android:id="@+id/notification_stack_scroller" + android:layout_marginTop="@dimen/notification_panel_margin_top" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_marginHorizontal="@dimen/notification_panel_margin_horizontal" + android:layout_marginBottom="@dimen/notification_panel_margin_bottom" + android:importantForAccessibility="no" + systemui:layout_constraintStart_toStartOf="parent" + systemui:layout_constraintEnd_toEndOf="parent" +/> diff --git a/packages/SystemUI/res/layout/qs_user_detail_item.xml b/packages/SystemUI/res/layout/qs_user_detail_item.xml index 0c847ed588e8..7c86bc77aa95 100644 --- a/packages/SystemUI/res/layout/qs_user_detail_item.xml +++ b/packages/SystemUI/res/layout/qs_user_detail_item.xml @@ -49,7 +49,8 @@ android:id="@+id/user_name" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center_horizontal" /> + android:gravity="center_horizontal" + android:hyphenationFrequency="full"/> <ImageView android:id="@+id/restricted_padlock" android:layout_width="@dimen/qs_tile_text_size" diff --git a/packages/SystemUI/res/layout/quick_settings_security_footer.xml b/packages/SystemUI/res/layout/quick_settings_security_footer.xml index 1b11816465ac..194f3dd5dc26 100644 --- a/packages/SystemUI/res/layout/quick_settings_security_footer.xml +++ b/packages/SystemUI/res/layout/quick_settings_security_footer.xml @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> +<!-- TODO(b/242040009): Remove this file. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="0dp" diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml index 6423a50fc107..f0e49d5c2011 100644 --- a/packages/SystemUI/res/layout/status_bar_expanded.xml +++ b/packages/SystemUI/res/layout/status_bar_expanded.xml @@ -109,17 +109,10 @@ systemui:layout_constraintGuide_percent="0.5" android:orientation="vertical"/> - <com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout - android:id="@+id/notification_stack_scroller" - android:layout_marginTop="@dimen/notification_panel_margin_top" - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_marginHorizontal="@dimen/notification_panel_margin_horizontal" - android:layout_marginBottom="@dimen/notification_panel_margin_bottom" - android:importantForAccessibility="no" - systemui:layout_constraintStart_toStartOf="parent" - systemui:layout_constraintEnd_toEndOf="parent" - /> + <!-- This layout should always include a version of + NotificationStackScrollLayout, as it is expected from + NotificationPanelViewController. --> + <include layout="@layout/notification_stack_scroll_layout" /> <include layout="@layout/photo_preview_overlay" /> diff --git a/packages/SystemUI/res/raw/fingerprint_dialogue_error_to_success_lottie.json b/packages/SystemUI/res/raw/fingerprint_dialogue_error_to_success_lottie.json new file mode 100644 index 000000000000..c5ed827de0d1 --- /dev/null +++ b/packages/SystemUI/res/raw/fingerprint_dialogue_error_to_success_lottie.json @@ -0,0 +1 @@ +{"v":"5.9.0","fr":60,"ip":0,"op":21,"w":80,"h":80,"nm":"RearFPS_error_to_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[28,47,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".red200","cl":"red200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[39.95,40,0],"ix":2,"l":2},"a":{"a":0,"k":[30,30,0],"ix":1,"l":2},"s":{"a":0,"k":[120,120,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-1.721,-7.982],[1.721,-7.982],[1.721,7.5],[-1.721,7.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.949019607843,0.721568627451,0.709803921569,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[30.002,32.488],"ix":2},"a":{"a":0,"k":[0.002,7.488],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.659,0.6],"y":[1,1]},"o":{"x":[0.8,0.8],"y":[0,0]},"t":0,"s":[100,100]},{"i":{"x":[0.6,0.92],"y":[1,1.096]},"o":{"x":[0.8,0.8],"y":[0,0]},"t":4,"s":[100,110]},{"t":10,"s":[100,0]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Top!","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-1.681,-1.25],[1.681,-1.25],[1.681,2.213],[-1.681,2.213]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.949019607843,0.721568627451,0.709803921569,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[30,38.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.6,0.6],"y":[1,1]},"o":{"x":[0.853,0.853],"y":[0,0]},"t":0,"s":[100,100]},{"i":{"x":[0.92,0.92],"y":[1.06,1.06]},"o":{"x":[0.8,0.8],"y":[0,0]},"t":4,"s":[110,110]},{"t":10,"s":[0,0]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Bottom!","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":86,"st":-30,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":21,"st":10,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".red200","cl":"red200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[100]},{"t":10,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[100]},{"t":10,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.949019607843,0.721568627451,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[4]},{"t":10,"s":[0]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":10,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/fingerprint_dialogue_fingerprint_to_success_lottie.json b/packages/SystemUI/res/raw/fingerprint_dialogue_fingerprint_to_success_lottie.json new file mode 100644 index 000000000000..3eb95ef1a718 --- /dev/null +++ b/packages/SystemUI/res/raw/fingerprint_dialogue_fingerprint_to_success_lottie.json @@ -0,0 +1 @@ +{"v":"5.9.0","fr":60,"ip":0,"op":21,"w":80,"h":80,"nm":"RearFPS_fingerprint_to_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[28,47,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":21,"st":10,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.091,40,0],"ix":2,"l":2},"a":{"a":0,"k":[19.341,24.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.701,0.42],[-1.757,0],[-1.577,-0.381],[-1.485,-0.816]],"o":[[1.455,-0.799],[1.608,-0.397],[1.719,0],[1.739,0.42],[0,0]],"v":[[-9.818,1.227],[-5.064,-0.618],[0,-1.227],[4.96,-0.643],[9.818,1.227]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450980392,0.890196078431,0.992156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[19.341,7.477],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Top","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-2.446,1.161],[-1.168,0.275],[-1.439,0],[-1.301,-0.304],[-1.225,-0.66],[-1.11,-1.844]],"o":[[1.23,-2.044],[1.024,-0.486],[1.312,-0.31],[1.425,0],[1.454,0.34],[2.122,1.143],[0,0]],"v":[[-13.091,3.273],[-7.438,-1.646],[-4.14,-2.797],[0,-3.273],[4.104,-2.805],[8.141,-1.29],[13.091,3.273]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450980392,0.890196078431,0.992156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[19.341,16.069],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Mid Top","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-6.53,0],[0,-5.793],[0,0],[2.159,0],[0.59,1.489],[0,0],[1.587,0],[0,-2.16],[-0.81,-1.363],[-0.844,-0.674],[0,0]],"o":[[-0.753,-2.095],[0,-5.793],[6.529,0],[0,0],[0,2.16],[-1.604,0],[0,0],[-0.589,-1.489],[-2.161,0],[0,1.62],[0.54,0.909],[0,0],[0,0]],"v":[[-10.702,5.728],[-11.454,1.506],[0.001,-9],[11.454,1.506],[11.454,1.817],[7.544,5.728],[3.926,3.273],[2.618,0],[-0.997,-2.454],[-4.91,1.457],[-3.657,6.014],[-1.57,8.412],[-0.818,9]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450980392,0.890196078431,0.992156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[19.341,28.341],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Inside to dot ","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[1.307,-0.561],[0.894,-0.16],[0.706,0],[0.844,0.193],[0.728,0.334],[0.967,0.901]],"o":[[-1.038,0.967],[-0.817,0.351],[-0.673,0.12],[-0.9,0],[-0.794,-0.182],[-1.203,-0.551],[0,0]],"v":[[8.182,-0.386],[4.642,1.931],[2.07,2.703],[-0.001,2.886],[-2.621,2.591],[-4.909,1.813],[-8.182,-0.386]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450980392,0.890196078431,0.992156862745,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[19.341,40.614],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Bottom","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".grey700","cl":"grey700","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 1eece4cee179..db2ac436229f 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -241,4 +241,6 @@ <color name="dream_overlay_aqi_very_unhealthy">#AD1457</color> <color name="dream_overlay_aqi_hazardous">#880E4F</color> <color name="dream_overlay_aqi_unknown">#BDC1C6</color> + <color name="dream_overlay_clock_key_text_shadow_color">#4D000000</color> + <color name="dream_overlay_clock_ambient_text_shadow_color">#4D000000</color> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 229858413f99..c9776dd58788 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -380,6 +380,7 @@ <!-- (48dp - 40dp) / 2 --> <dimen name="qs_footer_action_inset">4dp</dimen> + <dimen name="qs_footer_actions_top_padding">8dp</dimen> <dimen name="qs_footer_actions_bottom_padding">4dp</dimen> <dimen name="qs_footer_action_inset_negative">-4dp</dimen> @@ -1539,4 +1540,10 @@ <dimen name="broadcast_dialog_btn_text_size">16sp</dimen> <dimen name="broadcast_dialog_btn_minHeight">44dp</dimen> <dimen name="broadcast_dialog_margin">16dp</dimen> + <dimen name="dream_overlay_clock_key_text_shadow_dx">0dp</dimen> + <dimen name="dream_overlay_clock_key_text_shadow_dy">0dp</dimen> + <dimen name="dream_overlay_clock_key_text_shadow_radius">5dp</dimen> + <dimen name="dream_overlay_clock_ambient_text_shadow_dx">0dp</dimen> + <dimen name="dream_overlay_clock_ambient_text_shadow_dy">0dp</dimen> + <dimen name="dream_overlay_clock_ambient_text_shadow_radius">1dp</dimen> </resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt index 0146795f4988..dd2e55d4e7d7 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt @@ -35,6 +35,7 @@ open class RegionSamplingInstance( ) { private var isDark = RegionDarkness.DEFAULT private var samplingBounds = Rect() + private val tmpScreenLocation = IntArray(2) @VisibleForTesting var regionSampler: RegionSamplingHelper? = null /** @@ -99,10 +100,21 @@ open class RegionSamplingInstance( isDark = convertToClockDarkness(isRegionDark) updateFun.updateColors() } - + /** + * The method getLocationOnScreen is used to obtain the view coordinates + * relative to its left and top edges on the device screen. + * Directly accessing the X and Y coordinates of the view returns the + * location relative to its parent view instead. + */ override fun getSampledRegion(sampledView: View): Rect { - samplingBounds = Rect(sampledView.left, sampledView.top, - sampledView.right, sampledView.bottom) + val screenLocation = tmpScreenLocation + sampledView.getLocationOnScreen(screenLocation) + val left = screenLocation[0] + val top = screenLocation[1] + samplingBounds.left = left + samplingBounds.top = top + samplingBounds.right = left + sampledView.width + samplingBounds.bottom = top + sampledView.height return samplingBounds } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt index 589ec0e72b3b..9b5f54a0a91d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt @@ -92,7 +92,7 @@ open class AuthBiometricFingerprintIconController( STATE_ERROR -> true STATE_AUTHENTICATING_ANIMATING_IN, STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP - STATE_AUTHENTICATED -> false + STATE_AUTHENTICATED -> true else -> false } @@ -114,7 +114,13 @@ open class AuthBiometricFingerprintIconController( R.raw.fingerprint_dialogue_fingerprint_to_error_lottie } } - STATE_AUTHENTICATED -> R.raw.fingerprint_dialogue_fingerprint_to_error_lottie + STATE_AUTHENTICATED -> { + if (oldState == STATE_ERROR || oldState == STATE_HELP) { + R.raw.fingerprint_dialogue_error_to_success_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_success_lottie + } + } else -> return null } return if (id != null) return id else null diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.kt index 31baa0ff1154..9cce066afe9d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.kt @@ -75,7 +75,7 @@ open class AuthBiometricFingerprintView( } } - override fun getDelayAfterAuthenticatedDurationMs() = 0 + override fun getDelayAfterAuthenticatedDurationMs() = 500 override fun getStateForAfterError() = STATE_AUTHENTICATING diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index e866b9c0bb25..fc5cf9f005ed 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -468,6 +468,7 @@ public abstract class AuthBiometricView extends LinearLayout { break; case STATE_AUTHENTICATED: + removePendingAnimations(); if (mSize != AuthDialog.SIZE_SMALL) { mConfirmButton.setVisibility(View.GONE); mNegativeButton.setVisibility(View.GONE); diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt index d757b629c829..eb8cb47c2671 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt @@ -31,6 +31,8 @@ import android.util.SparseArray import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager @@ -38,6 +40,8 @@ import com.android.systemui.settings.UserTracker import java.io.PrintWriter import java.util.concurrent.Executor import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow data class ReceiverData( val receiver: BroadcastReceiver, @@ -153,6 +157,55 @@ open class BroadcastDispatcher @Inject constructor( .sendToTarget() } + /** + * Returns a [Flow] that, when collected, emits a new value whenever a broadcast matching + * [filter] is received. The value will be computed from the intent and the registered receiver + * using [map]. + * + * @see registerReceiver + */ + @JvmOverloads + fun <T> broadcastFlow( + filter: IntentFilter, + user: UserHandle? = null, + @Context.RegisterReceiverFlags flags: Int = Context.RECEIVER_EXPORTED, + permission: String? = null, + map: (Intent, BroadcastReceiver) -> T, + ): Flow<T> = conflatedCallbackFlow { + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + trySendWithFailureLogging(map(intent, this), TAG) + } + } + + registerReceiver( + receiver, + filter, + bgExecutor, + user, + flags, + permission, + ) + + awaitClose { + unregisterReceiver(receiver) + } + } + + /** + * Returns a [Flow] that, when collected, emits `Unit` whenever a broadcast matching [filter] is + * received. + * + * @see registerReceiver + */ + @JvmOverloads + fun broadcastFlow( + filter: IntentFilter, + user: UserHandle? = null, + @Context.RegisterReceiverFlags flags: Int = Context.RECEIVER_EXPORTED, + permission: String? = null, + ): Flow<Unit> = broadcastFlow(filter, user, flags, permission) { _, _ -> Unit } + private fun checkFilter(filter: IntentFilter) { val sb = StringBuilder() if (filter.countActions() == 0) sb.append("Filter must contain at least one action. ") diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt index 81da80233d42..4fe2dd810a08 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt @@ -33,10 +33,10 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.ActivityIntentHelper import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.shade.NotificationPanelViewController import com.android.systemui.shared.system.ActivityManagerKt.isInForeground import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.CentralSurfaces -import com.android.systemui.shade.PanelViewController import com.android.systemui.statusbar.policy.KeyguardStateController import java.util.concurrent.Executor import javax.inject.Inject @@ -117,7 +117,7 @@ class CameraGestureHelper @Inject constructor( ) } catch (e: RemoteException) { Log.w( - PanelViewController.TAG, + NotificationPanelViewController.TAG, "Unable to start camera activity", e ) diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt new file mode 100644 index 000000000000..bebade0cc484 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.shared.model + +import android.annotation.StringRes + +/** + * Models a content description, that can either be already [loaded][ContentDescription.Loaded] or + * be a [reference][ContentDescription.Resource] to a resource. + */ +sealed class ContentDescription { + data class Loaded( + val description: String?, + ) : ContentDescription() + + data class Resource( + @StringRes val res: Int, + ) : ContentDescription() +} diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt new file mode 100644 index 000000000000..0b65966ca109 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.shared.model + +import android.annotation.DrawableRes +import android.graphics.drawable.Drawable + +/** + * Models an icon, that can either be already [loaded][Icon.Loaded] or be a [reference] + * [Icon.Resource] to a resource. + */ +sealed class Icon { + data class Loaded( + val drawable: Drawable, + ) : Icon() + + data class Resource( + @DrawableRes val res: Int, + ) : Icon() +} diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/binder/ContentDescriptionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/common/ui/binder/ContentDescriptionViewBinder.kt new file mode 100644 index 000000000000..d6433aae9845 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/binder/ContentDescriptionViewBinder.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.ui.binder + +import android.view.View +import com.android.systemui.common.shared.model.ContentDescription + +object ContentDescriptionViewBinder { + fun bind( + contentDescription: ContentDescription, + view: View, + ) { + when (contentDescription) { + is ContentDescription.Loaded -> view.contentDescription = contentDescription.description + is ContentDescription.Resource -> { + view.contentDescription = view.context.resources.getString(contentDescription.res) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt new file mode 100644 index 000000000000..aecee2afc9d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.ui.binder + +import android.widget.ImageView +import com.android.systemui.common.shared.model.Icon + +object IconViewBinder { + fun bind( + icon: Icon, + view: ImageView, + ) { + when (icon) { + is Icon.Loaded -> view.setImageDrawable(icon.drawable) + is Icon.Resource -> view.setImageResource(icon.res) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt new file mode 100644 index 000000000000..c27b82aeeb47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableLinearLayout.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate + +/** A [LinearLayout] that also implements [LaunchableView]. */ +class LaunchableLinearLayout : LinearLayout, LaunchableView { + private val delegate = + LaunchableViewDelegate( + this, + superSetVisibility = { super.setVisibility(it) }, + superSetTransitionVisibility = { super.setTransitionVisibility(it) }, + ) + + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + delegate.setVisibility(visibility) + } + + override fun setTransitionVisibility(visibility: Int) { + delegate.setTransitionVisibility(visibility) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt index f611c3ef966d..5e8ce6db971c 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import android.view.ViewStub import android.widget.Button import android.widget.TextView +import androidx.activity.ComponentActivity import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView @@ -36,7 +37,6 @@ import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.settings.CurrentUserTracker -import com.android.systemui.util.LifecycleActivity import javax.inject.Inject /** @@ -47,7 +47,7 @@ class ControlsEditingActivity @Inject constructor( private val broadcastDispatcher: BroadcastDispatcher, private val customIconCache: CustomIconCache, private val uiController: ControlsUiController -) : LifecycleActivity() { +) : ComponentActivity() { companion object { private const val TAG = "ControlsEditingActivity" diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt index dca52a9678b9..be572c503bda 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt @@ -32,6 +32,7 @@ import android.widget.Button import android.widget.FrameLayout import android.widget.TextView import android.widget.Toast +import androidx.activity.ComponentActivity import androidx.viewpager2.widget.ViewPager2 import com.android.systemui.Prefs import com.android.systemui.R @@ -44,7 +45,6 @@ import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.CurrentUserTracker -import com.android.systemui.util.LifecycleActivity import java.text.Collator import java.util.concurrent.Executor import java.util.function.Consumer @@ -56,7 +56,7 @@ class ControlsFavoritingActivity @Inject constructor( private val listingController: ControlsListingController, private val broadcastDispatcher: BroadcastDispatcher, private val uiController: ControlsUiController -) : LifecycleActivity() { +) : ComponentActivity() { companion object { private const val TAG = "ControlsFavoritingActivity" diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt index 8ad5099dc42d..b26615fe4702 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt @@ -26,6 +26,7 @@ import android.view.ViewGroup import android.view.ViewStub import android.widget.Button import android.widget.TextView +import androidx.activity.ComponentActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver @@ -37,7 +38,6 @@ import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.CurrentUserTracker -import com.android.systemui.util.LifecycleActivity import java.util.concurrent.Executor import javax.inject.Inject @@ -51,7 +51,7 @@ class ControlsProviderSelectorActivity @Inject constructor( private val controlsController: ControlsController, private val broadcastDispatcher: BroadcastDispatcher, private val uiController: ControlsUiController -) : LifecycleActivity() { +) : ComponentActivity() { companion object { private const val TAG = "ControlsProviderSelectorActivity" diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt index f9e7f0e921f3..b376455ee815 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt @@ -30,6 +30,7 @@ import android.view.LayoutInflater import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.activity.ComponentActivity import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.ControlsServiceInfo @@ -38,14 +39,13 @@ import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.ui.RenderInfo import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.util.LifecycleActivity import javax.inject.Inject open class ControlsRequestDialog @Inject constructor( private val controller: ControlsController, private val broadcastDispatcher: BroadcastDispatcher, private val controlsListingController: ControlsListingController -) : LifecycleActivity(), DialogInterface.OnClickListener, DialogInterface.OnCancelListener { +) : ComponentActivity(), DialogInterface.OnClickListener, DialogInterface.OnCancelListener { companion object { private const val TAG = "ControlsRequestDialog" diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt index 49f758405fbd..77b65233c112 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -25,11 +25,10 @@ import android.view.View import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowInsets.Type - +import androidx.activity.ComponentActivity import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.management.ControlsAnimations -import com.android.systemui.util.LifecycleActivity import javax.inject.Inject /** @@ -42,7 +41,7 @@ import javax.inject.Inject class ControlsActivity @Inject constructor( private val uiController: ControlsUiController, private val broadcastDispatcher: BroadcastDispatcher -) : LifecycleActivity() { +) : ComponentActivity() { private lateinit var parent: ViewGroup private lateinit var broadcastReceiver: BroadcastReceiver diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index e549a96079bd..1b060e209579 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -49,8 +49,12 @@ import com.android.systemui.navigationbar.NavigationBarComponent; import com.android.systemui.people.PeopleModule; import com.android.systemui.plugins.BcSmartspaceDataPlugin; import com.android.systemui.privacy.PrivacyModule; +import com.android.systemui.qs.FgsManagerController; +import com.android.systemui.qs.FgsManagerControllerImpl; +import com.android.systemui.qs.footer.dagger.FooterActionsModule; import com.android.systemui.recents.Recents; import com.android.systemui.screenshot.dagger.ScreenshotModule; +import com.android.systemui.security.data.repository.SecurityRepositoryModule; import com.android.systemui.settings.dagger.MultiUserUtilsModule; import com.android.systemui.shade.ShadeController; import com.android.systemui.smartspace.dagger.SmartspaceModule; @@ -122,6 +126,7 @@ import dagger.Provides; DemoModeModule.class, FalsingModule.class, FlagsModule.class, + FooterActionsModule.class, LogModule.class, MediaProjectionModule.class, PeopleHubModule.class, @@ -132,6 +137,7 @@ import dagger.Provides; ScreenshotModule.class, SensorModule.class, MultiUserUtilsModule.class, + SecurityRepositoryModule.class, SettingsUtilModule.class, SmartRepliesInflationModule.class, SmartspaceModule.class, @@ -258,4 +264,7 @@ public abstract class SystemUIModule { return Optional.empty(); } } + + @Binds + abstract FgsManagerController bindFgsManagerController(FgsManagerControllerImpl impl); } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java new file mode 100644 index 000000000000..653f4dc66200 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DoubleShadowTextClock.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.complication; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.TextClock; + +import com.android.systemui.R; + +/** + * Extension of {@link TextClock} which draws two shadows on the text (ambient and key shadows) + */ +public class DoubleShadowTextClock extends TextClock { + private final float mAmbientShadowBlur; + private final int mAmbientShadowColor; + private final float mKeyShadowBlur; + private final float mKeyShadowOffsetX; + private final float mKeyShadowOffsetY; + private final int mKeyShadowColor; + private final float mAmbientShadowOffsetX; + private final float mAmbientShadowOffsetY; + + public DoubleShadowTextClock(Context context) { + this(context, null); + } + + public DoubleShadowTextClock(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DoubleShadowTextClock(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mKeyShadowBlur = context.getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_radius); + mKeyShadowOffsetX = context.getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_dx); + mKeyShadowOffsetY = context.getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_clock_key_text_shadow_dy); + mKeyShadowColor = context.getResources().getColor( + R.color.dream_overlay_clock_key_text_shadow_color); + mAmbientShadowBlur = context.getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_radius); + mAmbientShadowColor = context.getResources().getColor( + R.color.dream_overlay_clock_ambient_text_shadow_color); + mAmbientShadowOffsetX = context.getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_dx); + mAmbientShadowOffsetY = context.getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_clock_ambient_text_shadow_dy); + } + + @Override + public void onDraw(Canvas canvas) { + // We enhance the shadow by drawing the shadow twice + getPaint().setShadowLayer(mAmbientShadowBlur, mAmbientShadowOffsetX, mAmbientShadowOffsetY, + mAmbientShadowColor); + super.onDraw(canvas); + canvas.save(); + canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(), + getScrollX() + getWidth(), + getScrollY() + getHeight()); + + getPaint().setShadowLayer( + mKeyShadowBlur, mKeyShadowOffsetX, mKeyShadowOffsetY, mKeyShadowColor); + super.onDraw(canvas); + canvas.restore(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/BouncerSwipeModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/BouncerSwipeModule.java index 9c22dc61e67b..081bab085843 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/BouncerSwipeModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/BouncerSwipeModule.java @@ -25,7 +25,7 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.touch.BouncerSwipeTouchHandler; import com.android.systemui.dreams.touch.DreamTouchHandler; -import com.android.systemui.shade.PanelViewController; +import com.android.systemui.shade.NotificationPanelViewController; import com.android.wm.shell.animation.FlingAnimationUtils; import javax.inject.Named; @@ -77,8 +77,9 @@ public class BouncerSwipeModule { Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilderProvider) { return flingAnimationUtilsBuilderProvider.get() .reset() - .setMaxLengthSeconds(PanelViewController.FLING_CLOSING_MAX_LENGTH_SECONDS) - .setSpeedUpFactor(PanelViewController.FLING_SPEED_UP_FACTOR) + .setMaxLengthSeconds( + NotificationPanelViewController.FLING_CLOSING_MAX_LENGTH_SECONDS) + .setSpeedUpFactor(NotificationPanelViewController.FLING_SPEED_UP_FACTOR) .build(); } @@ -91,8 +92,8 @@ public class BouncerSwipeModule { Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilderProvider) { return flingAnimationUtilsBuilderProvider.get() .reset() - .setMaxLengthSeconds(PanelViewController.FLING_MAX_LENGTH_SECONDS) - .setSpeedUpFactor(PanelViewController.FLING_SPEED_UP_FACTOR) + .setMaxLengthSeconds(NotificationPanelViewController.FLING_MAX_LENGTH_SECONDS) + .setSpeedUpFactor(NotificationPanelViewController.FLING_SPEED_UP_FACTOR) .build(); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 0ee53cd68dbe..d8423c3c530c 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -103,6 +103,10 @@ public class Flags { */ public static final ReleasedFlag MODERN_BOUNCER = new ReleasedFlag(208); + /** Whether UserSwitcherActivity should use modern architecture. */ + public static final UnreleasedFlag MODERN_USER_SWITCHER_ACTIVITY = + new UnreleasedFlag(209, true); + /***************************************/ // 300 - power menu public static final ReleasedFlag POWER_MENU_LITE = @@ -147,6 +151,8 @@ public class Flags { public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER = new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher); + public static final UnreleasedFlag NEW_FOOTER_ACTIONS = new UnreleasedFlag(507, true); + /***************************************/ // 600- status bar public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER = diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 6ac3eadb838d..7c4c64c20089 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -814,8 +814,10 @@ public class EdgeBackGestureHandler extends CurrentUserTracker } mLogGesture = false; String logPackageName = ""; + Map<String, Integer> vocab = mVocab; // Due to privacy, only top 100 most used apps by all users can be logged. - if (mUseMLModel && mVocab.containsKey(mPackageName) && mVocab.get(mPackageName) < 100) { + if (mUseMLModel && vocab != null && vocab.containsKey(mPackageName) + && vocab.get(mPackageName) < 100) { logPackageName = mPackageName; } SysUiStatsLog.write(SysUiStatsLog.BACK_GESTURE_REPORTED_REPORTED, backType, diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 0288c9fce64a..482a1397642b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -40,11 +40,13 @@ import android.widget.Button import android.widget.ImageView import android.widget.TextView import androidx.annotation.GuardedBy +import androidx.annotation.VisibleForTesting import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_FOOTER_DOT +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.Dumpable import com.android.systemui.R @@ -66,9 +68,73 @@ import java.util.Objects import java.util.concurrent.Executor import javax.inject.Inject import kotlin.math.max +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** A controller for the dealing with services running in the foreground. */ +interface FgsManagerController { + /** Whether the TaskManager (and therefore this controller) is actually available. */ + val isAvailable: StateFlow<Boolean> + + /** The number of packages with a service running in the foreground. */ + val numRunningPackages: Int + + /** + * Whether there were new changes to the foreground services since the last [shown][showDialog] + * dialog was dismissed. + */ + val newChangesSinceDialogWasDismissed: Boolean + + /** + * Whether we should show a dot to indicate when [newChangesSinceDialogWasDismissed] is true. + */ + val showFooterDot: StateFlow<Boolean> + + /** + * Initialize this controller. This should be called once, before this controller is used for + * the first time. + */ + fun init() + + /** + * Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if + * it's not `null`. + */ + fun showDialog(viewLaunchedFrom: View?) + + /** Add a [OnNumberOfPackagesChangedListener]. */ + fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) + + /** Remove a [OnNumberOfPackagesChangedListener]. */ + fun removeOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) + + /** Add a [OnDialogDismissedListener]. */ + fun addOnDialogDismissedListener(listener: OnDialogDismissedListener) + + /** Remove a [OnDialogDismissedListener]. */ + fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener) + + /** Whether we should update the footer visibility. */ + // TODO(b/242040009): Remove this. + fun shouldUpdateFooterVisibility(): Boolean + + @VisibleForTesting + fun visibleButtonsCount(): Int + + interface OnNumberOfPackagesChangedListener { + /** Called when [numRunningPackages] changed. */ + fun onNumberOfPackagesChanged(numPackages: Int) + } + + interface OnDialogDismissedListener { + /** Called when a dialog shown using [showDialog] was dismissed. */ + fun onDialogDismissed() + } +} @SysUISingleton -class FgsManagerController @Inject constructor( +class FgsManagerControllerImpl @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor, @Background private val backgroundExecutor: Executor, @@ -80,22 +146,32 @@ class FgsManagerController @Inject constructor( private val dialogLaunchAnimator: DialogLaunchAnimator, private val broadcastDispatcher: BroadcastDispatcher, private val dumpManager: DumpManager -) : IForegroundServiceObserver.Stub(), Dumpable { +) : IForegroundServiceObserver.Stub(), Dumpable, FgsManagerController { companion object { private const val INTERACTION_JANK_TAG = "active_background_apps" - private val LOG_TAG = FgsManagerController::class.java.simpleName private const val DEFAULT_TASK_MANAGER_ENABLED = true private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false + private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true } - var changesSinceDialog = false + override var newChangesSinceDialogWasDismissed = false private set - var isAvailable = false - private set - var showFooterDot = false - private set + val _isAvailable = MutableStateFlow(false) + override val isAvailable: StateFlow<Boolean> = _isAvailable.asStateFlow() + + val _showFooterDot = MutableStateFlow(false) + override val showFooterDot: StateFlow<Boolean> = _showFooterDot.asStateFlow() + + private var showStopBtnForUserAllowlistedApps = false + + override val numRunningPackages: Int + get() { + synchronized(lock) { + return getNumVisiblePackagesLocked() + } + } private val lock = Any() @@ -133,15 +209,7 @@ class FgsManagerController @Inject constructor( } } - interface OnNumberOfPackagesChangedListener { - fun onNumberOfPackagesChanged(numPackages: Int) - } - - interface OnDialogDismissedListener { - fun onDialogDismissed() - } - - fun init() { + override fun init() { synchronized(lock) { if (initialized) { return @@ -160,19 +228,26 @@ class FgsManagerController @Inject constructor( NAMESPACE_SYSTEMUI, backgroundExecutor ) { - isAvailable = it.getBoolean(TASK_MANAGER_ENABLED, isAvailable) - showFooterDot = - it.getBoolean(TASK_MANAGER_SHOW_FOOTER_DOT, showFooterDot) + _isAvailable.value = it.getBoolean(TASK_MANAGER_ENABLED, _isAvailable.value) + _showFooterDot.value = + it.getBoolean(TASK_MANAGER_SHOW_FOOTER_DOT, _showFooterDot.value) + showStopBtnForUserAllowlistedApps = it.getBoolean( + TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS, + showStopBtnForUserAllowlistedApps) } - isAvailable = deviceConfigProxy.getBoolean( + _isAvailable.value = deviceConfigProxy.getBoolean( NAMESPACE_SYSTEMUI, TASK_MANAGER_ENABLED, DEFAULT_TASK_MANAGER_ENABLED ) - showFooterDot = deviceConfigProxy.getBoolean( + _showFooterDot.value = deviceConfigProxy.getBoolean( NAMESPACE_SYSTEMUI, TASK_MANAGER_SHOW_FOOTER_DOT, DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT ) + showStopBtnForUserAllowlistedApps = deviceConfigProxy.getBoolean( + NAMESPACE_SYSTEMUI, + TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS, + DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS) dumpManager.registerDumpable(this) @@ -220,42 +295,45 @@ class FgsManagerController @Inject constructor( } @GuardedBy("lock") - val onNumberOfPackagesChangedListeners: MutableSet<OnNumberOfPackagesChangedListener> = - mutableSetOf() + private val onNumberOfPackagesChangedListeners = + mutableSetOf<FgsManagerController.OnNumberOfPackagesChangedListener>() @GuardedBy("lock") - val onDialogDismissedListeners: MutableSet<OnDialogDismissedListener> = mutableSetOf() + private val onDialogDismissedListeners = + mutableSetOf<FgsManagerController.OnDialogDismissedListener>() - fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) { + override fun addOnNumberOfPackagesChangedListener( + listener: FgsManagerController.OnNumberOfPackagesChangedListener + ) { synchronized(lock) { onNumberOfPackagesChangedListeners.add(listener) } } - fun removeOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) { + override fun removeOnNumberOfPackagesChangedListener( + listener: FgsManagerController.OnNumberOfPackagesChangedListener + ) { synchronized(lock) { onNumberOfPackagesChangedListeners.remove(listener) } } - fun addOnDialogDismissedListener(listener: OnDialogDismissedListener) { + override fun addOnDialogDismissedListener( + listener: FgsManagerController.OnDialogDismissedListener + ) { synchronized(lock) { onDialogDismissedListeners.add(listener) } } - fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener) { + override fun removeOnDialogDismissedListener( + listener: FgsManagerController.OnDialogDismissedListener + ) { synchronized(lock) { onDialogDismissedListeners.remove(listener) } } - fun getNumRunningPackages(): Int { - synchronized(lock) { - return getNumVisiblePackagesLocked() - } - } - private fun getNumVisiblePackagesLocked(): Int { return runningServiceTokens.keys.count { it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId) @@ -266,7 +344,7 @@ class FgsManagerController @Inject constructor( val num = getNumVisiblePackagesLocked() if (num != lastNumberOfVisiblePackages) { lastNumberOfVisiblePackages = num - changesSinceDialog = true + newChangesSinceDialogWasDismissed = true onNumberOfPackagesChangedListeners.forEach { backgroundExecutor.execute { it.onNumberOfPackagesChanged(num) @@ -275,9 +353,21 @@ class FgsManagerController @Inject constructor( } } - fun shouldUpdateFooterVisibility() = dialog == null + override fun visibleButtonsCount(): Int { + synchronized(lock) { + return getNumVisibleButtonsLocked() + } + } + + private fun getNumVisibleButtonsLocked(): Int { + return runningServiceTokens.keys.count { + it.uiControl != UIControl.HIDE_BUTTON && currentProfileIds.contains(it.userId) + } + } - fun showDialog(viewLaunchedFrom: View?) { + override fun shouldUpdateFooterVisibility() = dialog == null + + override fun showDialog(viewLaunchedFrom: View?) { synchronized(lock) { if (dialog == null) { @@ -302,7 +392,7 @@ class FgsManagerController @Inject constructor( this.dialog = dialog dialog.setOnDismissListener { - changesSinceDialog = false + newChangesSinceDialogWasDismissed = false synchronized(lock) { this.dialog = null updateAppItemsLocked() @@ -505,6 +595,13 @@ class FgsManagerController @Inject constructor( PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI, PowerExemptionManager.REASON_ROLE_DIALER, PowerExemptionManager.REASON_SYSTEM_MODULE -> UIControl.HIDE_BUTTON + + PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE -> + if (showStopBtnForUserAllowlistedApps) { + UIControl.NORMAL + } else { + UIControl.HIDE_BUTTON + } else -> UIControl.NORMAL } uiControlInitialized = true @@ -623,7 +720,7 @@ class FgsManagerController @Inject constructor( val pw = IndentingPrintWriter(printwriter) synchronized(lock) { pw.println("current user profiles = $currentProfileIds") - pw.println("changesSinceDialog=$changesSinceDialog") + pw.println("newChangesSinceDialogWasShown=$newChangesSinceDialogWasDismissed") pw.println("Running service tokens: [") pw.indentIfPossible { runningServiceTokens.forEach { (userPackage, startTimeAndTokens) -> diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt index c790cfe7b7b7..9d64781ef2e9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt @@ -56,6 +56,7 @@ import javax.inject.Provider * determined by [buttonsVisibleState] */ @QSScope +// TODO(b/242040009): Remove this file. internal class FooterActionsController @Inject constructor( view: FooterActionsView, multiUserSwitchControllerFactory: MultiUserSwitchController.Factory, diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt index 309ac2a66e6b..d602b0b27977 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt @@ -38,6 +38,7 @@ import com.android.systemui.statusbar.phone.MultiUserSwitch * in split shade mode visible also in collapsed state. May contain up to 5 buttons: settings, * edit tiles, power off and conditionally: user switch and tuner */ +// TODO(b/242040009): Remove this file. class FooterActionsView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) { private lateinit var settingsContainer: View private lateinit var multiUserSwitch: MultiUserSwitch diff --git a/packages/SystemUI/src/com/android/systemui/qs/NewFooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/NewFooterActionsController.kt new file mode 100644 index 000000000000..7c67d9f42b55 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/NewFooterActionsController.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs + +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +/** Controller for the footer actions. This manages the initialization of its dependencies. */ +@SysUISingleton +class NewFooterActionsController +@Inject +// TODO(b/242040009): Rename this to FooterActionsController. +constructor( + private val fgsManagerController: FgsManagerController, +) { + fun init() { + fgsManagerController.init() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java index 875493d73f9e..7511278e0919 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java @@ -41,6 +41,7 @@ import javax.inject.Named; /** * Footer entry point for the foreground service manager */ +// TODO(b/242040009): Remove this file. @QSScope public class QSFgsManagerFooter implements View.OnClickListener, FgsManagerController.OnDialogDismissedListener, @@ -149,9 +150,11 @@ public class QSFgsManagerFooter implements View.OnClickListener, mNumberView.setContentDescription(text); if (mFgsManagerController.shouldUpdateFooterVisibility()) { mRootView.setVisibility(mNumPackages > 0 - && mFgsManagerController.isAvailable() ? View.VISIBLE : View.GONE); - int dotVis = mFgsManagerController.getShowFooterDot() - && mFgsManagerController.getChangesSinceDialog() ? View.VISIBLE : View.GONE; + && mFgsManagerController.isAvailable().getValue() ? View.VISIBLE + : View.GONE); + int dotVis = mFgsManagerController.getShowFooterDot().getValue() + && mFgsManagerController.getNewChangesSinceDialogWasDismissed() + ? View.VISIBLE : View.GONE; mDotView.setVisibility(dotVis); mCollapsedDotView.setVisibility(dotVis); if (mVisibilityChangedListener != null) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 139fb8b0bc14..05b3eae1d2f2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -36,6 +36,9 @@ import android.view.ViewTreeObserver; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.systemui.Dumpable; @@ -43,6 +46,8 @@ import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; @@ -50,6 +55,8 @@ import com.android.systemui.plugins.qs.QSContainerController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSFragmentComponent; +import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; @@ -104,6 +111,10 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private final QSFragmentComponent.Factory mQsComponentFactory; private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger; private final QSTileHost mHost; + private final FeatureFlags mFeatureFlags; + private final NewFooterActionsController mNewFooterActionsController; + private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory; + private final ListeningAndVisibilityLifecycleOwner mListeningAndVisibilityLifecycleOwner; private boolean mShowCollapsedOnKeyguard; private boolean mLastKeyguardAndExpanded; /** @@ -119,8 +130,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private QSPanelController mQSPanelController; private QuickQSPanelController mQuickQSPanelController; private QSCustomizerController mQSCustomizerController; + @Nullable private FooterActionsController mQSFooterActionController; @Nullable + private FooterActionsViewModel mQSFooterActionsViewModel; + @Nullable private ScrollListener mScrollListener; /** * When true, QS will translate from outside the screen. It will be clipped with parallax @@ -161,7 +175,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca KeyguardBypassController keyguardBypassController, QSFragmentComponent.Factory qsComponentFactory, QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger, - FalsingManager falsingManager, DumpManager dumpManager) { + FalsingManager falsingManager, DumpManager dumpManager, FeatureFlags featureFlags, + NewFooterActionsController newFooterActionsController, + FooterActionsViewModel.Factory footerActionsViewModelFactory) { mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler; mQsMediaHost = qsMediaHost; mQqsMediaHost = qqsMediaHost; @@ -173,6 +189,10 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mBypassController = keyguardBypassController; mStatusBarStateController = statusBarStateController; mDumpManager = dumpManager; + mFeatureFlags = featureFlags; + mNewFooterActionsController = newFooterActionsController; + mFooterActionsViewModelFactory = footerActionsViewModelFactory; + mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner(); } @Override @@ -193,11 +213,22 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this); mQSPanelController = qsFragmentComponent.getQSPanelController(); mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController(); - mQSFooterActionController = qsFragmentComponent.getQSFooterActionController(); mQSPanelController.init(); mQuickQSPanelController.init(); - mQSFooterActionController.init(); + + if (mFeatureFlags.isEnabled(Flags.NEW_FOOTER_ACTIONS)) { + mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */ + this); + FooterActionsView footerActionsView = view.findViewById(R.id.qs_footer_actions); + FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel, + mListeningAndVisibilityLifecycleOwner); + + mNewFooterActionsController.init(); + } else { + mQSFooterActionController = qsFragmentComponent.getQSFooterActionController(); + mQSFooterActionController.init(); + } mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view); mQSPanelScrollView.addOnLayoutChangeListener( @@ -283,6 +314,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mDumpManager.unregisterDumpable(mContainer.getClass().getName()); } mDumpManager.unregisterDumpable(getClass().getName()); + mListeningAndVisibilityLifecycleOwner.destroy(); } @Override @@ -395,7 +427,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mContainer.disable(state1, state2, animate); mHeader.disable(state1, state2, animate); mFooter.disable(state1, state2, animate); - mQSFooterActionController.disable(state2); + if (mQSFooterActionController != null) { + mQSFooterActionController.disable(state2); + } updateQsState(); } @@ -415,7 +449,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca boolean footerVisible = qsPanelVisible && (expanded || !keyguardShowing || mHeaderAnimating || mShowCollapsedOnKeyguard); mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); - mQSFooterActionController.setVisible(footerVisible); + if (mQSFooterActionController != null) { + mQSFooterActionController.setVisible(footerVisible); + } else { + mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible); + } mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) || (expanded && !mStackScrollerOverscrolling)); mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE); @@ -482,7 +520,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } mFooter.setKeyguardShowing(keyguardShowing); - mQSFooterActionController.setKeyguardShowing(keyguardShowing); + if (mQSFooterActionController != null) { + mQSFooterActionController.setKeyguardShowing(keyguardShowing); + } updateQsState(); } @@ -498,7 +538,10 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (DEBUG) Log.d(TAG, "setListening " + listening); mListening = listening; mQSContainerImplController.setListening(listening && mQsVisible); - mQSFooterActionController.setListening(listening && mQsVisible); + if (mQSFooterActionController != null) { + mQSFooterActionController.setListening(listening && mQsVisible); + } + mListeningAndVisibilityLifecycleOwner.updateState(); updateQsPanelControllerListening(); } @@ -511,6 +554,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (DEBUG) Log.d(TAG, "setQsVisible " + visible); mQsVisible = visible; setListening(mListening); + mListeningAndVisibilityLifecycleOwner.updateState(); } @Override @@ -602,7 +646,12 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion); float footerActionsExpansion = onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion; - mQSFooterActionController.setExpansion(footerActionsExpansion); + if (mQSFooterActionController != null) { + mQSFooterActionController.setExpansion(footerActionsExpansion); + } else { + mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion, + mInSplitShade); + } mQSPanelController.setRevealExpansion(expansion); mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); @@ -714,7 +763,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca boolean customizing = isCustomizing(); mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); - mQSFooterActionController.setVisible(!customizing); + if (mQSFooterActionController != null) { + mQSFooterActionController.setVisible(!customizing); + } else { + mQSFooterActionsViewModel.onVisibilityChangeRequested(!customizing); + } mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); // Let the panel know the position changed and it needs to update where notifications // and whatnot are. @@ -860,4 +913,56 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } return "GONE"; } + + /** + * A {@link LifecycleOwner} whose state is driven by the current state of this fragment: + * + * - DESTROYED when the fragment is destroyed. + * - CREATED when mListening == mQsVisible == false. + * - STARTED when mListening == true && mQsVisible == false. + * - RESUMED when mListening == true && mQsVisible == true. + */ + private class ListeningAndVisibilityLifecycleOwner implements LifecycleOwner { + private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this); + private boolean mDestroyed = false; + + { + updateState(); + } + + @Override + public Lifecycle getLifecycle() { + return mLifecycleRegistry; + } + + /** + * Update the state of the associated lifecycle. This should be called whenever + * {@code mListening} or {@code mQsVisible} is changed. + */ + public void updateState() { + if (mDestroyed) { + mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED); + return; + } + + if (!mListening) { + mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED); + return; + } + + // mListening && !mQsVisible. + if (!mQsVisible) { + mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED); + return; + } + + // mListening && mQsVisible. + mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED); + } + + public void destroy() { + mDestroyed = true; + updateState(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index 87fcce455ea6..b20d7ba33397 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -15,126 +15,66 @@ */ package com.android.systemui.qs; -import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_CA_CERT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NAMED_VPN; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NETWORK; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TITLE; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_CA_CERT_SUBTITLE; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_NETWORK_SUBTITLE; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_VPN_SUBTITLE; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_NAMED_MANAGEMENT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_VIEW_POLICIES; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_CA_CERT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NAMED_VPN; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NETWORK; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MONITORING; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MULTIPLE_VPNS; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_NAMED_VPN; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MONITORING; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_NAMED_VPN; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_WORK_PROFILE_MONITORING; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_PERSONAL_PROFILE_NAMED_VPN; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_MONITORING; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NAMED_VPN; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NETWORK; - import static com.android.systemui.qs.dagger.QSFragmentModule.QS_SECURITY_FOOTER_VIEW; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.admin.DeviceAdminInfo; import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.UserInfo; import android.content.res.Resources; -import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.UserHandle; -import android.os.UserManager; -import android.provider.Settings; -import android.text.SpannableStringBuilder; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; -import android.view.Window; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.util.FrameworkStatsLog; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; -import com.android.systemui.animation.DialogCuj; -import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.dagger.QSScope; -import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.phone.SystemUIDialog; +import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig; +import com.android.systemui.security.data.model.SecurityModel; import com.android.systemui.statusbar.policy.SecurityController; import com.android.systemui.util.ViewController; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; - import javax.inject.Inject; import javax.inject.Named; +/** ViewController for the footer actions. */ +// TODO(b/242040009): Remove this class. @QSScope -class QSSecurityFooter extends ViewController<View> - implements OnClickListener, DialogInterface.OnClickListener, - VisibilityChangedDispatcher { +public class QSSecurityFooter extends ViewController<View> + implements OnClickListener, VisibilityChangedDispatcher { protected static final String TAG = "QSSecurityFooter"; - protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final boolean DEBUG_FORCE_VISIBLE = false; - - private static final String INTERACTION_JANK_TAG = "managed_device_info"; private final TextView mFooterText; private final ImageView mPrimaryFooterIcon; private Context mContext; - private final DevicePolicyManager mDpm; private final Callback mCallback = new Callback(); private final SecurityController mSecurityController; - private final ActivityStarter mActivityStarter; private final Handler mMainHandler; - private final UserTracker mUserTracker; - private final DialogLaunchAnimator mDialogLaunchAnimator; private final BroadcastDispatcher mBroadcastDispatcher; + private final QSSecurityFooterUtils mQSSecurityFooterUtils; - private final AtomicBoolean mShouldUseSettingsButton = new AtomicBoolean(false); - - private AlertDialog mDialog; protected H mHandler; private boolean mIsVisible; + private boolean mIsClickable; @Nullable private CharSequence mFooterTextContent = null; - private int mFooterIconId; - @Nullable - private Drawable mPrimaryFooterIconDrawable; + private Icon mFooterIcon; @Nullable private VisibilityChangedDispatcher.OnVisibilityChangedListener mVisibilityChangedListener; @@ -149,82 +89,21 @@ class QSSecurityFooter extends ViewController<View> } }; - private Supplier<String> mManagementTitleSupplier = () -> - mContext == null ? null : mContext.getString(R.string.monitoring_title_device_owned); - - private Supplier<String> mManagementMessageSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.quick_settings_disclosure_management); - - private Supplier<String> mManagementMonitoringStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.quick_settings_disclosure_management_monitoring); - - private Supplier<String> mManagementMultipleVpnStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.quick_settings_disclosure_management_vpns); - - private Supplier<String> mWorkProfileMonitoringStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.quick_settings_disclosure_managed_profile_monitoring); - - private Supplier<String> mWorkProfileNetworkStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.quick_settings_disclosure_managed_profile_network_activity); - - private Supplier<String> mMonitoringSubtitleCaCertStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.monitoring_subtitle_ca_certificate); - - private Supplier<String> mMonitoringSubtitleNetworkStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.monitoring_subtitle_network_logging); - - private Supplier<String> mMonitoringSubtitleVpnStringSupplier = () -> - mContext == null ? null : mContext.getString(R.string.monitoring_subtitle_vpn); - - private Supplier<String> mViewPoliciesButtonStringSupplier = () -> - mContext == null ? null : mContext.getString(R.string.monitoring_button_view_policies); - - private Supplier<String> mManagementDialogStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.monitoring_description_management); - - private Supplier<String> mManagementDialogCaCertStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.monitoring_description_management_ca_certificate); - - private Supplier<String> mWorkProfileDialogCaCertStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.monitoring_description_managed_profile_ca_certificate); - - private Supplier<String> mManagementDialogNetworkStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.monitoring_description_management_network_logging); - - private Supplier<String> mWorkProfileDialogNetworkStringSupplier = () -> - mContext == null ? null : mContext.getString( - R.string.monitoring_description_managed_profile_network_logging); - @Inject QSSecurityFooter(@Named(QS_SECURITY_FOOTER_VIEW) View rootView, - UserTracker userTracker, @Main Handler mainHandler, - ActivityStarter activityStarter, SecurityController securityController, - DialogLaunchAnimator dialogLaunchAnimator, @Background Looper bgLooper, - BroadcastDispatcher broadcastDispatcher) { + @Main Handler mainHandler, SecurityController securityController, + @Background Looper bgLooper, BroadcastDispatcher broadcastDispatcher, + QSSecurityFooterUtils qSSecurityFooterUtils) { super(rootView); mFooterText = mView.findViewById(R.id.footer_text); mPrimaryFooterIcon = mView.findViewById(R.id.primary_footer_icon); - mFooterIconId = R.drawable.ic_info_outline; + mFooterIcon = new Icon.Resource(R.drawable.ic_info_outline); mContext = rootView.getContext(); - mDpm = rootView.getContext().getSystemService(DevicePolicyManager.class); - mMainHandler = mainHandler; - mActivityStarter = activityStarter; mSecurityController = securityController; + mMainHandler = mainHandler; mHandler = new H(bgLooper); - mUserTracker = userTracker; - mDialogLaunchAnimator = dialogLaunchAnimator; mBroadcastDispatcher = broadcastDispatcher; + mQSSecurityFooterUtils = qSSecurityFooterUtils; } @Override @@ -287,8 +166,9 @@ class QSSecurityFooter extends ViewController<View> .write(); } + // TODO(b/242040009): Remove this. public void showDeviceMonitoringDialog() { - createDialog(); + mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView); } public void refreshState() { @@ -296,590 +176,30 @@ class QSSecurityFooter extends ViewController<View> } private void handleRefreshState() { - final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); - final UserInfo currentUser = mUserTracker.getUserInfo(); - final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null - && currentUser.isDemo(); - final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); - final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser(); - final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile(); - final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled(); - final String vpnName = mSecurityController.getPrimaryVpnName(); - final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); - final CharSequence organizationName = mSecurityController.getDeviceOwnerOrganizationName(); - final CharSequence workProfileOrganizationName = - mSecurityController.getWorkProfileOrganizationName(); - final boolean isProfileOwnerOfOrganizationOwnedDevice = - mSecurityController.isProfileOwnerOfOrganizationOwnedDevice(); - final boolean isParentalControlsEnabled = mSecurityController.isParentalControlsEnabled(); - final boolean isWorkProfileOn = mSecurityController.isWorkProfileOn(); - final boolean hasDisclosableWorkProfilePolicy = hasCACertsInWorkProfile - || vpnNameWorkProfile != null || (hasWorkProfile && isNetworkLoggingEnabled); - // Update visibility of footer - mIsVisible = (isDeviceManaged && !isDemoDevice) - || hasCACerts - || vpnName != null - || isProfileOwnerOfOrganizationOwnedDevice - || isParentalControlsEnabled - || (hasDisclosableWorkProfilePolicy && isWorkProfileOn); - // Update the view to be untappable if the device is an organization-owned device with a - // managed profile and there is either: - // a) no policy set which requires a privacy disclosure. - // b) a specific work policy set but the work profile is turned off. - if (mIsVisible && isProfileOwnerOfOrganizationOwnedDevice - && (!hasDisclosableWorkProfilePolicy || !isWorkProfileOn)) { - mView.setClickable(false); - mView.findViewById(R.id.footer_icon).setVisibility(View.GONE); - } else { - mView.setClickable(true); - mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE); - } - // Update the string - mFooterTextContent = getFooterText(isDeviceManaged, hasWorkProfile, - hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, - vpnNameWorkProfile, organizationName, workProfileOrganizationName, - isProfileOwnerOfOrganizationOwnedDevice, isParentalControlsEnabled, - isWorkProfileOn); - // Update the icon - int footerIconId = R.drawable.ic_info_outline; - if (vpnName != null || vpnNameWorkProfile != null) { - if (mSecurityController.isVpnBranded()) { - footerIconId = R.drawable.stat_sys_branded_vpn; - } else { - footerIconId = R.drawable.stat_sys_vpn_ic; - } - } - if (mFooterIconId != footerIconId) { - mFooterIconId = footerIconId; - } + SecurityModel securityModel = SecurityModel.create(mSecurityController); + SecurityButtonConfig buttonConfig = mQSSecurityFooterUtils.getButtonConfig(securityModel); - // Update the primary icon - if (isParentalControlsEnabled) { - if (mPrimaryFooterIconDrawable == null) { - DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo(); - mPrimaryFooterIconDrawable = mSecurityController.getIcon(info); - } + if (buttonConfig == null) { + mIsVisible = false; } else { - mPrimaryFooterIconDrawable = null; + mIsVisible = true; + mIsClickable = buttonConfig.isClickable(); + mFooterTextContent = buttonConfig.getText(); + mFooterIcon = buttonConfig.getIcon(); } - mMainHandler.post(mUpdatePrimaryIcon); + // Update the UI. + mMainHandler.post(mUpdatePrimaryIcon); mMainHandler.post(mUpdateDisplayState); } - @Nullable - protected CharSequence getFooterText(boolean isDeviceManaged, boolean hasWorkProfile, - boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, - String vpnName, String vpnNameWorkProfile, CharSequence organizationName, - CharSequence workProfileOrganizationName, - boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isParentalControlsEnabled, - boolean isWorkProfileOn) { - if (isParentalControlsEnabled) { - return mContext.getString(R.string.quick_settings_disclosure_parental_controls); - } - if (isDeviceManaged || DEBUG_FORCE_VISIBLE) { - return getManagedDeviceFooterText(hasCACerts, hasCACertsInWorkProfile, - isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, organizationName); - } - return getManagedAndPersonalProfileFooterText(hasWorkProfile, hasCACerts, - hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, - workProfileOrganizationName, isProfileOwnerOfOrganizationOwnedDevice, - isWorkProfileOn); - } - - private String getManagedDeviceFooterText( - boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, - String vpnName, String vpnNameWorkProfile, CharSequence organizationName) { - if (hasCACerts || hasCACertsInWorkProfile || isNetworkLoggingEnabled) { - return getManagedDeviceMonitoringText(organizationName); - } - if (vpnName != null || vpnNameWorkProfile != null) { - return getManagedDeviceVpnText(vpnName, vpnNameWorkProfile, organizationName); - } - return getMangedDeviceGeneralText(organizationName); - } - - private String getManagedDeviceMonitoringText(CharSequence organizationName) { - if (organizationName == null) { - return mDpm.getResources().getString( - QS_MSG_MANAGEMENT_MONITORING, mManagementMonitoringStringSupplier); - } - return mDpm.getResources().getString( - QS_MSG_NAMED_MANAGEMENT_MONITORING, - () -> mContext.getString( - R.string.quick_settings_disclosure_named_management_monitoring, - organizationName), - organizationName); - } - - private String getManagedDeviceVpnText( - String vpnName, String vpnNameWorkProfile, CharSequence organizationName) { - if (vpnName != null && vpnNameWorkProfile != null) { - if (organizationName == null) { - return mDpm.getResources().getString( - QS_MSG_MANAGEMENT_MULTIPLE_VPNS, mManagementMultipleVpnStringSupplier); - } - return mDpm.getResources().getString( - QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS, - () -> mContext.getString( - R.string.quick_settings_disclosure_named_management_vpns, - organizationName), - organizationName); - } - String name = vpnName != null ? vpnName : vpnNameWorkProfile; - if (organizationName == null) { - return mDpm.getResources().getString( - QS_MSG_MANAGEMENT_NAMED_VPN, - () -> mContext.getString( - R.string.quick_settings_disclosure_management_named_vpn, - name), - name); - } - return mDpm.getResources().getString( - QS_MSG_NAMED_MANAGEMENT_NAMED_VPN, - () -> mContext.getString( - R.string.quick_settings_disclosure_named_management_named_vpn, - organizationName, - name), - organizationName, - name); - } - - private String getMangedDeviceGeneralText(CharSequence organizationName) { - if (organizationName == null) { - return mDpm.getResources().getString(QS_MSG_MANAGEMENT, mManagementMessageSupplier); - } - if (isFinancedDevice()) { - return mContext.getString( - R.string.quick_settings_financed_disclosure_named_management, - organizationName); - } else { - return mDpm.getResources().getString( - QS_MSG_NAMED_MANAGEMENT, - () -> mContext.getString( - R.string.quick_settings_disclosure_named_management, - organizationName), - organizationName); - } - } - - private String getManagedAndPersonalProfileFooterText(boolean hasWorkProfile, - boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, - String vpnName, String vpnNameWorkProfile, CharSequence workProfileOrganizationName, - boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isWorkProfileOn) { - if (hasCACerts || (hasCACertsInWorkProfile && isWorkProfileOn)) { - return getMonitoringText( - hasCACerts, hasCACertsInWorkProfile, workProfileOrganizationName, - isWorkProfileOn); - } - if (vpnName != null || (vpnNameWorkProfile != null && isWorkProfileOn)) { - return getVpnText(hasWorkProfile, vpnName, vpnNameWorkProfile, isWorkProfileOn); - } - if (hasWorkProfile && isNetworkLoggingEnabled && isWorkProfileOn) { - return getManagedProfileNetworkActivityText(); - } - if (isProfileOwnerOfOrganizationOwnedDevice) { - return getMangedDeviceGeneralText(workProfileOrganizationName); - } - return null; - } - - private String getMonitoringText(boolean hasCACerts, boolean hasCACertsInWorkProfile, - CharSequence workProfileOrganizationName, boolean isWorkProfileOn) { - if (hasCACertsInWorkProfile && isWorkProfileOn) { - if (workProfileOrganizationName == null) { - return mDpm.getResources().getString( - QS_MSG_WORK_PROFILE_MONITORING, mWorkProfileMonitoringStringSupplier); - } - return mDpm.getResources().getString( - QS_MSG_NAMED_WORK_PROFILE_MONITORING, - () -> mContext.getString( - R.string.quick_settings_disclosure_named_managed_profile_monitoring, - workProfileOrganizationName), - workProfileOrganizationName); - } - if (hasCACerts) { - return mContext.getString(R.string.quick_settings_disclosure_monitoring); - } - return null; - } - - private String getVpnText(boolean hasWorkProfile, String vpnName, String vpnNameWorkProfile, - boolean isWorkProfileOn) { - if (vpnName != null && vpnNameWorkProfile != null) { - return mContext.getString(R.string.quick_settings_disclosure_vpns); - } - if (vpnNameWorkProfile != null && isWorkProfileOn) { - return mDpm.getResources().getString( - QS_MSG_WORK_PROFILE_NAMED_VPN, - () -> mContext.getString( - R.string.quick_settings_disclosure_managed_profile_named_vpn, - vpnNameWorkProfile), - vpnNameWorkProfile); - } - if (vpnName != null) { - if (hasWorkProfile) { - return mDpm.getResources().getString( - QS_MSG_PERSONAL_PROFILE_NAMED_VPN, - () -> mContext.getString( - R.string.quick_settings_disclosure_personal_profile_named_vpn, - vpnName), - vpnName); - } - return mContext.getString(R.string.quick_settings_disclosure_named_vpn, - vpnName); - } - return null; - } - - private String getManagedProfileNetworkActivityText() { - return mDpm.getResources().getString( - QS_MSG_WORK_PROFILE_NETWORK, mWorkProfileNetworkStringSupplier); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == DialogInterface.BUTTON_NEGATIVE) { - final Intent intent = new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS); - dialog.dismiss(); - // This dismisses the shade on opening the activity - mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); - } - } - - private void createDialog() { - mShouldUseSettingsButton.set(false); - mHandler.post(() -> { - String settingsButtonText = getSettingsButton(); - final View view = createDialogView(); - mMainHandler.post(() -> { - mDialog = new SystemUIDialog(mContext, 0); // Use mContext theme - mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this); - mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, mShouldUseSettingsButton.get() - ? settingsButtonText : getNegativeButton(), this); - - mDialog.setView(view); - if (mView.isAggregatedVisible()) { - mDialogLaunchAnimator.showFromView(mDialog, mView, new DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)); - } else { - mDialog.show(); - } - }); - }); - } - - @VisibleForTesting - Dialog getDialog() { - return mDialog; - } - - @VisibleForTesting - View createDialogView() { - if (mSecurityController.isParentalControlsEnabled()) { - return createParentalControlsDialogView(); - } - return createOrganizationDialogView(); - } - - private View createOrganizationDialogView() { - final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); - final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); - final CharSequence deviceOwnerOrganization = - mSecurityController.getDeviceOwnerOrganizationName(); - final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser(); - final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile(); - final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled(); - final String vpnName = mSecurityController.getPrimaryVpnName(); - final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); - - View dialogView = LayoutInflater.from(mContext) - .inflate(R.layout.quick_settings_footer_dialog, null, false); - - // device management section - TextView deviceManagementSubtitle = - dialogView.findViewById(R.id.device_management_subtitle); - deviceManagementSubtitle.setText(getManagementTitle(deviceOwnerOrganization)); - - CharSequence managementMessage = getManagementMessage(isDeviceManaged, - deviceOwnerOrganization); - if (managementMessage == null) { - dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.GONE); - } else { - dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.VISIBLE); - TextView deviceManagementWarning = - (TextView) dialogView.findViewById(R.id.device_management_warning); - deviceManagementWarning.setText(managementMessage); - mShouldUseSettingsButton.set(true); - } - - // ca certificate section - CharSequence caCertsMessage = getCaCertsMessage(isDeviceManaged, hasCACerts, - hasCACertsInWorkProfile); - if (caCertsMessage == null) { - dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.GONE); - } else { - dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.VISIBLE); - TextView caCertsWarning = (TextView) dialogView.findViewById(R.id.ca_certs_warning); - caCertsWarning.setText(caCertsMessage); - // Make "Open trusted credentials"-link clickable - caCertsWarning.setMovementMethod(new LinkMovementMethod()); - - TextView caCertsSubtitle = (TextView) dialogView.findViewById(R.id.ca_certs_subtitle); - String caCertsSubtitleMessage = mDpm.getResources().getString( - QS_DIALOG_MONITORING_CA_CERT_SUBTITLE, mMonitoringSubtitleCaCertStringSupplier); - caCertsSubtitle.setText(caCertsSubtitleMessage); - - } - - // network logging section - CharSequence networkLoggingMessage = getNetworkLoggingMessage(isDeviceManaged, - isNetworkLoggingEnabled); - if (networkLoggingMessage == null) { - dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.GONE); - } else { - dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.VISIBLE); - TextView networkLoggingWarning = - (TextView) dialogView.findViewById(R.id.network_logging_warning); - networkLoggingWarning.setText(networkLoggingMessage); - - TextView networkLoggingSubtitle = (TextView) dialogView.findViewById( - R.id.network_logging_subtitle); - String networkLoggingSubtitleMessage = mDpm.getResources().getString( - QS_DIALOG_MONITORING_NETWORK_SUBTITLE, - mMonitoringSubtitleNetworkStringSupplier); - networkLoggingSubtitle.setText(networkLoggingSubtitleMessage); - } - - // vpn section - CharSequence vpnMessage = getVpnMessage(isDeviceManaged, hasWorkProfile, vpnName, - vpnNameWorkProfile); - if (vpnMessage == null) { - dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.GONE); - } else { - dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.VISIBLE); - TextView vpnWarning = (TextView) dialogView.findViewById(R.id.vpn_warning); - vpnWarning.setText(vpnMessage); - // Make "Open VPN Settings"-link clickable - vpnWarning.setMovementMethod(new LinkMovementMethod()); - - TextView vpnSubtitle = (TextView) dialogView.findViewById(R.id.vpn_subtitle); - String vpnSubtitleMessage = mDpm.getResources().getString( - QS_DIALOG_MONITORING_VPN_SUBTITLE, mMonitoringSubtitleVpnStringSupplier); - vpnSubtitle.setText(vpnSubtitleMessage); - } - - // Note: if a new section is added, should update configSubtitleVisibility to include - // the handling of the subtitle - configSubtitleVisibility(managementMessage != null, - caCertsMessage != null, - networkLoggingMessage != null, - vpnMessage != null, - dialogView); - - return dialogView; - } - - private View createParentalControlsDialogView() { - View dialogView = LayoutInflater.from(mContext) - .inflate(R.layout.quick_settings_footer_dialog_parental_controls, null, false); - - DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo(); - Drawable icon = mSecurityController.getIcon(info); - if (icon != null) { - ImageView imageView = (ImageView) dialogView.findViewById(R.id.parental_controls_icon); - imageView.setImageDrawable(icon); - } - - TextView parentalControlsTitle = - (TextView) dialogView.findViewById(R.id.parental_controls_title); - parentalControlsTitle.setText(mSecurityController.getLabel(info)); - - return dialogView; - } - - protected void configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts, - boolean showNetworkLogging, boolean showVpn, View dialogView) { - // Device Management title should always been shown - // When there is a Device Management message, all subtitles should be shown - if (showDeviceManagement) { - return; - } - // Hide the subtitle if there is only 1 message shown - int mSectionCountExcludingDeviceMgt = 0; - if (showCaCerts) { mSectionCountExcludingDeviceMgt++; } - if (showNetworkLogging) { mSectionCountExcludingDeviceMgt++; } - if (showVpn) { mSectionCountExcludingDeviceMgt++; } - - // No work needed if there is no sections or more than 1 section - if (mSectionCountExcludingDeviceMgt != 1) { - return; - } - if (showCaCerts) { - dialogView.findViewById(R.id.ca_certs_subtitle).setVisibility(View.GONE); - } - if (showNetworkLogging) { - dialogView.findViewById(R.id.network_logging_subtitle).setVisibility(View.GONE); - } - if (showVpn) { - dialogView.findViewById(R.id.vpn_subtitle).setVisibility(View.GONE); - } - } - - // This should not be called on the main thread to avoid making an IPC. - @VisibleForTesting - String getSettingsButton() { - return mDpm.getResources().getString( - QS_DIALOG_VIEW_POLICIES, mViewPoliciesButtonStringSupplier); - } - - private String getPositiveButton() { - return mContext.getString(R.string.ok); - } - - @Nullable - private String getNegativeButton() { - if (mSecurityController.isParentalControlsEnabled()) { - return mContext.getString(R.string.monitoring_button_view_controls); - } - return null; - } - - @Nullable - protected CharSequence getManagementMessage(boolean isDeviceManaged, - CharSequence organizationName) { - if (!isDeviceManaged) { - return null; - } - if (organizationName != null) { - if (isFinancedDevice()) { - return mContext.getString(R.string.monitoring_financed_description_named_management, - organizationName, organizationName); - } else { - return mDpm.getResources().getString( - QS_DIALOG_NAMED_MANAGEMENT, - () -> mContext.getString( - R.string.monitoring_description_named_management, - organizationName), - organizationName); - } - } - return mDpm.getResources().getString(QS_DIALOG_MANAGEMENT, mManagementDialogStringSupplier); - } - - @Nullable - protected CharSequence getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts, - boolean hasCACertsInWorkProfile) { - if (!(hasCACerts || hasCACertsInWorkProfile)) return null; - if (isDeviceManaged) { - return mDpm.getResources().getString( - QS_DIALOG_MANAGEMENT_CA_CERT, mManagementDialogCaCertStringSupplier); - } - if (hasCACertsInWorkProfile) { - return mDpm.getResources().getString( - QS_DIALOG_WORK_PROFILE_CA_CERT, mWorkProfileDialogCaCertStringSupplier); - } - return mContext.getString(R.string.monitoring_description_ca_certificate); - } - - @Nullable - protected CharSequence getNetworkLoggingMessage(boolean isDeviceManaged, - boolean isNetworkLoggingEnabled) { - if (!isNetworkLoggingEnabled) return null; - if (isDeviceManaged) { - return mDpm.getResources().getString( - QS_DIALOG_MANAGEMENT_NETWORK, mManagementDialogNetworkStringSupplier); - } else { - return mDpm.getResources().getString( - QS_DIALOG_WORK_PROFILE_NETWORK, mWorkProfileDialogNetworkStringSupplier); - } - } - - @Nullable - protected CharSequence getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile, - String vpnName, String vpnNameWorkProfile) { - if (vpnName == null && vpnNameWorkProfile == null) return null; - final SpannableStringBuilder message = new SpannableStringBuilder(); - if (isDeviceManaged) { - if (vpnName != null && vpnNameWorkProfile != null) { - String namedVpns = mDpm.getResources().getString( - QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN, - () -> mContext.getString( - R.string.monitoring_description_two_named_vpns, - vpnName, vpnNameWorkProfile), - vpnName, vpnNameWorkProfile); - message.append(namedVpns); - } else { - String name = vpnName != null ? vpnName : vpnNameWorkProfile; - String namedVp = mDpm.getResources().getString( - QS_DIALOG_MANAGEMENT_NAMED_VPN, - () -> mContext.getString(R.string.monitoring_description_named_vpn, name), - name); - message.append(namedVp); - } - } else { - if (vpnName != null && vpnNameWorkProfile != null) { - String namedVpns = mDpm.getResources().getString( - QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN, - () -> mContext.getString( - R.string.monitoring_description_two_named_vpns, - vpnName, vpnNameWorkProfile), - vpnName, vpnNameWorkProfile); - message.append(namedVpns); - } else if (vpnNameWorkProfile != null) { - String namedVpn = mDpm.getResources().getString( - QS_DIALOG_WORK_PROFILE_NAMED_VPN, - () -> mContext.getString( - R.string.monitoring_description_managed_profile_named_vpn, - vpnNameWorkProfile), - vpnNameWorkProfile); - message.append(namedVpn); - } else if (hasWorkProfile) { - String namedVpn = mDpm.getResources().getString( - QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN, - () -> mContext.getString( - R.string.monitoring_description_personal_profile_named_vpn, - vpnName), - vpnName); - message.append(namedVpn); - } else { - message.append(mContext.getString(R.string.monitoring_description_named_vpn, - vpnName)); - } - } - message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator)); - message.append(mContext.getString(R.string.monitoring_description_vpn_settings), - new VpnSpan(), 0); - return message; - } - - @VisibleForTesting - CharSequence getManagementTitle(CharSequence deviceOwnerOrganization) { - if (deviceOwnerOrganization != null && isFinancedDevice()) { - return mContext.getString(R.string.monitoring_title_financed_device, - deviceOwnerOrganization); - } else { - return mDpm.getResources().getString( - QS_DIALOG_MANAGEMENT_TITLE, - mManagementTitleSupplier); - } - } - - private boolean isFinancedDevice() { - return mSecurityController.isDeviceManaged() - && mSecurityController.getDeviceOwnerType( - mSecurityController.getDeviceOwnerComponentOnAnyUser()) - == DEVICE_OWNER_TYPE_FINANCED; - } - private final Runnable mUpdatePrimaryIcon = new Runnable() { @Override public void run() { - if (mPrimaryFooterIconDrawable != null) { - mPrimaryFooterIcon.setImageDrawable(mPrimaryFooterIconDrawable); - } else { - mPrimaryFooterIcon.setImageResource(mFooterIconId); + if (mFooterIcon instanceof Icon.Loaded) { + mPrimaryFooterIcon.setImageDrawable(((Icon.Loaded) mFooterIcon).getDrawable()); + } else if (mFooterIcon instanceof Icon.Resource) { + mPrimaryFooterIcon.setImageResource(((Icon.Resource) mFooterIcon).getRes()); } } }; @@ -890,10 +210,18 @@ class QSSecurityFooter extends ViewController<View> if (mFooterTextContent != null) { mFooterText.setText(mFooterTextContent); } - mView.setVisibility(mIsVisible || DEBUG_FORCE_VISIBLE ? View.VISIBLE : View.GONE); + mView.setVisibility(mIsVisible ? View.VISIBLE : View.GONE); if (mVisibilityChangedListener != null) { mVisibilityChangedListener.onVisibilityChanged(mView.getVisibility()); } + + if (mIsVisible && mIsClickable) { + mView.setClickable(true); + mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE); + } else { + mView.setClickable(false); + mView.findViewById(R.id.footer_icon).setVisibility(View.GONE); + } } }; @@ -929,25 +257,4 @@ class QSSecurityFooter extends ViewController<View> } } } - - protected class VpnSpan extends ClickableSpan { - @Override - public void onClick(View widget) { - final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS); - mDialog.dismiss(); - // This dismisses the shade on opening the activity - mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); - } - - // for testing, to compare two CharSequences containing VpnSpans - @Override - public boolean equals(Object object) { - return object instanceof VpnSpan; - } - - @Override - public int hashCode() { - return 314159257; // prime - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java new file mode 100644 index 000000000000..f6322743eaa1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java @@ -0,0 +1,793 @@ +/* + * Copyright (C) 2014 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; + +import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_CA_CERT; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NAMED_VPN; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NETWORK; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TITLE; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_CA_CERT_SUBTITLE; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_NETWORK_SUBTITLE; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_VPN_SUBTITLE; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_NAMED_MANAGEMENT; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_VIEW_POLICIES; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_CA_CERT; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NAMED_VPN; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NETWORK; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MONITORING; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MULTIPLE_VPNS; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_NAMED_VPN; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MONITORING; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_NAMED_VPN; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_WORK_PROFILE_MONITORING; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_PERSONAL_PROFILE_NAMED_VPN; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_MONITORING; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NAMED_VPN; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NETWORK; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.admin.DeviceAdminInfo; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.UserInfo; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.os.UserManager; +import android.provider.Settings; +import android.text.SpannableStringBuilder; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.Window; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.jank.InteractionJankMonitor; +import com.android.systemui.R; +import com.android.systemui.animation.DialogCuj; +import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.common.shared.model.Icon; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig; +import com.android.systemui.security.data.model.SecurityModel; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.phone.SystemUIDialog; +import com.android.systemui.statusbar.policy.SecurityController; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import javax.inject.Inject; + +/** Helper class for the configuration of the QS security footer button. */ +@SysUISingleton +public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { + protected static final String TAG = "QSSecurityFooterUtils"; + protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final boolean DEBUG_FORCE_VISIBLE = false; + + private static final String INTERACTION_JANK_TAG = "managed_device_info"; + + @Application private Context mContext; + private final DevicePolicyManager mDpm; + + private final SecurityController mSecurityController; + private final ActivityStarter mActivityStarter; + private final Handler mMainHandler; + private final UserTracker mUserTracker; + private final DialogLaunchAnimator mDialogLaunchAnimator; + + private final AtomicBoolean mShouldUseSettingsButton = new AtomicBoolean(false); + + protected Handler mBgHandler; + private AlertDialog mDialog; + + private Supplier<String> mManagementTitleSupplier = () -> + mContext == null ? null : mContext.getString(R.string.monitoring_title_device_owned); + + private Supplier<String> mManagementMessageSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_management); + + private Supplier<String> mManagementMonitoringStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_management_monitoring); + + private Supplier<String> mManagementMultipleVpnStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_management_vpns); + + private Supplier<String> mWorkProfileMonitoringStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_managed_profile_monitoring); + + private Supplier<String> mWorkProfileNetworkStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_managed_profile_network_activity); + + private Supplier<String> mMonitoringSubtitleCaCertStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_subtitle_ca_certificate); + + private Supplier<String> mMonitoringSubtitleNetworkStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_subtitle_network_logging); + + private Supplier<String> mMonitoringSubtitleVpnStringSupplier = () -> + mContext == null ? null : mContext.getString(R.string.monitoring_subtitle_vpn); + + private Supplier<String> mViewPoliciesButtonStringSupplier = () -> + mContext == null ? null : mContext.getString(R.string.monitoring_button_view_policies); + + private Supplier<String> mManagementDialogStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_management); + + private Supplier<String> mManagementDialogCaCertStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_management_ca_certificate); + + private Supplier<String> mWorkProfileDialogCaCertStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_managed_profile_ca_certificate); + + private Supplier<String> mManagementDialogNetworkStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_management_network_logging); + + private Supplier<String> mWorkProfileDialogNetworkStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_managed_profile_network_logging); + + @Inject + QSSecurityFooterUtils( + @Application Context context, DevicePolicyManager devicePolicyManager, + UserTracker userTracker, @Main Handler mainHandler, ActivityStarter activityStarter, + SecurityController securityController, @Background Looper bgLooper, + DialogLaunchAnimator dialogLaunchAnimator) { + mContext = context; + mDpm = devicePolicyManager; + mUserTracker = userTracker; + mMainHandler = mainHandler; + mActivityStarter = activityStarter; + mSecurityController = securityController; + mBgHandler = new Handler(bgLooper); + mDialogLaunchAnimator = dialogLaunchAnimator; + } + + /** Show the device monitoring dialog. */ + public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) { + createDialog(quickSettingsContext, view); + } + + /** + * Return the {@link SecurityButtonConfig} of the security button, or {@code null} if no + * security button should be shown. + */ + @Nullable + public SecurityButtonConfig getButtonConfig(SecurityModel securityModel) { + final boolean isDeviceManaged = securityModel.isDeviceManaged(); + final UserInfo currentUser = mUserTracker.getUserInfo(); + final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null + && currentUser.isDemo(); + final boolean hasWorkProfile = securityModel.getHasWorkProfile(); + final boolean hasCACerts = securityModel.getHasCACertInCurrentUser(); + final boolean hasCACertsInWorkProfile = securityModel.getHasCACertInWorkProfile(); + final boolean isNetworkLoggingEnabled = securityModel.isNetworkLoggingEnabled(); + final String vpnName = securityModel.getPrimaryVpnName(); + final String vpnNameWorkProfile = securityModel.getWorkProfileVpnName(); + final CharSequence organizationName = securityModel.getDeviceOwnerOrganizationName(); + final CharSequence workProfileOrganizationName = + securityModel.getWorkProfileOrganizationName(); + final boolean isProfileOwnerOfOrganizationOwnedDevice = + securityModel.isProfileOwnerOfOrganizationOwnedDevice(); + final boolean isParentalControlsEnabled = securityModel.isParentalControlsEnabled(); + final boolean isWorkProfileOn = securityModel.isWorkProfileOn(); + final boolean hasDisclosableWorkProfilePolicy = hasCACertsInWorkProfile + || vpnNameWorkProfile != null || (hasWorkProfile && isNetworkLoggingEnabled); + // Update visibility of footer + boolean isVisible = (isDeviceManaged && !isDemoDevice) + || hasCACerts + || vpnName != null + || isProfileOwnerOfOrganizationOwnedDevice + || isParentalControlsEnabled + || (hasDisclosableWorkProfilePolicy && isWorkProfileOn); + if (!isVisible && !DEBUG_FORCE_VISIBLE) { + return null; + } + + // Update the view to be untappable if the device is an organization-owned device with a + // managed profile and there is either: + // a) no policy set which requires a privacy disclosure. + // b) a specific work policy set but the work profile is turned off. + boolean isClickable = !(isProfileOwnerOfOrganizationOwnedDevice + && (!hasDisclosableWorkProfilePolicy || !isWorkProfileOn)); + + String text = getFooterText(isDeviceManaged, hasWorkProfile, + hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, + vpnNameWorkProfile, organizationName, workProfileOrganizationName, + isProfileOwnerOfOrganizationOwnedDevice, isParentalControlsEnabled, + isWorkProfileOn).toString(); + + Icon icon; + if (isParentalControlsEnabled) { + icon = new Icon.Loaded(securityModel.getDeviceAdminIcon()); + } else if (vpnName != null || vpnNameWorkProfile != null) { + if (securityModel.isVpnBranded()) { + icon = new Icon.Resource(R.drawable.stat_sys_branded_vpn); + } else { + icon = new Icon.Resource(R.drawable.stat_sys_vpn_ic); + } + } else { + icon = new Icon.Resource(R.drawable.ic_info_outline); + } + + return new SecurityButtonConfig(icon, text, isClickable); + } + + @Nullable + protected CharSequence getFooterText(boolean isDeviceManaged, boolean hasWorkProfile, + boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, + String vpnName, String vpnNameWorkProfile, CharSequence organizationName, + CharSequence workProfileOrganizationName, + boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isParentalControlsEnabled, + boolean isWorkProfileOn) { + if (isParentalControlsEnabled) { + return mContext.getString(R.string.quick_settings_disclosure_parental_controls); + } + if (isDeviceManaged || DEBUG_FORCE_VISIBLE) { + return getManagedDeviceFooterText(hasCACerts, hasCACertsInWorkProfile, + isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, organizationName); + } + return getManagedAndPersonalProfileFooterText(hasWorkProfile, hasCACerts, + hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, + workProfileOrganizationName, isProfileOwnerOfOrganizationOwnedDevice, + isWorkProfileOn); + } + + private String getManagedDeviceFooterText( + boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, + String vpnName, String vpnNameWorkProfile, CharSequence organizationName) { + if (hasCACerts || hasCACertsInWorkProfile || isNetworkLoggingEnabled) { + return getManagedDeviceMonitoringText(organizationName); + } + if (vpnName != null || vpnNameWorkProfile != null) { + return getManagedDeviceVpnText(vpnName, vpnNameWorkProfile, organizationName); + } + return getMangedDeviceGeneralText(organizationName); + } + + private String getManagedDeviceMonitoringText(CharSequence organizationName) { + if (organizationName == null) { + return mDpm.getResources().getString( + QS_MSG_MANAGEMENT_MONITORING, mManagementMonitoringStringSupplier); + } + return mDpm.getResources().getString( + QS_MSG_NAMED_MANAGEMENT_MONITORING, + () -> mContext.getString( + R.string.quick_settings_disclosure_named_management_monitoring, + organizationName), + organizationName); + } + + private String getManagedDeviceVpnText( + String vpnName, String vpnNameWorkProfile, CharSequence organizationName) { + if (vpnName != null && vpnNameWorkProfile != null) { + if (organizationName == null) { + return mDpm.getResources().getString( + QS_MSG_MANAGEMENT_MULTIPLE_VPNS, mManagementMultipleVpnStringSupplier); + } + return mDpm.getResources().getString( + QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS, + () -> mContext.getString( + R.string.quick_settings_disclosure_named_management_vpns, + organizationName), + organizationName); + } + String name = vpnName != null ? vpnName : vpnNameWorkProfile; + if (organizationName == null) { + return mDpm.getResources().getString( + QS_MSG_MANAGEMENT_NAMED_VPN, + () -> mContext.getString( + R.string.quick_settings_disclosure_management_named_vpn, + name), + name); + } + return mDpm.getResources().getString( + QS_MSG_NAMED_MANAGEMENT_NAMED_VPN, + () -> mContext.getString( + R.string.quick_settings_disclosure_named_management_named_vpn, + organizationName, + name), + organizationName, + name); + } + + private String getMangedDeviceGeneralText(CharSequence organizationName) { + if (organizationName == null) { + return mDpm.getResources().getString(QS_MSG_MANAGEMENT, mManagementMessageSupplier); + } + if (isFinancedDevice()) { + return mContext.getString( + R.string.quick_settings_financed_disclosure_named_management, + organizationName); + } else { + return mDpm.getResources().getString( + QS_MSG_NAMED_MANAGEMENT, + () -> mContext.getString( + R.string.quick_settings_disclosure_named_management, + organizationName), + organizationName); + } + } + + private String getManagedAndPersonalProfileFooterText(boolean hasWorkProfile, + boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled, + String vpnName, String vpnNameWorkProfile, CharSequence workProfileOrganizationName, + boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isWorkProfileOn) { + if (hasCACerts || (hasCACertsInWorkProfile && isWorkProfileOn)) { + return getMonitoringText( + hasCACerts, hasCACertsInWorkProfile, workProfileOrganizationName, + isWorkProfileOn); + } + if (vpnName != null || (vpnNameWorkProfile != null && isWorkProfileOn)) { + return getVpnText(hasWorkProfile, vpnName, vpnNameWorkProfile, isWorkProfileOn); + } + if (hasWorkProfile && isNetworkLoggingEnabled && isWorkProfileOn) { + return getManagedProfileNetworkActivityText(); + } + if (isProfileOwnerOfOrganizationOwnedDevice) { + return getMangedDeviceGeneralText(workProfileOrganizationName); + } + return null; + } + + private String getMonitoringText(boolean hasCACerts, boolean hasCACertsInWorkProfile, + CharSequence workProfileOrganizationName, boolean isWorkProfileOn) { + if (hasCACertsInWorkProfile && isWorkProfileOn) { + if (workProfileOrganizationName == null) { + return mDpm.getResources().getString( + QS_MSG_WORK_PROFILE_MONITORING, mWorkProfileMonitoringStringSupplier); + } + return mDpm.getResources().getString( + QS_MSG_NAMED_WORK_PROFILE_MONITORING, + () -> mContext.getString( + R.string.quick_settings_disclosure_named_managed_profile_monitoring, + workProfileOrganizationName), + workProfileOrganizationName); + } + if (hasCACerts) { + return mContext.getString(R.string.quick_settings_disclosure_monitoring); + } + return null; + } + + private String getVpnText(boolean hasWorkProfile, String vpnName, String vpnNameWorkProfile, + boolean isWorkProfileOn) { + if (vpnName != null && vpnNameWorkProfile != null) { + return mContext.getString(R.string.quick_settings_disclosure_vpns); + } + if (vpnNameWorkProfile != null && isWorkProfileOn) { + return mDpm.getResources().getString( + QS_MSG_WORK_PROFILE_NAMED_VPN, + () -> mContext.getString( + R.string.quick_settings_disclosure_managed_profile_named_vpn, + vpnNameWorkProfile), + vpnNameWorkProfile); + } + if (vpnName != null) { + if (hasWorkProfile) { + return mDpm.getResources().getString( + QS_MSG_PERSONAL_PROFILE_NAMED_VPN, + () -> mContext.getString( + R.string.quick_settings_disclosure_personal_profile_named_vpn, + vpnName), + vpnName); + } + return mContext.getString(R.string.quick_settings_disclosure_named_vpn, + vpnName); + } + return null; + } + + private String getManagedProfileNetworkActivityText() { + return mDpm.getResources().getString( + QS_MSG_WORK_PROFILE_NETWORK, mWorkProfileNetworkStringSupplier); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_NEGATIVE) { + final Intent intent = new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS); + dialog.dismiss(); + // This dismisses the shade on opening the activity + mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); + } + } + + private void createDialog(Context quickSettingsContext, @Nullable View view) { + mShouldUseSettingsButton.set(false); + mBgHandler.post(() -> { + String settingsButtonText = getSettingsButton(); + final View dialogView = createDialogView(); + mMainHandler.post(() -> { + mDialog = new SystemUIDialog(quickSettingsContext, 0); + mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this); + mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, mShouldUseSettingsButton.get() + ? settingsButtonText : getNegativeButton(), this); + + mDialog.setView(dialogView); + if (view != null && view.isAggregatedVisible()) { + mDialogLaunchAnimator.showFromView(mDialog, view, new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)); + } else { + mDialog.show(); + } + }); + }); + } + + @VisibleForTesting + Dialog getDialog() { + return mDialog; + } + + @VisibleForTesting + View createDialogView() { + if (mSecurityController.isParentalControlsEnabled()) { + return createParentalControlsDialogView(); + } + return createOrganizationDialogView(); + } + + private View createOrganizationDialogView() { + final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); + final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); + final CharSequence deviceOwnerOrganization = + mSecurityController.getDeviceOwnerOrganizationName(); + final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser(); + final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile(); + final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled(); + final String vpnName = mSecurityController.getPrimaryVpnName(); + final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName(); + + View dialogView = LayoutInflater.from(mContext) + .inflate(R.layout.quick_settings_footer_dialog, null, false); + + // device management section + TextView deviceManagementSubtitle = + dialogView.findViewById(R.id.device_management_subtitle); + deviceManagementSubtitle.setText(getManagementTitle(deviceOwnerOrganization)); + + CharSequence managementMessage = getManagementMessage(isDeviceManaged, + deviceOwnerOrganization); + if (managementMessage == null) { + dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.GONE); + } else { + dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.VISIBLE); + TextView deviceManagementWarning = + (TextView) dialogView.findViewById(R.id.device_management_warning); + deviceManagementWarning.setText(managementMessage); + mShouldUseSettingsButton.set(true); + } + + // ca certificate section + CharSequence caCertsMessage = getCaCertsMessage(isDeviceManaged, hasCACerts, + hasCACertsInWorkProfile); + if (caCertsMessage == null) { + dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.GONE); + } else { + dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.VISIBLE); + TextView caCertsWarning = (TextView) dialogView.findViewById(R.id.ca_certs_warning); + caCertsWarning.setText(caCertsMessage); + // Make "Open trusted credentials"-link clickable + caCertsWarning.setMovementMethod(new LinkMovementMethod()); + + TextView caCertsSubtitle = (TextView) dialogView.findViewById(R.id.ca_certs_subtitle); + String caCertsSubtitleMessage = mDpm.getResources().getString( + QS_DIALOG_MONITORING_CA_CERT_SUBTITLE, mMonitoringSubtitleCaCertStringSupplier); + caCertsSubtitle.setText(caCertsSubtitleMessage); + + } + + // network logging section + CharSequence networkLoggingMessage = getNetworkLoggingMessage(isDeviceManaged, + isNetworkLoggingEnabled); + if (networkLoggingMessage == null) { + dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.GONE); + } else { + dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.VISIBLE); + TextView networkLoggingWarning = + (TextView) dialogView.findViewById(R.id.network_logging_warning); + networkLoggingWarning.setText(networkLoggingMessage); + + TextView networkLoggingSubtitle = (TextView) dialogView.findViewById( + R.id.network_logging_subtitle); + String networkLoggingSubtitleMessage = mDpm.getResources().getString( + QS_DIALOG_MONITORING_NETWORK_SUBTITLE, + mMonitoringSubtitleNetworkStringSupplier); + networkLoggingSubtitle.setText(networkLoggingSubtitleMessage); + } + + // vpn section + CharSequence vpnMessage = getVpnMessage(isDeviceManaged, hasWorkProfile, vpnName, + vpnNameWorkProfile); + if (vpnMessage == null) { + dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.GONE); + } else { + dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.VISIBLE); + TextView vpnWarning = (TextView) dialogView.findViewById(R.id.vpn_warning); + vpnWarning.setText(vpnMessage); + // Make "Open VPN Settings"-link clickable + vpnWarning.setMovementMethod(new LinkMovementMethod()); + + TextView vpnSubtitle = (TextView) dialogView.findViewById(R.id.vpn_subtitle); + String vpnSubtitleMessage = mDpm.getResources().getString( + QS_DIALOG_MONITORING_VPN_SUBTITLE, mMonitoringSubtitleVpnStringSupplier); + vpnSubtitle.setText(vpnSubtitleMessage); + } + + // Note: if a new section is added, should update configSubtitleVisibility to include + // the handling of the subtitle + configSubtitleVisibility(managementMessage != null, + caCertsMessage != null, + networkLoggingMessage != null, + vpnMessage != null, + dialogView); + + return dialogView; + } + + private View createParentalControlsDialogView() { + View dialogView = LayoutInflater.from(mContext) + .inflate(R.layout.quick_settings_footer_dialog_parental_controls, null, false); + + DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo(); + Drawable icon = mSecurityController.getIcon(info); + if (icon != null) { + ImageView imageView = (ImageView) dialogView.findViewById(R.id.parental_controls_icon); + imageView.setImageDrawable(icon); + } + + TextView parentalControlsTitle = + (TextView) dialogView.findViewById(R.id.parental_controls_title); + parentalControlsTitle.setText(mSecurityController.getLabel(info)); + + return dialogView; + } + + protected void configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts, + boolean showNetworkLogging, boolean showVpn, View dialogView) { + // Device Management title should always been shown + // When there is a Device Management message, all subtitles should be shown + if (showDeviceManagement) { + return; + } + // Hide the subtitle if there is only 1 message shown + int mSectionCountExcludingDeviceMgt = 0; + if (showCaCerts) { + mSectionCountExcludingDeviceMgt++; + } + if (showNetworkLogging) { + mSectionCountExcludingDeviceMgt++; + } + if (showVpn) { + mSectionCountExcludingDeviceMgt++; + } + + // No work needed if there is no sections or more than 1 section + if (mSectionCountExcludingDeviceMgt != 1) { + return; + } + if (showCaCerts) { + dialogView.findViewById(R.id.ca_certs_subtitle).setVisibility(View.GONE); + } + if (showNetworkLogging) { + dialogView.findViewById(R.id.network_logging_subtitle).setVisibility(View.GONE); + } + if (showVpn) { + dialogView.findViewById(R.id.vpn_subtitle).setVisibility(View.GONE); + } + } + + // This should not be called on the main thread to avoid making an IPC. + @VisibleForTesting + String getSettingsButton() { + return mDpm.getResources().getString( + QS_DIALOG_VIEW_POLICIES, mViewPoliciesButtonStringSupplier); + } + + private String getPositiveButton() { + return mContext.getString(R.string.ok); + } + + @Nullable + private String getNegativeButton() { + if (mSecurityController.isParentalControlsEnabled()) { + return mContext.getString(R.string.monitoring_button_view_controls); + } + return null; + } + + @Nullable + protected CharSequence getManagementMessage(boolean isDeviceManaged, + CharSequence organizationName) { + if (!isDeviceManaged) { + return null; + } + if (organizationName != null) { + if (isFinancedDevice()) { + return mContext.getString(R.string.monitoring_financed_description_named_management, + organizationName, organizationName); + } else { + return mDpm.getResources().getString( + QS_DIALOG_NAMED_MANAGEMENT, + () -> mContext.getString( + R.string.monitoring_description_named_management, + organizationName), + organizationName); + } + } + return mDpm.getResources().getString(QS_DIALOG_MANAGEMENT, mManagementDialogStringSupplier); + } + + @Nullable + protected CharSequence getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts, + boolean hasCACertsInWorkProfile) { + if (!(hasCACerts || hasCACertsInWorkProfile)) return null; + if (isDeviceManaged) { + return mDpm.getResources().getString( + QS_DIALOG_MANAGEMENT_CA_CERT, mManagementDialogCaCertStringSupplier); + } + if (hasCACertsInWorkProfile) { + return mDpm.getResources().getString( + QS_DIALOG_WORK_PROFILE_CA_CERT, mWorkProfileDialogCaCertStringSupplier); + } + return mContext.getString(R.string.monitoring_description_ca_certificate); + } + + @Nullable + protected CharSequence getNetworkLoggingMessage(boolean isDeviceManaged, + boolean isNetworkLoggingEnabled) { + if (!isNetworkLoggingEnabled) return null; + if (isDeviceManaged) { + return mDpm.getResources().getString( + QS_DIALOG_MANAGEMENT_NETWORK, mManagementDialogNetworkStringSupplier); + } else { + return mDpm.getResources().getString( + QS_DIALOG_WORK_PROFILE_NETWORK, mWorkProfileDialogNetworkStringSupplier); + } + } + + @Nullable + protected CharSequence getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile, + String vpnName, String vpnNameWorkProfile) { + if (vpnName == null && vpnNameWorkProfile == null) return null; + final SpannableStringBuilder message = new SpannableStringBuilder(); + if (isDeviceManaged) { + if (vpnName != null && vpnNameWorkProfile != null) { + String namedVpns = mDpm.getResources().getString( + QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN, + () -> mContext.getString( + R.string.monitoring_description_two_named_vpns, + vpnName, vpnNameWorkProfile), + vpnName, vpnNameWorkProfile); + message.append(namedVpns); + } else { + String name = vpnName != null ? vpnName : vpnNameWorkProfile; + String namedVp = mDpm.getResources().getString( + QS_DIALOG_MANAGEMENT_NAMED_VPN, + () -> mContext.getString(R.string.monitoring_description_named_vpn, name), + name); + message.append(namedVp); + } + } else { + if (vpnName != null && vpnNameWorkProfile != null) { + String namedVpns = mDpm.getResources().getString( + QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN, + () -> mContext.getString( + R.string.monitoring_description_two_named_vpns, + vpnName, vpnNameWorkProfile), + vpnName, vpnNameWorkProfile); + message.append(namedVpns); + } else if (vpnNameWorkProfile != null) { + String namedVpn = mDpm.getResources().getString( + QS_DIALOG_WORK_PROFILE_NAMED_VPN, + () -> mContext.getString( + R.string.monitoring_description_managed_profile_named_vpn, + vpnNameWorkProfile), + vpnNameWorkProfile); + message.append(namedVpn); + } else if (hasWorkProfile) { + String namedVpn = mDpm.getResources().getString( + QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN, + () -> mContext.getString( + R.string.monitoring_description_personal_profile_named_vpn, + vpnName), + vpnName); + message.append(namedVpn); + } else { + message.append(mContext.getString(R.string.monitoring_description_named_vpn, + vpnName)); + } + } + message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator)); + message.append(mContext.getString(R.string.monitoring_description_vpn_settings), + new VpnSpan(), 0); + return message; + } + + @VisibleForTesting + CharSequence getManagementTitle(CharSequence deviceOwnerOrganization) { + if (deviceOwnerOrganization != null && isFinancedDevice()) { + return mContext.getString(R.string.monitoring_title_financed_device, + deviceOwnerOrganization); + } else { + return mDpm.getResources().getString( + QS_DIALOG_MANAGEMENT_TITLE, + mManagementTitleSupplier); + } + } + + private boolean isFinancedDevice() { + return mSecurityController.isDeviceManaged() + && mSecurityController.getDeviceOwnerType( + mSecurityController.getDeviceOwnerComponentOnAnyUser()) + == DEVICE_OWNER_TYPE_FINANCED; + } + + protected class VpnSpan extends ClickableSpan { + @Override + public void onClick(View widget) { + final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS); + mDialog.dismiss(); + // This dismisses the shade on opening the activity + mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); + } + + // for testing, to compare two CharSequences containing VpnSpans + @Override + public boolean equals(Object object) { + return object instanceof VpnSpan; + } + + @Override + public int hashCode() { + return 314159257; // prime + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt new file mode 100644 index 000000000000..38fe34eb8f9f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.dagger + +import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository +import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl +import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository +import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl +import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor +import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl +import dagger.Binds +import dagger.Module + +/** Dagger module to provide/bind footer actions singletons. */ +@Module +interface FooterActionsModule { + @Binds fun userSwitcherRepository(impl: UserSwitcherRepositoryImpl): UserSwitcherRepository + + @Binds + fun foregroundServicesRepository( + impl: ForegroundServicesRepositoryImpl + ): ForegroundServicesRepository + + @Binds fun footerActionsInteractor(impl: FooterActionsInteractorImpl): FooterActionsInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/data/model/UserSwitcherStatusModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/data/model/UserSwitcherStatusModel.kt new file mode 100644 index 000000000000..4ca229ab4e61 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/data/model/UserSwitcherStatusModel.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.data.model + +import android.graphics.drawable.Drawable + +/** The current status of the User Switcher. */ +sealed class UserSwitcherStatusModel { + /** The user switcher is disabled. */ + object Disabled : UserSwitcherStatusModel() + + /** The user switcher is enabled. */ + data class Enabled( + val currentUserName: String?, + val currentUserImage: Drawable?, + val isGuestUser: Boolean, + ) : UserSwitcherStatusModel() +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt new file mode 100644 index 000000000000..37a9c40ffacf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.data.repository + +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.FgsManagerController +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +interface ForegroundServicesRepository { + /** + * The number of packages with a service running in the foreground. + * + * Note that this will be equal to 0 if [FgsManagerController.isAvailable] is false. + */ + val foregroundServicesCount: Flow<Int> + + /** + * Whether there were new changes to the foreground packages since a dialog was last shown. + * + * Note that this will be equal to `false` if [FgsManagerController.showFooterDot] is false. + */ + val hasNewChanges: Flow<Boolean> +} + +@SysUISingleton +class ForegroundServicesRepositoryImpl +@Inject +constructor( + fgsManagerController: FgsManagerController, +) : ForegroundServicesRepository { + override val foregroundServicesCount: Flow<Int> = + fgsManagerController.isAvailable + .flatMapLatest { isAvailable -> + if (!isAvailable) { + return@flatMapLatest flowOf(0) + } + + conflatedCallbackFlow { + fun updateState(numberOfPackages: Int) { + trySendWithFailureLogging(numberOfPackages, TAG) + } + + val listener = + object : FgsManagerController.OnNumberOfPackagesChangedListener { + override fun onNumberOfPackagesChanged(numberOfPackages: Int) { + updateState(numberOfPackages) + } + } + + fgsManagerController.addOnNumberOfPackagesChangedListener(listener) + updateState(fgsManagerController.numRunningPackages) + awaitClose { + fgsManagerController.removeOnNumberOfPackagesChangedListener(listener) + } + } + } + .distinctUntilChanged() + + override val hasNewChanges: Flow<Boolean> = + fgsManagerController.showFooterDot.flatMapLatest { showFooterDot -> + if (!showFooterDot) { + return@flatMapLatest flowOf(false) + } + + // A flow that emits whenever the FGS dialog is dismissed. + val dialogDismissedEvents = conflatedCallbackFlow { + fun updateState() { + trySendWithFailureLogging( + Unit, + TAG, + ) + } + + val listener = + object : FgsManagerController.OnDialogDismissedListener { + override fun onDialogDismissed() { + updateState() + } + } + + fgsManagerController.addOnDialogDismissedListener(listener) + awaitClose { fgsManagerController.removeOnDialogDismissedListener(listener) } + } + + // Query [fgsManagerController.newChangesSinceDialogWasDismissed] everytime the dialog + // is dismissed or when [foregroundServices] is changing. + merge( + foregroundServicesCount, + dialogDismissedEvents, + ) + .map { fgsManagerController.newChangesSinceDialogWasDismissed } + .distinctUntilChanged() + } + + companion object { + private const val TAG = "ForegroundServicesRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/UserSwitcherRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/UserSwitcherRepository.kt new file mode 100644 index 000000000000..e969d4c6e08a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/UserSwitcherRepository.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.data.repository + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Handler +import android.os.UserManager +import android.provider.Settings.Global.USER_SWITCHER_ENABLED +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.R +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.SettingObserver +import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.UserInfoController +import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.util.settings.GlobalSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +interface UserSwitcherRepository { + /** The current [UserSwitcherStatusModel]. */ + val userSwitcherStatus: Flow<UserSwitcherStatusModel> +} + +@SysUISingleton +class UserSwitcherRepositoryImpl +@Inject +constructor( + @Application private val context: Context, + @Background private val bgHandler: Handler, + @Background private val bgDispatcher: CoroutineDispatcher, + private val userManager: UserManager, + private val userTracker: UserTracker, + private val userSwitcherController: UserSwitcherController, + private val userInfoController: UserInfoController, + private val globalSetting: GlobalSettings, +) : UserSwitcherRepository { + private val showUserSwitcherForSingleUser = + context.resources.getBoolean(R.bool.qs_show_user_switcher_for_single_user) + + /** Whether the user switcher is currently enabled. */ + private val isEnabled: Flow<Boolean> = conflatedCallbackFlow { + suspend fun updateState() { + trySendWithFailureLogging(isUserSwitcherEnabled(), TAG) + } + + val observer = + object : + SettingObserver( + globalSetting, + bgHandler, + USER_SWITCHER_ENABLED, + userTracker.userId, + ) { + override fun handleValueChanged(value: Int, observedChange: Boolean) { + if (observedChange) { + launch { updateState() } + } + } + } + + observer.isListening = true + updateState() + awaitClose { observer.isListening = false } + } + + /** The current user name. */ + private val currentUserName: Flow<String?> = conflatedCallbackFlow { + suspend fun updateState() { + trySendWithFailureLogging(getCurrentUser(), TAG) + } + + val callback = UserSwitcherController.UserSwitchCallback { launch { updateState() } } + + userSwitcherController.addUserSwitchCallback(callback) + updateState() + awaitClose { userSwitcherController.removeUserSwitchCallback(callback) } + } + + /** The current (icon, isGuestUser) values. */ + // TODO(b/242040009): Could we only use this callback to get the user name and remove + // currentUsername above? + private val currentUserInfo: Flow<Pair<Drawable?, Boolean>> = conflatedCallbackFlow { + val listener = + UserInfoController.OnUserInfoChangedListener { _, picture, _ -> + launch { trySendWithFailureLogging(picture to isGuestUser(), TAG) } + } + + // This will automatically call the listener when attached, so no need to update the state + // here. + userInfoController.addCallback(listener) + awaitClose { userInfoController.removeCallback(listener) } + } + + override val userSwitcherStatus: Flow<UserSwitcherStatusModel> = + isEnabled + .flatMapLatest { enabled -> + if (enabled) { + combine(currentUserName, currentUserInfo) { name, (icon, isGuest) -> + UserSwitcherStatusModel.Enabled(name, icon, isGuest) + } + } else { + flowOf(UserSwitcherStatusModel.Disabled) + } + } + .distinctUntilChanged() + + private suspend fun isUserSwitcherEnabled(): Boolean { + return withContext(bgDispatcher) { + userManager.isUserSwitcherEnabled(showUserSwitcherForSingleUser) + } + } + + private suspend fun getCurrentUser(): String? { + return withContext(bgDispatcher) { userSwitcherController.currentUserName } + } + + private suspend fun isGuestUser(): Boolean { + return withContext(bgDispatcher) { + userManager.isGuestUser(KeyguardUpdateMonitor.getCurrentUser()) + } + } + + companion object { + private const val TAG = "UserSwitcherRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt new file mode 100644 index 000000000000..cf9b41c25388 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.domain.interactor + +import android.app.admin.DevicePolicyEventLogger +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle +import android.provider.Settings +import android.view.View +import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.logging.MetricsLogger +import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.nano.MetricsProto +import com.android.internal.util.FrameworkStatsLog +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.globalactions.GlobalActionsDialogLite +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.FgsManagerController +import com.android.systemui.qs.QSSecurityFooterUtils +import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel +import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository +import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository +import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig +import com.android.systemui.qs.user.UserSwitchDialogController +import com.android.systemui.security.data.repository.SecurityRepository +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.UserSwitcherActivity +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +/** Interactor for the footer actions business logic. */ +interface FooterActionsInteractor { + /** The current [SecurityButtonConfig]. */ + val securityButtonConfig: Flow<SecurityButtonConfig?> + + /** The number of packages with a service running in the foreground. */ + val foregroundServicesCount: Flow<Int> + + /** Whether there are new packages with a service running in the foreground. */ + val hasNewForegroundServices: Flow<Boolean> + + /** The current [UserSwitcherStatusModel]. */ + val userSwitcherStatus: Flow<UserSwitcherStatusModel> + + /** + * The flow emitting `Unit` whenever a request to show the device monitoring dialog is fired. + */ + val deviceMonitoringDialogRequests: Flow<Unit> + + /** + * Show the device monitoring dialog, expanded from [view]. + * + * Important: [view] must be associated to the same [Context] as the [Quick Settings fragment] + * [com.android.systemui.qs.QSFragment]. + */ + // TODO(b/230830644): Replace view by Expandable interface. + fun showDeviceMonitoringDialog(view: View) + + /** + * Show the device monitoring dialog. + * + * Important: [quickSettingsContext] *must* be the [Context] associated to the [Quick Settings + * fragment][com.android.systemui.qs.QSFragment]. + */ + // TODO(b/230830644): Replace view by Expandable interface. + fun showDeviceMonitoringDialog(quickSettingsContext: Context) + + /** Show the foreground services dialog. */ + // TODO(b/230830644): Replace view by Expandable interface. + fun showForegroundServicesDialog(view: View) + + /** Show the power menu dialog. */ + // TODO(b/230830644): Replace view by Expandable interface. + fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) + + /** Show the settings. */ + fun showSettings(expandable: Expandable) + + /** Show the user switcher. */ + // TODO(b/230830644): Replace view by Expandable interface. + fun showUserSwitcher(view: View) +} + +@SysUISingleton +class FooterActionsInteractorImpl +@Inject +constructor( + private val activityStarter: ActivityStarter, + private val featureFlags: FeatureFlags, + private val metricsLogger: MetricsLogger, + private val uiEventLogger: UiEventLogger, + private val deviceProvisionedController: DeviceProvisionedController, + private val qsSecurityFooterUtils: QSSecurityFooterUtils, + private val fgsManagerController: FgsManagerController, + private val userSwitchDialogController: UserSwitchDialogController, + securityRepository: SecurityRepository, + foregroundServicesRepository: ForegroundServicesRepository, + userSwitcherRepository: UserSwitcherRepository, + broadcastDispatcher: BroadcastDispatcher, + @Background bgDispatcher: CoroutineDispatcher, +) : FooterActionsInteractor { + override val securityButtonConfig: Flow<SecurityButtonConfig?> = + securityRepository.security.map { security -> + withContext(bgDispatcher) { qsSecurityFooterUtils.getButtonConfig(security) } + } + + override val foregroundServicesCount: Flow<Int> = + foregroundServicesRepository.foregroundServicesCount + + override val hasNewForegroundServices: Flow<Boolean> = + foregroundServicesRepository.hasNewChanges + + override val userSwitcherStatus: Flow<UserSwitcherStatusModel> = + userSwitcherRepository.userSwitcherStatus + + override val deviceMonitoringDialogRequests: Flow<Unit> = + broadcastDispatcher.broadcastFlow( + IntentFilter(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG), + UserHandle.ALL, + Context.RECEIVER_EXPORTED, + null, + ) + + override fun showDeviceMonitoringDialog(view: View) { + qsSecurityFooterUtils.showDeviceMonitoringDialog(view.context, view) + DevicePolicyEventLogger.createEvent( + FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED + ) + .write() + } + + override fun showDeviceMonitoringDialog(quickSettingsContext: Context) { + qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null) + } + + override fun showForegroundServicesDialog(view: View) { + fgsManagerController.showDialog(view) + } + + override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) { + uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) + globalActionsDialogLite.showOrHideDialog( + /* keyguardShowing= */ false, + /* isDeviceProvisioned= */ true, + view, + ) + } + + override fun showSettings(expandable: Expandable) { + if (!deviceProvisionedController.isCurrentUserSetup) { + // If user isn't setup just unlock the device and dump them back at SUW. + activityStarter.postQSRunnableDismissingKeyguard {} + return + } + + metricsLogger.action(MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH) + activityStarter.startActivity( + Intent(Settings.ACTION_SETTINGS), + true /* dismissShade */, + expandable.activityLaunchController( + InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON + ), + ) + } + + override fun showUserSwitcher(view: View) { + if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { + userSwitchDialogController.showDialog(view) + return + } + + val intent = + Intent(view.context, UserSwitcherActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + } + + activityStarter.startActivity( + intent, + true /* dismissShade */, + ActivityLaunchAnimator.Controller.fromView(view, null), + true /* showOverlockscreenwhenlocked */, + UserHandle.SYSTEM, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/model/SecurityButtonConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/model/SecurityButtonConfig.kt new file mode 100644 index 000000000000..be9c0c1de799 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/model/SecurityButtonConfig.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.domain.model + +import com.android.systemui.common.shared.model.Icon + +/** The config for the security button. */ +data class SecurityButtonConfig( + val icon: Icon, + val text: String, + val isClickable: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt new file mode 100644 index 000000000000..8dd506ec8775 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.ui.binder + +import android.content.Context +import android.graphics.PorterDuff +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R +import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.people.ui.view.PeopleViewBinder.bind +import com.android.systemui.qs.FooterActionsView +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** A ViewBinder for [FooterActionsViewBinder]. */ +object FooterActionsViewBinder { + /** + * Create a [FooterActionsView] that can later be [bound][bind] to a [FooterActionsViewModel]. + */ + @JvmStatic + fun create(context: Context): FooterActionsView { + return LayoutInflater.from(context).inflate(R.layout.footer_actions, /* root= */ null) + as FooterActionsView + } + + /** Bind [view] to [viewModel]. */ + @JvmStatic + fun bind( + view: FooterActionsView, + viewModel: FooterActionsViewModel, + qsVisibilityLifecycleOwner: LifecycleOwner, + ) { + // Remove all children of the FooterActionsView that are used by the old implementation. + // TODO(b/242040009): Clean up the XML once the old implementation is removed. + view.removeAllViews() + + // Add the views used by this new implementation. + val context = view.context + val inflater = LayoutInflater.from(context) + + val securityHolder = TextButtonViewHolder.createAndAdd(inflater, view) + val foregroundServicesWithTextHolder = TextButtonViewHolder.createAndAdd(inflater, view) + val foregroundServicesWithNumberHolder = NumberButtonViewHolder.createAndAdd(inflater, view) + val userSwitcherHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = false) + val settingsHolder = + IconButtonViewHolder.createAndAdd(inflater, view, isLast = viewModel.power == null) + + // Bind the static power and settings buttons. + bindButton(settingsHolder, viewModel.settings) + + if (viewModel.power != null) { + val powerHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = true) + bindButton(powerHolder, viewModel.power) + } + + // There are 2 lifecycle scopes we are using here: + // 1) The scope created by [repeatWhenAttached] when [view] is attached, and destroyed + // when the [view] is detached. We use this as the parent scope for all our [viewModel] + // state collection, given that we don't want to do any work when [view] is detached. + // 2) The scope owned by [lifecycleOwner], which should be RESUMED only when Quick + // Settings are visible. We use this to make sure we collect UI state only when the + // View is visible. + // + // Given that we start our collection when the Quick Settings become visible, which happens + // every time the user swipes down the shade, we remember our previous UI state already + // bound to the UI to avoid binding the same values over and over for nothing. + + // TODO(b/242040009): Look into using only a single scope. + + var previousSecurity: FooterActionsSecurityButtonViewModel? = null + var previousForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null + var previousUserSwitcher: FooterActionsButtonViewModel? = null + + view.repeatWhenAttached { + val attachedScope = this.lifecycleScope + + attachedScope.launch { + // Listen for dialog requests as soon as we are attached, even when not visible. + // TODO(b/242040009): Should this move somewhere else? + launch { viewModel.observeDeviceMonitoringDialogRequests(view.context) } + + // Make sure we set the correct visibility and alpha even when QS are not currently + // shown. + launch { + viewModel.isVisible.collect { isVisible -> view.isInvisible = !isVisible } + } + + launch { viewModel.alpha.collect { view.alpha = it } } + launch { viewModel.backgroundAlpha.collect { view.backgroundAlpha = it } } + } + + // Listen for model changes only when QS are visible. + qsVisibilityLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + // Security. + launch { + viewModel.security.collect { security -> + if (previousSecurity != security) { + bindSecurity(securityHolder, security) + previousSecurity = security + } + } + } + + // Foreground services. + launch { + viewModel.foregroundServices.collect { foregroundServices -> + if (previousForegroundServices != foregroundServices) { + bindForegroundService( + foregroundServicesWithNumberHolder, + foregroundServicesWithTextHolder, + foregroundServices, + ) + previousForegroundServices = foregroundServices + } + } + } + + // User switcher. + launch { + viewModel.userSwitcher.collect { userSwitcher -> + if (previousUserSwitcher != userSwitcher) { + bindButton(userSwitcherHolder, userSwitcher) + previousUserSwitcher = userSwitcher + } + } + } + } + } + } + + private fun bindSecurity( + securityHolder: TextButtonViewHolder, + security: FooterActionsSecurityButtonViewModel?, + ) { + val securityView = securityHolder.view + securityView.isVisible = security != null + if (security == null) { + return + } + + // Make sure that the chevron is visible and that the button is clickable if there is a + // listener. + val chevron = securityHolder.chevron + if (security.onClick != null) { + securityView.isClickable = true + securityView.setOnClickListener(security.onClick) + chevron.isVisible = true + } else { + securityView.isClickable = false + securityView.setOnClickListener(null) + chevron.isVisible = false + } + + securityHolder.text.text = security.text + securityHolder.newDot.isVisible = false + IconViewBinder.bind(security.icon, securityHolder.icon) + } + + private fun bindForegroundService( + foregroundServicesWithNumberHolder: NumberButtonViewHolder, + foregroundServicesWithTextHolder: TextButtonViewHolder, + foregroundServices: FooterActionsForegroundServicesButtonViewModel?, + ) { + val foregroundServicesWithNumberView = foregroundServicesWithNumberHolder.view + val foregroundServicesWithTextView = foregroundServicesWithTextHolder.view + if (foregroundServices == null) { + foregroundServicesWithNumberView.isVisible = false + foregroundServicesWithTextView.isVisible = false + return + } + + val foregroundServicesCount = foregroundServices.foregroundServicesCount + if (foregroundServices.displayText) { + // Button with text, icon and chevron. + foregroundServicesWithNumberView.isVisible = false + + foregroundServicesWithTextView.isVisible = true + foregroundServicesWithTextView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithTextHolder.text.text = foregroundServices.text + foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges + } else { + // Small button with the number only. + foregroundServicesWithTextView.isVisible = false + + foregroundServicesWithNumberView.visibility = View.VISIBLE + foregroundServicesWithNumberView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString() + foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text + foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges + } + } + + private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) { + val buttonView = button.view + buttonView.isVisible = model != null + if (model == null) { + return + } + + buttonView.setBackgroundResource(model.background) + buttonView.setOnClickListener(model.onClick) + + val icon = model.icon + val iconView = button.icon + val contentDescription = model.contentDescription + + IconViewBinder.bind(icon, iconView) + ContentDescriptionViewBinder.bind(contentDescription, iconView) + if (model.iconTint != null) { + iconView.setColorFilter(model.iconTint, PorterDuff.Mode.SRC_IN) + } else { + iconView.clearColorFilter() + } + } +} + +private class TextButtonViewHolder(val view: View) { + val icon = view.requireViewById<ImageView>(R.id.icon) + val text = view.requireViewById<TextView>(R.id.text) + val newDot = view.requireViewById<ImageView>(R.id.new_dot) + val chevron = view.requireViewById<ImageView>(R.id.chevron_icon) + + companion object { + fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): TextButtonViewHolder { + val view = + inflater.inflate( + R.layout.footer_actions_text_button, + /* root= */ root, + /* attachToRoot= */ false, + ) + root.addView(view) + return TextButtonViewHolder(view) + } + } +} + +private class NumberButtonViewHolder(val view: View) { + val number = view.requireViewById<TextView>(R.id.number) + val newDot = view.requireViewById<ImageView>(R.id.new_dot) + + companion object { + fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): NumberButtonViewHolder { + val view = + inflater.inflate( + R.layout.footer_actions_number_button, + /* root= */ root, + /* attachToRoot= */ false, + ) + root.addView(view) + return NumberButtonViewHolder(view) + } + } +} + +private class IconButtonViewHolder(val view: View) { + val icon = view.requireViewById<ImageView>(R.id.icon) + + companion object { + fun createAndAdd( + inflater: LayoutInflater, + root: ViewGroup, + isLast: Boolean, + ): IconButtonViewHolder { + val view = + inflater.inflate( + R.layout.footer_actions_icon_button, + /* root= */ root, + /* attachToRoot= */ false, + ) + + // All buttons have a background with an inset of qs_footer_action_inset, so the last + // button must have a negative inset of -qs_footer_action_inset to compensate and be + // aligned with its parent. + val marginEnd = + if (isLast) { + -view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) + } else { + 0 + } + + val size = + view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_button_size) + root.addView( + view, + LinearLayout.LayoutParams(size, size).apply { this.marginEnd = marginEnd }, + ) + return IconButtonViewHolder(view) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt new file mode 100644 index 000000000000..4c0879e225c1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.ui.viewmodel + +import android.annotation.DrawableRes +import android.view.View +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon + +/** + * A ViewModel for a simple footer actions button. This is used for the user switcher, settings and + * power buttons. + */ +data class FooterActionsButtonViewModel( + val icon: Icon, + val iconTint: Int?, + @DrawableRes val background: Int, + val contentDescription: ContentDescription, + // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog + // or activity. + val onClick: (View) -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt new file mode 100644 index 000000000000..98b53cb0ed5a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.ui.viewmodel + +import android.view.View + +/** A ViewModel for the foreground services button. */ +data class FooterActionsForegroundServicesButtonViewModel( + val foregroundServicesCount: Int, + val text: String, + val displayText: Boolean, + val hasNewChanges: Boolean, + val onClick: (View) -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt new file mode 100644 index 000000000000..98ab129fc9de --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.ui.viewmodel + +import android.view.View +import com.android.systemui.common.shared.model.Icon + +/** A ViewModel for the security button. */ +data class FooterActionsSecurityButtonViewModel( + val icon: Icon, + val text: String, + val onClick: ((View) -> Unit)?, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt new file mode 100644 index 000000000000..b556a3e0d66b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.ui.viewmodel + +import android.content.Context +import android.util.Log +import android.view.View +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.android.settingslib.Utils +import com.android.settingslib.drawable.UserIconDrawable +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.globalactions.GlobalActionsDialogLite +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED +import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel +import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor +import com.android.systemui.util.icuMessageFormat +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider +import kotlin.math.max +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +/** A ViewModel for the footer actions. */ +class FooterActionsViewModel( + @Application private val context: Context, + private val footerActionsInteractor: FooterActionsInteractor, + private val falsingManager: FalsingManager, + private val globalActionsDialogLite: GlobalActionsDialogLite, + showPowerButton: Boolean, +) { + /** + * Whether the UI rendering this ViewModel should be visible. Note that even when this is false, + * the UI should still participate to the layout it is included in (i.e. in the View world it + * should be INVISIBLE, not GONE). + */ + private val _isVisible = MutableStateFlow(true) + val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow() + + /** The alpha the UI rendering this ViewModel should have. */ + private val _alpha = MutableStateFlow(1f) + val alpha: StateFlow<Float> = _alpha.asStateFlow() + + /** The alpha the background of the UI rendering this ViewModel should have. */ + private val _backgroundAlpha = MutableStateFlow(1f) + val backgroundAlpha: StateFlow<Float> = _backgroundAlpha.asStateFlow() + + /** The model for the security button. */ + val security: Flow<FooterActionsSecurityButtonViewModel?> = + footerActionsInteractor.securityButtonConfig + .map { config -> + val (icon, text, isClickable) = config ?: return@map null + FooterActionsSecurityButtonViewModel( + icon, + text, + if (isClickable) this::onSecurityButtonClicked else null, + ) + } + .distinctUntilChanged() + + /** The model for the foreground services button. */ + val foregroundServices: Flow<FooterActionsForegroundServicesButtonViewModel?> = + combine( + footerActionsInteractor.foregroundServicesCount, + footerActionsInteractor.hasNewForegroundServices, + security, + ) { foregroundServicesCount, hasNewChanges, securityModel -> + if (foregroundServicesCount <= 0) { + return@combine null + } + + val text = + icuMessageFormat( + context.resources, + R.string.fgs_manager_footer_label, + foregroundServicesCount, + ) + FooterActionsForegroundServicesButtonViewModel( + foregroundServicesCount, + text = text, + displayText = securityModel == null, + hasNewChanges = hasNewChanges, + this::onForegroundServiceButtonClicked, + ) + } + .distinctUntilChanged() + + /** The model for the user switcher button. */ + val userSwitcher: Flow<FooterActionsButtonViewModel?> = + footerActionsInteractor.userSwitcherStatus + .map { userSwitcherStatus -> + when (userSwitcherStatus) { + UserSwitcherStatusModel.Disabled -> null + is UserSwitcherStatusModel.Enabled -> { + if (userSwitcherStatus.currentUserImage == null) { + Log.e( + TAG, + "Skipped the addition of user switcher button because " + + "currentUserImage is missing", + ) + return@map null + } + + userSwitcherButton(userSwitcherStatus) + } + } + } + .distinctUntilChanged() + + /** The model for the settings button. */ + val settings: FooterActionsButtonViewModel = + FooterActionsButtonViewModel( + Icon.Resource(R.drawable.ic_settings), + iconTint = null, + R.drawable.qs_footer_action_circle, + ContentDescription.Resource(R.string.accessibility_quick_settings_settings), + this::onSettingsButtonClicked, + ) + + /** The model for the power button. */ + val power: FooterActionsButtonViewModel? = + if (showPowerButton) { + FooterActionsButtonViewModel( + Icon.Resource(android.R.drawable.ic_lock_power_off), + iconTint = + Utils.getColorAttrDefaultColor( + context, + com.android.internal.R.attr.textColorOnAccent, + ), + R.drawable.qs_footer_action_circle_color, + ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu), + this::onPowerButtonClicked, + ) + } else { + null + } + + /** Called when the visibility of the UI rendering this model should be changed. */ + fun onVisibilityChangeRequested(visible: Boolean) { + _isVisible.value = visible + } + + /** Called when the expansion of the Quick Settings changed. */ + fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) { + if (isInSplitShade) { + // In split shade, we want to fade in the background only at the very end (see + // b/240563302). + val delay = 0.99f + _alpha.value = expansion + _backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay) + } else { + // Only start fading in the footer actions when we are at least 90% expanded. + val delay = 0.9f + _alpha.value = max(0f, expansion - delay) / (1 - delay) + _backgroundAlpha.value = 1f + } + } + + /** + * Observe the device monitoring dialog requests and show the dialog accordingly. This function + * will suspend indefinitely and will need to be cancelled to stop observing. + * + * Important: [quickSettingsContext] must be the [Context] associated to the [Quick Settings + * fragment][com.android.systemui.qs.QSFragment], and the call to this function must be + * cancelled when that fragment is destroyed. + */ + suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) { + footerActionsInteractor.deviceMonitoringDialogRequests.collect { + footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext) + } + } + + private fun onSecurityButtonClicked(view: View) { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + return + } + + footerActionsInteractor.showDeviceMonitoringDialog(view) + } + + private fun onForegroundServiceButtonClicked(view: View) { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + return + } + + footerActionsInteractor.showForegroundServicesDialog(view) + } + + private fun onUserSwitcherClicked(view: View) { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + return + } + + footerActionsInteractor.showUserSwitcher(view) + } + + // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog + // or activity. + private fun onSettingsButtonClicked(view: View) { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + return + } + + footerActionsInteractor.showSettings(Expandable.fromView(view)) + } + + private fun onPowerButtonClicked(view: View) { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + return + } + + footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view) + } + + private fun userSwitcherButton( + status: UserSwitcherStatusModel.Enabled + ): FooterActionsButtonViewModel { + val icon = status.currentUserImage!! + val iconTint = + if (status.isGuestUser && icon !is UserIconDrawable) { + Utils.getColorAttrDefaultColor(context, android.R.attr.colorForeground) + } else { + null + } + + return FooterActionsButtonViewModel( + Icon.Loaded(icon), + iconTint, + R.drawable.qs_footer_action_circle, + ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)), + this::onUserSwitcherClicked, + ) + } + + private fun userSwitcherContentDescription(currentUser: String?): String? { + return currentUser?.let { user -> + context.getString(R.string.accessibility_quick_settings_user, user) + } + } + + @SysUISingleton + class Factory + @Inject + constructor( + @Application private val context: Context, + private val falsingManager: FalsingManager, + private val footerActionsInteractor: FooterActionsInteractor, + private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>, + @Named(PM_LITE_ENABLED) private val showPowerButton: Boolean, + ) { + /** Create a [FooterActionsViewModel] bound to the lifecycle of [lifecycleOwner]. */ + fun create(lifecycleOwner: LifecycleOwner): FooterActionsViewModel { + val globalActionsDialogLite = globalActionsDialogLiteProvider.get() + if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) { + // This should usually not happen, but let's make sure we already destroy + // globalActionsDialogLite. + globalActionsDialogLite.destroy() + } else { + // Destroy globalActionsDialogLite when the lifecycle is destroyed. + lifecycleOwner.lifecycle.addObserver( + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + globalActionsDialogLite.destroy() + } + } + ) + } + + return FooterActionsViewModel( + context, + footerActionsInteractor, + falsingManager, + globalActionsDialogLite, + showPowerButton, + ) + } + } + + companion object { + private const val TAG = "FooterActionsViewModel" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 5147d5934039..2731d64ee4e7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -44,6 +44,7 @@ import com.android.settingslib.Utils import com.android.systemui.FontSizeUtils import com.android.systemui.R import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate import com.android.systemui.plugins.qs.QSIconView import com.android.systemui.plugins.qs.QSTile import com.android.systemui.plugins.qs.QSTile.BooleanState @@ -138,8 +139,11 @@ open class QSTileViewImpl @JvmOverloads constructor( private var lastStateDescription: CharSequence? = null private var tileState = false private var lastState = INVALID - private var blockVisibilityChanges = false - private var lastVisibility = View.VISIBLE + private val launchableViewDelegate = LaunchableViewDelegate( + this, + superSetVisibility = { super.setVisibility(it) }, + superSetTransitionVisibility = { super.setTransitionVisibility(it) }, + ) private val locInScreen = IntArray(2) @@ -343,33 +347,15 @@ open class QSTileViewImpl @JvmOverloads constructor( } override fun setShouldBlockVisibilityChanges(block: Boolean) { - blockVisibilityChanges = block - - if (block) { - lastVisibility = visibility - } else { - visibility = lastVisibility - } + launchableViewDelegate.setShouldBlockVisibilityChanges(block) } override fun setVisibility(visibility: Int) { - if (blockVisibilityChanges) { - lastVisibility = visibility - return - } - - super.setVisibility(visibility) + launchableViewDelegate.setVisibility(visibility) } override fun setTransitionVisibility(visibility: Int) { - if (blockVisibilityChanges) { - // View.setTransitionVisibility just sets the visibility flag, so we don't have to save - // the transition visibility separately from the normal visibility. - lastVisibility = visibility - return - } - - super.setTransitionVisibility(visibility) + launchableViewDelegate.setTransitionVisibility(visibility) } // Accessibility diff --git a/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt b/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt new file mode 100644 index 000000000000..50af260684f8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/security/data/model/SecurityModel.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.security.data.model + +import android.graphics.drawable.Drawable +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.policy.SecurityController +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +/** The security info exposed by [com.android.systemui.statusbar.policy.SecurityController]. */ +// TODO(b/242040009): Consider splitting this model into smaller submodels. +data class SecurityModel( + val isDeviceManaged: Boolean, + val hasWorkProfile: Boolean, + val isWorkProfileOn: Boolean, + val isProfileOwnerOfOrganizationOwnedDevice: Boolean, + val deviceOwnerOrganizationName: String?, + val workProfileOrganizationName: String?, + val isNetworkLoggingEnabled: Boolean, + val isVpnBranded: Boolean, + val primaryVpnName: String?, + val workProfileVpnName: String?, + val hasCACertInCurrentUser: Boolean, + val hasCACertInWorkProfile: Boolean, + val isParentalControlsEnabled: Boolean, + val deviceAdminIcon: Drawable?, +) { + companion object { + /** Create a [SecurityModel] from the current [securityController] state. */ + suspend fun create( + securityController: SecurityController, + @Background bgDispatcher: CoroutineDispatcher, + ): SecurityModel { + return withContext(bgDispatcher) { create(securityController) } + } + + /** + * Create a [SecurityModel] from the current [securityController] state. + * + * Important: This method should be called from a background thread as this will do a lot of + * binder calls. + */ + // TODO(b/242040009): Remove this. + @JvmStatic + fun create(securityController: SecurityController): SecurityModel { + val deviceAdminInfo = + if (securityController.isParentalControlsEnabled) { + securityController.deviceAdminInfo + } else { + null + } + + return SecurityModel( + isDeviceManaged = securityController.isDeviceManaged, + hasWorkProfile = securityController.hasWorkProfile(), + isWorkProfileOn = securityController.isWorkProfileOn, + isProfileOwnerOfOrganizationOwnedDevice = + securityController.isProfileOwnerOfOrganizationOwnedDevice, + deviceOwnerOrganizationName = + securityController.deviceOwnerOrganizationName?.toString(), + workProfileOrganizationName = + securityController.workProfileOrganizationName?.toString(), + isNetworkLoggingEnabled = securityController.isNetworkLoggingEnabled, + isVpnBranded = securityController.isVpnBranded, + primaryVpnName = securityController.primaryVpnName, + workProfileVpnName = securityController.workProfileVpnName, + hasCACertInCurrentUser = securityController.hasCACertInCurrentUser(), + hasCACertInWorkProfile = securityController.hasCACertInWorkProfile(), + isParentalControlsEnabled = securityController.isParentalControlsEnabled, + deviceAdminIcon = securityController.getIcon(deviceAdminInfo), + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt b/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt new file mode 100644 index 000000000000..8f4402eaa406 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.security.data.repository + +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.security.data.model.SecurityModel +import com.android.systemui.statusbar.policy.SecurityController +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +interface SecurityRepository { + /** The current [SecurityModel]. */ + val security: Flow<SecurityModel> +} + +@SysUISingleton +class SecurityRepositoryImpl +@Inject +constructor( + private val securityController: SecurityController, + @Background private val bgDispatcher: CoroutineDispatcher, +) : SecurityRepository { + override val security: Flow<SecurityModel> = conflatedCallbackFlow { + suspend fun updateState() { + trySendWithFailureLogging(SecurityModel.create(securityController, bgDispatcher), TAG) + } + + val callback = SecurityController.SecurityControllerCallback { launch { updateState() } } + + securityController.addCallback(callback) + updateState() + awaitClose { securityController.removeCallback(callback) } + } + + companion object { + private const val TAG = "SecurityRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepositoryModule.kt new file mode 100644 index 000000000000..39a57cafaecc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepositoryModule.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.security.data.repository + +import dagger.Binds +import dagger.Module + +/** Dagger module to provide/bind security repositories. */ +@Module +interface SecurityRepositoryModule { + @Binds fun securityRepository(impl: SecurityRepositoryImpl): SecurityRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index e6d10228dc55..3b7c7ce359b3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -17,6 +17,8 @@ package com.android.systemui.shade; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; import static androidx.constraintlayout.widget.ConstraintSet.END; import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; @@ -26,8 +28,12 @@ import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE; import static com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE; +import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; +import static com.android.systemui.classifier.Classifier.GENERIC; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; +import static com.android.systemui.classifier.Classifier.UNLOCK; +import static com.android.systemui.shade.PanelView.DEBUG; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; @@ -40,6 +46,8 @@ import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStat import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING; import static com.android.systemui.util.DumpUtilsKt.asIndenting; +import static java.lang.Float.isNaN; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -47,6 +55,8 @@ import android.annotation.NonNull; import android.app.Fragment; import android.app.StatusBarManager; import android.content.ContentResolver; +import android.content.res.Configuration; +import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Canvas; import android.graphics.Color; @@ -71,11 +81,13 @@ import android.transition.TransitionManager; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; +import android.view.InputDevice; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.View.AccessibilityDelegate; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewPropertyAnimator; import android.view.ViewStub; @@ -84,6 +96,7 @@ import android.view.WindowInsets; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.Interpolator; import android.widget.FrameLayout; import androidx.annotation.Nullable; @@ -178,6 +191,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; +import com.android.systemui.statusbar.phone.BounceInterpolator; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; @@ -233,12 +247,25 @@ import javax.inject.Inject; import javax.inject.Provider; @CentralSurfacesComponent.CentralSurfacesScope -public final class NotificationPanelViewController extends PanelViewController { +public final class NotificationPanelViewController { + public static final String TAG = PanelView.class.getSimpleName(); + public static final float FLING_MAX_LENGTH_SECONDS = 0.6f; + public static final float FLING_SPEED_UP_FACTOR = 0.6f; + public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f; + public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f; + private static final int NO_FIXED_DURATION = -1; + private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L; + private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L; + + /** + * The factor of the usual high velocity that is needed in order to reach the maximum overshoot + * when flinging. A low value will make it that most flings will reach the maximum overshoot. + */ + private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f; private static final boolean DEBUG_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG); private static final boolean SPEW_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE); private static final boolean DEBUG_DRAWABLE = false; - /** * The parallax amount of the quick settings translation when dragging down the panel */ @@ -263,6 +290,16 @@ public final class NotificationPanelViewController extends PanelViewController { - CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY - 48; + private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager; + private final Resources mResources; + private final KeyguardStateController mKeyguardStateController; + private final SysuiStatusBarStateController mStatusBarStateController; + private final AmbientState mAmbientState; + private final LockscreenGestureLogger mLockscreenGestureLogger; + private final SystemClock mSystemClock; + + private final ShadeLogger mShadeLog; + private final DozeParameters mDozeParameters; private final OnHeightChangedListener mOnHeightChangedListener = new OnHeightChangedListener(); private final Runnable mCollapseExpandAction = new CollapseExpandAction(); @@ -341,6 +378,28 @@ public final class NotificationPanelViewController extends PanelViewController { private final LargeScreenShadeHeaderController mLargeScreenShadeHeaderController; private final RecordingController mRecordingController; private final PanelEventsEmitter mPanelEventsEmitter; + private final boolean mVibrateOnOpening; + private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + private final FlingAnimationUtils mFlingAnimationUtilsClosing; + private final FlingAnimationUtils mFlingAnimationUtilsDismissing; + private final LatencyTracker mLatencyTracker; + private final DozeLog mDozeLog; + /** Whether or not the PanelView can be expanded or collapsed with a drag. */ + private final boolean mNotificationsDragEnabled; + private final Interpolator mBounceInterpolator; + private final NotificationShadeWindowController mNotificationShadeWindowController; + private final PanelExpansionStateManager mPanelExpansionStateManager; + private long mDownTime; + private boolean mTouchSlopExceededBeforeDown; + private boolean mIsLaunchAnimationRunning; + private float mOverExpansion; + private CentralSurfaces mCentralSurfaces; + private HeadsUpManagerPhone mHeadsUpManager; + private float mExpandedHeight = 0; + private boolean mTracking; + private boolean mHintAnimationRunning; + private KeyguardBottomAreaView mKeyguardBottomArea; + private boolean mExpanding; private boolean mSplitShadeEnabled; /** The bottom padding reserved for elements of the keyguard measuring notifications. */ private float mKeyguardNotificationBottomPadding; @@ -371,7 +430,7 @@ public final class NotificationPanelViewController extends PanelViewController { private final ScreenOffAnimationController mScreenOffAnimationController; private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; - private int mTrackingPointer; + private int mQsTrackingPointer; private VelocityTracker mQsVelocityTracker; private boolean mQsTracking; @@ -703,6 +762,51 @@ public final class NotificationPanelViewController extends PanelViewController { private final CameraGestureHelper mCameraGestureHelper; private final Provider<KeyguardBottomAreaViewModel> mKeyguardBottomAreaViewModelProvider; private final Provider<KeyguardBottomAreaInteractor> mKeyguardBottomAreaInteractorProvider; + private float mMinExpandHeight; + private boolean mPanelUpdateWhenAnimatorEnds; + private int mFixedDuration = NO_FIXED_DURATION; + /** The overshoot amount when the panel flings open */ + private float mPanelFlingOvershootAmount; + /** The amount of pixels that we have overexpanded the last time with a gesture */ + private float mLastGesturedOverExpansion = -1; + /** Is the current animator the spring back animation? */ + private boolean mIsSpringBackAnimation; + private boolean mInSplitShade; + private float mHintDistance; + private float mInitialOffsetOnTouch; + private boolean mCollapsedAndHeadsUpOnDown; + private float mExpandedFraction = 0; + private float mExpansionDragDownAmountPx = 0; + private boolean mPanelClosedOnDown; + private boolean mHasLayoutedSinceDown; + private float mUpdateFlingVelocity; + private boolean mUpdateFlingOnLayout; + private boolean mClosing; + private boolean mTouchSlopExceeded; + private int mTrackingPointer; + private int mTouchSlop; + private float mSlopMultiplier; + private boolean mTouchAboveFalsingThreshold; + private boolean mTouchStartedInEmptyArea; + private boolean mMotionAborted; + private boolean mUpwardsWhenThresholdReached; + private boolean mAnimatingOnDown; + private boolean mHandlingPointerUp; + private ValueAnimator mHeightAnimator; + /** Whether instant expand request is currently pending and we are just waiting for layout. */ + private boolean mInstantExpanding; + private boolean mAnimateAfterExpanding; + private boolean mIsFlinging; + private String mViewName; + private float mInitialExpandY; + private float mInitialExpandX; + private boolean mTouchDisabled; + private boolean mInitialTouchFromKeyguard; + /** Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. */ + private float mNextCollapseSpeedUpFactor = 1.0f; + private boolean mGestureWaitForTouchSlop; + private boolean mIgnoreXTouchSlop; + private boolean mExpandLatencyTracking; @Inject public NotificationPanelViewController(NotificationPanelView view, @@ -726,7 +830,7 @@ public final class NotificationPanelViewController extends PanelViewController { MetricsLogger metricsLogger, ShadeLogger shadeLogger, ConfigurationController configurationController, - Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilder, + Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilderProvider, StatusBarTouchableRegionManager statusBarTouchableRegionManager, ConversationNotificationManager conversationNotificationManager, MediaHierarchyManager mediaHierarchyManager, @@ -776,25 +880,68 @@ public final class NotificationPanelViewController extends PanelViewController { CameraGestureHelper cameraGestureHelper, Provider<KeyguardBottomAreaViewModel> keyguardBottomAreaViewModelProvider, Provider<KeyguardBottomAreaInteractor> keyguardBottomAreaInteractorProvider) { - super(view, - falsingManager, - dozeLog, - keyguardStateController, - (SysuiStatusBarStateController) statusBarStateController, - notificationShadeWindowController, - vibratorHelper, - statusBarKeyguardViewManager, - latencyTracker, - flingAnimationUtilsBuilder.get(), - statusBarTouchableRegionManager, - lockscreenGestureLogger, - panelExpansionStateManager, - ambientState, - interactionJankMonitor, - shadeLogger, - systemClock); + keyguardStateController.addCallback(new KeyguardStateController.Callback() { + @Override + public void onKeyguardFadingAwayChanged() { + requestPanelHeightUpdate(); + } + }); + mAmbientState = ambientState; mView = view; + mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; + mLockscreenGestureLogger = lockscreenGestureLogger; + mPanelExpansionStateManager = panelExpansionStateManager; + mShadeLog = shadeLogger; + TouchHandler touchHandler = createTouchHandler(); + mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + mViewName = mResources.getResourceName(mView.getId()); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } + }); + + mView.addOnLayoutChangeListener(createLayoutChangeListener()); + mView.setOnTouchListener(touchHandler); + mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener()); + + mResources = mView.getResources(); + mKeyguardStateController = keyguardStateController; + mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController; + mNotificationShadeWindowController = notificationShadeWindowController; + FlingAnimationUtils.Builder flingAnimationUtilsBuilder = + flingAnimationUtilsBuilderProvider.get(); + mFlingAnimationUtils = flingAnimationUtilsBuilder + .reset() + .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS) + .setSpeedUpFactor(FLING_SPEED_UP_FACTOR) + .build(); + mFlingAnimationUtilsClosing = flingAnimationUtilsBuilder + .reset() + .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS) + .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR) + .build(); + mFlingAnimationUtilsDismissing = flingAnimationUtilsBuilder + .reset() + .setMaxLengthSeconds(0.5f) + .setSpeedUpFactor(0.6f) + .setX2(0.6f) + .setY2(0.84f) + .build(); + mLatencyTracker = latencyTracker; + mBounceInterpolator = new BounceInterpolator(); + mFalsingManager = falsingManager; + mDozeLog = dozeLog; + mNotificationsDragEnabled = mResources.getBoolean( + R.bool.config_enableNotificationShadeDrag); mVibratorHelper = vibratorHelper; + mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation); + mStatusBarTouchableRegionManager = statusBarTouchableRegionManager; + mInteractionJankMonitor = interactionJankMonitor; + mSystemClock = systemClock; mKeyguardMediaController = keyguardMediaController; mPrivacyDotViewController = privacyDotViewController; mQuickAccessWalletController = quickAccessWalletController; @@ -802,9 +949,8 @@ public final class NotificationPanelViewController extends PanelViewController { mControlsComponent = controlsComponent; mMetricsLogger = metricsLogger; mConfigurationController = configurationController; - mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder; + mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilderProvider; mMediaHierarchyManager = mediaHierarchyManager; - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mNotificationsQSContainerController = notificationsQSContainerController; mNotificationListContainer = notificationListContainer; mNotificationStackSizeCalculator = notificationStackSizeCalculator; @@ -826,7 +972,6 @@ public final class NotificationPanelViewController extends PanelViewController { mLargeScreenShadeHeaderController = largeScreenShadeHeaderController; mLayoutInflater = layoutInflater; mFeatureFlags = featureFlags; - mFalsingManager = falsingManager; mFalsingCollector = falsingCollector; mPowerManager = powerManager; mWakeUpCoordinator = coordinator; @@ -842,7 +987,6 @@ public final class NotificationPanelViewController extends PanelViewController { mUserManager = userManager; mMediaDataManager = mediaDataManager; mTapAgainViewController = tapAgainViewController; - mInteractionJankMonitor = interactionJankMonitor; mSysUiState = sysUiState; mPanelEventsEmitter = panelEventsEmitter; pulseExpansionHandler.setPulseExpandAbortListener(() -> { @@ -1045,9 +1189,14 @@ public final class NotificationPanelViewController extends PanelViewController { controller.setup(mNotificationContainerParent)); } - @Override - protected void loadDimens() { - super.loadDimens(); + @VisibleForTesting + void loadDimens() { + final ViewConfiguration configuration = ViewConfiguration.get(this.mView.getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier(); + mHintDistance = mResources.getDimension(R.dimen.hint_move_distance); + mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount); + mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade); mFlingAnimationUtils = mFlingAnimationUtilsBuilder.get() .setMaxLengthSeconds(0.4f).build(); mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mView.getContext()); @@ -1738,7 +1887,6 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override public void collapse(boolean delayed, float speedUpFactor) { if (!canPanelBeCollapsed()) { return; @@ -1748,7 +1896,20 @@ public final class NotificationPanelViewController extends PanelViewController { setQsExpandImmediate(true); setShowShelfOnly(true); } - super.collapse(delayed, speedUpFactor); + if (DEBUG) this.logf("collapse: " + this); + if (canPanelBeCollapsed()) { + cancelHeightAnimator(); + notifyExpandingStarted(); + + // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state. + setIsClosing(true); + if (delayed) { + mNextCollapseSpeedUpFactor = speedUpFactor; + this.mView.postDelayed(mFlingCollapseRunnable, 120); + } else { + fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */); + } + } } private void setQsExpandImmediate(boolean expandImmediate) { @@ -1766,10 +1927,15 @@ public final class NotificationPanelViewController extends PanelViewController { setQsExpansion(mQsMinExpansionHeight); } - @Override @VisibleForTesting - protected void cancelHeightAnimator() { - super.cancelHeightAnimator(); + void cancelHeightAnimator() { + if (mHeightAnimator != null) { + if (mHeightAnimator.isRunning()) { + mPanelUpdateWhenAnimatorEnds = false; + } + mHeightAnimator.cancel(); + } + endClosing(); } public void cancelAnimation() { @@ -1837,37 +2003,132 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override public void fling(float vel, boolean expand) { GestureRecorder gr = mCentralSurfaces.getGestureRecorder(); if (gr != null) { gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel); } - super.fling(vel, expand); + fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false); } - @Override - protected void flingToHeight(float vel, boolean expand, float target, + @VisibleForTesting + void flingToHeight(float vel, boolean expand, float target, float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) { mHeadsUpTouchHelper.notifyFling(!expand); mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */); setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f); mNotificationStackScrollLayoutController.setPanelFlinging(true); - super.flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); + if (target == mExpandedHeight && mOverExpansion == 0.0f) { + // We're at the target and didn't fling and there's no overshoot + onFlingEnd(false /* cancelled */); + return; + } + mIsFlinging = true; + // we want to perform an overshoot animation when flinging open + final boolean addOverscroll = + expand + && !mInSplitShade // Split shade has its own overscroll logic + && mStatusBarStateController.getState() != KEYGUARD + && mOverExpansion == 0.0f + && vel >= 0; + final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand); + float overshootAmount = 0.0f; + if (addOverscroll) { + // Let's overshoot depending on the amount of velocity + overshootAmount = MathUtils.lerp( + 0.2f, + 1.0f, + MathUtils.saturate(vel + / (this.mFlingAnimationUtils.getHighVelocityPxPerSecond() + * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT))); + overshootAmount += mOverExpansion / mPanelFlingOvershootAmount; + } + ValueAnimator animator = createHeightAnimator(target, overshootAmount); + if (expand) { + if (expandBecauseOfFalsing && vel < 0) { + vel = 0; + } + this.mFlingAnimationUtils.apply(animator, mExpandedHeight, + target + overshootAmount * mPanelFlingOvershootAmount, vel, + this.mView.getHeight()); + if (vel == 0) { + animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION); + } + } else { + if (shouldUseDismissingAnimation()) { + if (vel == 0) { + animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); + long duration = (long) (200 + mExpandedHeight / this.mView.getHeight() * 100); + animator.setDuration(duration); + } else { + mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel, + this.mView.getHeight()); + } + } else { + mFlingAnimationUtilsClosing.apply( + animator, mExpandedHeight, target, vel, this.mView.getHeight()); + } + + // Make it shorter if we run a canned animation + if (vel == 0) { + animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor)); + } + if (mFixedDuration != NO_FIXED_DURATION) { + animator.setDuration(mFixedDuration); + } + } + animator.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationStart(Animator animation) { + if (!mStatusBarStateController.isDozing()) { + beginJankMonitoring(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (shouldSpringBack && !mCancelled) { + // After the shade is flinged open to an overscrolled state, spring back + // the shade by reducing section padding to 0. + springBack(); + } else { + onFlingEnd(mCancelled); + } + } + }); + setAnimator(animator); + animator.start(); } - @Override - protected void onFlingEnd(boolean cancelled) { - super.onFlingEnd(cancelled); + private void onFlingEnd(boolean cancelled) { + mIsFlinging = false; + // No overshoot when the animation ends + setOverExpansionInternal(0, false /* isFromGesture */); + setAnimator(null); + mKeyguardStateController.notifyPanelFlingEnd(); + if (!cancelled) { + endJankMonitoring(); + notifyExpandingFinished(); + } else { + cancelJankMonitoring(); + } + updatePanelExpansionAndVisibility(); mNotificationStackScrollLayoutController.setPanelFlinging(false); } private boolean onQsIntercept(MotionEvent event) { if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept"); - int pointerIndex = event.findPointerIndex(mTrackingPointer); + int pointerIndex = event.findPointerIndex(mQsTrackingPointer); if (pointerIndex < 0) { pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); + mQsTrackingPointer = event.getPointerId(pointerIndex); } final float x = event.getX(pointerIndex); final float y = event.getY(pointerIndex); @@ -1896,10 +2157,10 @@ public final class NotificationPanelViewController extends PanelViewController { break; case MotionEvent.ACTION_POINTER_UP: final int upPointer = event.getPointerId(event.getActionIndex()); - if (mTrackingPointer == upPointer) { + if (mQsTrackingPointer == upPointer) { // gesture is ongoing, find a new pointer to track final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; - mTrackingPointer = event.getPointerId(newIndex); + mQsTrackingPointer = event.getPointerId(newIndex); mInitialTouchX = event.getX(newIndex); mInitialTouchY = event.getY(newIndex); } @@ -1953,8 +2214,7 @@ public final class NotificationPanelViewController extends PanelViewController { return mQsTracking; } - @Override - protected boolean isInContentBounds(float x, float y) { + private boolean isInContentBounds(float x, float y) { float stackScrollerX = mNotificationStackScrollLayoutController.getX(); return !mNotificationStackScrollLayoutController .isBelowLastNotification(x - stackScrollerX, y) @@ -2087,9 +2347,8 @@ public final class NotificationPanelViewController extends PanelViewController { - mQsMinExpansionHeight)); } - @Override - protected boolean shouldExpandWhenNotFlinging() { - if (super.shouldExpandWhenNotFlinging()) { + private boolean shouldExpandWhenNotFlinging() { + if (getExpandedFraction() > 0.5f) { return true; } if (mAllowExpandForSmallExpansion) { @@ -2101,8 +2360,7 @@ public final class NotificationPanelViewController extends PanelViewController { return false; } - @Override - protected float getOpeningHeight() { + private float getOpeningHeight() { return mNotificationStackScrollLayoutController.getOpeningHeight(); } @@ -2252,9 +2510,20 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override - protected boolean flingExpands(float vel, float vectorVel, float x, float y) { - boolean expands = super.flingExpands(vel, vectorVel, x, y); + private boolean flingExpands(float vel, float vectorVel, float x, float y) { + boolean expands = true; + if (!this.mFalsingManager.isUnlockingDisabled()) { + @Classifier.InteractionType int interactionType = y - mInitialExpandY > 0 + ? QUICK_SETTINGS : ( + mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK); + if (!isFalseTouch(x, y, interactionType)) { + if (Math.abs(vectorVel) < this.mFlingAnimationUtils.getMinVelocityPxPerSecond()) { + expands = shouldExpandWhenNotFlinging(); + } else { + expands = vel > 0; + } + } + } // If we are already running a QS expansion, make sure that we keep the panel open. if (mQsExpansionAnimator != null) { @@ -2263,8 +2532,7 @@ public final class NotificationPanelViewController extends PanelViewController { return expands; } - @Override - protected boolean shouldGestureWaitForTouchSlop() { + private boolean shouldGestureWaitForTouchSlop() { if (mExpectingSynthesizedDown) { mExpectingSynthesizedDown = false; return false; @@ -2273,10 +2541,10 @@ public final class NotificationPanelViewController extends PanelViewController { } private void onQsTouch(MotionEvent event) { - int pointerIndex = event.findPointerIndex(mTrackingPointer); + int pointerIndex = event.findPointerIndex(mQsTrackingPointer); if (pointerIndex < 0) { pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); + mQsTrackingPointer = event.getPointerId(pointerIndex); } final float y = event.getY(pointerIndex); final float x = event.getX(pointerIndex); @@ -2297,12 +2565,12 @@ public final class NotificationPanelViewController extends PanelViewController { case MotionEvent.ACTION_POINTER_UP: final int upPointer = event.getPointerId(event.getActionIndex()); - if (mTrackingPointer == upPointer) { + if (mQsTrackingPointer == upPointer) { // gesture is ongoing, find a new pointer to track final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; final float newY = event.getY(newIndex); final float newX = event.getX(newIndex); - mTrackingPointer = event.getPointerId(newIndex); + mQsTrackingPointer = event.getPointerId(newIndex); mInitialHeightOnTouch = mQsExpansionHeight; mInitialTouchY = newY; mInitialTouchX = newX; @@ -2324,7 +2592,7 @@ public final class NotificationPanelViewController extends PanelViewController { mShadeLog.logMotionEvent(event, "onQsTouch: up/cancel action, QS tracking disabled"); mQsTracking = false; - mTrackingPointer = -1; + mQsTrackingPointer = -1; trackMovement(event); float fraction = computeQsExpansionFraction(); if (fraction != 0f || y >= mInitialTouchY) { @@ -3076,8 +3344,8 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override - protected boolean canCollapsePanelOnTouch() { + @VisibleForTesting + boolean canCollapsePanelOnTouch() { if (!isInSettings() && mBarState == KEYGUARD) { return true; } @@ -3089,7 +3357,6 @@ public final class NotificationPanelViewController extends PanelViewController { return !mSplitShadeEnabled && (isInSettings() || mIsPanelCollapseOnQQS); } - @Override public int getMaxPanelHeight() { int min = mStatusBarMinHeight; if (!(mBarState == KEYGUARD) @@ -3132,8 +3399,7 @@ public final class NotificationPanelViewController extends PanelViewController { return mIsExpanding; } - @Override - protected void onHeightUpdated(float expandedHeight) { + private void onHeightUpdated(float expandedHeight) { if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) { // Updating the clock position will set the top padding which might // trigger a new panel height and re-position the clock. @@ -3315,9 +3581,7 @@ public final class NotificationPanelViewController extends PanelViewController { mLockIconViewController.setAlpha(alpha); } - @Override - protected void onExpandingStarted() { - super.onExpandingStarted(); + private void onExpandingStarted() { mNotificationStackScrollLayoutController.onExpansionStarted(); mIsExpanding = true; mQsExpandedWhenExpandingStarted = mQsFullyExpanded; @@ -3333,8 +3597,7 @@ public final class NotificationPanelViewController extends PanelViewController { mQs.setHeaderListening(true); } - @Override - protected void onExpandingFinished() { + private void onExpandingFinished() { mScrimController.onExpandingFinished(); mNotificationStackScrollLayoutController.onExpansionStopped(); mHeadsUpManager.onExpandingFinished(); @@ -3382,18 +3645,54 @@ public final class NotificationPanelViewController extends PanelViewController { mQs.setListening(listening); } - @Override public void expand(boolean animate) { - super.expand(animate); + if (isFullyCollapsed() || isCollapsing()) { + mInstantExpanding = true; + mAnimateAfterExpanding = animate; + mUpdateFlingOnLayout = false; + abortAnimations(); + if (mTracking) { + onTrackingStopped(true /* expands */); // The panel is expanded after this call. + } + if (mExpanding) { + notifyExpandingFinished(); + } + updatePanelExpansionAndVisibility();// Wait for window manager to pickup the change, + // so we know the maximum height of the panel then. + this.mView.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (!mInstantExpanding) { + mView.getViewTreeObserver().removeOnGlobalLayoutListener( + this); + return; + } + if (mCentralSurfaces.getNotificationShadeWindowView().isVisibleToUser()) { + mView.getViewTreeObserver().removeOnGlobalLayoutListener( + this); + if (mAnimateAfterExpanding) { + notifyExpandingStarted(); + beginJankMonitoring(); + fling(0, true /* expand */); + } else { + setExpandedFraction(1f); + } + mInstantExpanding = false; + } + } + });// Make sure a layout really happens. + this.mView.requestLayout(); + } + setListening(true); } - @Override public void setOverExpansion(float overExpansion) { if (overExpansion == mOverExpansion) { return; } - super.setOverExpansion(overExpansion); + mOverExpansion = overExpansion; // Translating the quick settings by half the overexpansion to center it in the background // frame updateQsFrameTranslation(); @@ -3405,10 +3704,13 @@ public final class NotificationPanelViewController extends PanelViewController { mQsTranslationForFullShadeTransition); } - @Override - protected void onTrackingStarted() { + private void onTrackingStarted() { mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen()); - super.onTrackingStarted(); + endClosing(); + mTracking = true; + mCentralSurfaces.onTrackingStarted(); + notifyExpandingStarted(); + updatePanelExpansionAndVisibility(); mScrimController.onTrackingStarted(); if (mQsFullyExpanded) { setQsExpandImmediate(true); @@ -3418,10 +3720,11 @@ public final class NotificationPanelViewController extends PanelViewController { cancelPendingPanelCollapse(); } - @Override - protected void onTrackingStopped(boolean expand) { + private void onTrackingStopped(boolean expand) { mFalsingCollector.onTrackingStopped(); - super.onTrackingStopped(expand); + mTracking = false; + mCentralSurfaces.onTrackingStopped(expand); + updatePanelExpansionAndVisibility(); if (expand) { mNotificationStackScrollLayoutController.setOverScrollAmount(0.0f, true /* onTop */, true /* animate */); @@ -3438,38 +3741,50 @@ public final class NotificationPanelViewController extends PanelViewController { getHeight(), mNavigationBarBottomHeight); } - @Override - protected void startUnlockHintAnimation() { + @VisibleForTesting + void startUnlockHintAnimation() { if (mPowerManager.isPowerSaveMode() || mAmbientState.getDozeAmount() > 0f) { onUnlockHintStarted(); onUnlockHintFinished(); return; } - super.startUnlockHintAnimation(); + + // We don't need to hint the user if an animation is already running or the user is changing + // the expansion. + if (mHeightAnimator != null || mTracking) { + return; + } + notifyExpandingStarted(); + startUnlockHintAnimationPhase1(() -> { + notifyExpandingFinished(); + onUnlockHintFinished(); + mHintAnimationRunning = false; + }); + onUnlockHintStarted(); + mHintAnimationRunning = true; } - @Override - protected void onUnlockHintFinished() { - super.onUnlockHintFinished(); + @VisibleForTesting + void onUnlockHintFinished() { + mCentralSurfaces.onHintFinished(); mScrimController.setExpansionAffectsAlpha(true); mNotificationStackScrollLayoutController.setUnlockHintRunning(false); } - @Override - protected void onUnlockHintStarted() { - super.onUnlockHintStarted(); + @VisibleForTesting + void onUnlockHintStarted() { + mCentralSurfaces.onUnlockHintStarted(); mScrimController.setExpansionAffectsAlpha(false); mNotificationStackScrollLayoutController.setUnlockHintRunning(true); } - @Override - protected boolean shouldUseDismissingAnimation() { + private boolean shouldUseDismissingAnimation() { return mBarState != StatusBarState.SHADE && (mKeyguardStateController.canDismissLockScreen() || !isTracking()); } - @Override - protected boolean isTrackingBlocked() { + @VisibleForTesting + boolean isTrackingBlocked() { return mConflictingQsExpansionGesture && mQsExpanded || mBlockingExpansionForCurrentTouch; } @@ -3491,19 +3806,17 @@ public final class NotificationPanelViewController extends PanelViewController { return mIsLaunchTransitionFinished; } - @Override public void setIsLaunchAnimationRunning(boolean running) { boolean wasRunning = mIsLaunchAnimationRunning; - super.setIsLaunchAnimationRunning(running); + mIsLaunchAnimationRunning = running; if (wasRunning != mIsLaunchAnimationRunning) { mPanelEventsEmitter.notifyLaunchingActivityChanged(running); } } - @Override - protected void setIsClosing(boolean isClosing) { + private void setIsClosing(boolean isClosing) { boolean wasClosing = isClosing(); - super.setIsClosing(isClosing); + mClosing = isClosing; if (wasClosing != isClosing) { mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing); } @@ -3517,7 +3830,6 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override public boolean isDozing() { return mDozing; } @@ -3534,8 +3846,7 @@ public final class NotificationPanelViewController extends PanelViewController { mKeyguardStatusViewController.dozeTimeTick(); } - @Override - protected boolean onMiddleClicked() { + private boolean onMiddleClicked() { switch (mBarState) { case KEYGUARD: if (!mDozingOnDown) { @@ -3593,15 +3904,13 @@ public final class NotificationPanelViewController extends PanelViewController { updateVisibility(); } - @Override - protected boolean shouldPanelBeVisible() { + private boolean shouldPanelBeVisible() { boolean headsUpVisible = mHeadsUpAnimatingAway || mHeadsUpPinnedMode; return headsUpVisible || isExpanded() || mBouncerShowing; } - @Override public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) { - super.setHeadsUpManager(headsUpManager); + mHeadsUpManager = headsUpManager; mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager, mNotificationStackScrollLayoutController.getHeadsUpCallback(), NotificationPanelViewController.this); @@ -3615,8 +3924,7 @@ public final class NotificationPanelViewController extends PanelViewController { // otherwise we update the state when the expansion is finished } - @Override - protected void onClosingFinished() { + private void onClosingFinished() { mCentralSurfaces.onClosingFinished(); setClosingWithAlphaFadeout(false); mMediaHierarchyManager.closeGuts(); @@ -3680,8 +3988,7 @@ public final class NotificationPanelViewController extends PanelViewController { mCentralSurfaces.clearNotificationEffects(); } - @Override - protected boolean isPanelVisibleBecauseOfHeadsUp() { + private boolean isPanelVisibleBecauseOfHeadsUp() { return (mHeadsUpManager.hasPinnedHeadsUp() || mHeadsUpAnimatingAway) && mBarState == StatusBarState.SHADE; } @@ -3795,9 +4102,15 @@ public final class NotificationPanelViewController extends PanelViewController { mNotificationBoundsAnimationDelay = delay; } - @Override public void setTouchAndAnimationDisabled(boolean disabled) { - super.setTouchAndAnimationDisabled(disabled); + mTouchDisabled = disabled; + if (mTouchDisabled) { + cancelHeightAnimator(); + if (mTracking) { + onTrackingStopped(true /* expanded */); + } + notifyExpandingFinished(); + } mNotificationStackScrollLayoutController.setAnimationsEnabled(!disabled); } @@ -3999,9 +4312,14 @@ public final class NotificationPanelViewController extends PanelViewController { mBlockingExpansionForCurrentTouch = mTracking; } - @Override public void dump(PrintWriter pw, String[] args) { - super.dump(pw, args); + pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s" + + " tracking=%s timeAnim=%s%s " + + "touchDisabled=%s" + "]", + this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(), + mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator, + ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""), + mTouchDisabled ? "T" : "f")); IndentingPrintWriter ipw = asIndenting(pw); ipw.increaseIndent(); ipw.println("gestureExclusionRect:" + calculateGestureExclusionRect()); @@ -4144,127 +4462,359 @@ public final class NotificationPanelViewController extends PanelViewController { mConfigurationListener.onThemeChanged(); } - @Override - public OnLayoutChangeListener createLayoutChangeListener() { + private OnLayoutChangeListener createLayoutChangeListener() { return new OnLayoutChangeListener(); } - @Override - protected TouchHandler createTouchHandler() { - return new TouchHandler() { + @VisibleForTesting + TouchHandler createTouchHandler() { + return new TouchHandler(); + } - private long mLastTouchDownTime = -1L; + public class TouchHandler implements View.OnTouchListener { - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - if (SPEW_LOGCAT) { - Log.v(TAG, - "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX() - + "," + event.getY() + ")"); - } - if (mBlockTouches || mQs.disallowPanelTouches()) { - return false; - } - initDownStates(event); - // Do not let touches go to shade or QS if the bouncer is visible, - // but still let user swipe down to expand the panel, dismissing the bouncer. - if (mCentralSurfaces.isBouncerShowing()) { - return true; - } - if (mCommandQueue.panelsEnabled() - && !mNotificationStackScrollLayoutController.isLongPressInProgress() - && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { - mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); - mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); - return true; - } - if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0) - && mPulseExpansionHandler.onInterceptTouchEvent(event)) { - return true; - } + private long mLastTouchDownTime = -1L; - if (!isFullyCollapsed() && onQsIntercept(event)) { - if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true"); - return true; - } - return super.onInterceptTouchEvent(event); + public boolean onInterceptTouchEvent(MotionEvent event) { + if (SPEW_LOGCAT) { + Log.v(TAG, + "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX() + + "," + event.getY() + ")"); + } + if (mBlockTouches || mQs.disallowPanelTouches()) { + return false; + } + initDownStates(event); + // Do not let touches go to shade or QS if the bouncer is visible, + // but still let user swipe down to expand the panel, dismissing the bouncer. + if (mCentralSurfaces.isBouncerShowing()) { + return true; + } + if (mCommandQueue.panelsEnabled() + && !mNotificationStackScrollLayoutController.isLongPressInProgress() + && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { + mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); + mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); + return true; + } + if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0) + && mPulseExpansionHandler.onInterceptTouchEvent(event)) { + return true; } - @Override - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (event.getDownTime() == mLastTouchDownTime) { - // An issue can occur when swiping down after unlock, where multiple down - // events are received in this handler with identical downTimes. Until the - // source of the issue can be located, detect this case and ignore. - // see b/193350347 - Log.w(TAG, "Duplicate down event detected... ignoring"); + if (!isFullyCollapsed() && onQsIntercept(event)) { + if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true"); + return true; + } + if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted + && event.getActionMasked() != MotionEvent.ACTION_DOWN)) { + return false; + } + + /* + * If the user drags anywhere inside the panel we intercept it if the movement is + * upwards. This allows closing the shade from anywhere inside the panel. + * + * We only do this if the current content is scrolled to the bottom, + * i.e. canCollapsePanelOnTouch() is true and therefore there is no conflicting + * scrolling gesture possible. + */ + int pointerIndex = event.findPointerIndex(mTrackingPointer); + if (pointerIndex < 0) { + pointerIndex = 0; + mTrackingPointer = event.getPointerId(pointerIndex); + } + final float x = event.getX(pointerIndex); + final float y = event.getY(pointerIndex); + boolean canCollapsePanel = canCollapsePanelOnTouch(); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mCentralSurfaces.userActivity(); + mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation; + mMinExpandHeight = 0.0f; + mDownTime = mSystemClock.uptimeMillis(); + if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) { + cancelHeightAnimator(); + mTouchSlopExceeded = true; return true; } - mLastTouchDownTime = event.getDownTime(); + mInitialExpandY = y; + mInitialExpandX = x; + mTouchStartedInEmptyArea = !isInContentBounds(x, y); + mTouchSlopExceeded = mTouchSlopExceededBeforeDown; + mMotionAborted = false; + mPanelClosedOnDown = isFullyCollapsed(); + mCollapsedAndHeadsUpOnDown = false; + mHasLayoutedSinceDown = false; + mUpdateFlingOnLayout = false; + mTouchAboveFalsingThreshold = false; + addMovement(event); + break; + case MotionEvent.ACTION_POINTER_UP: + final int upPointer = event.getPointerId(event.getActionIndex()); + if (mTrackingPointer == upPointer) { + // gesture is ongoing, find a new pointer to track + final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; + mTrackingPointer = event.getPointerId(newIndex); + mInitialExpandX = event.getX(newIndex); + mInitialExpandY = event.getY(newIndex); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: + if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { + mMotionAborted = true; + mVelocityTracker.clear(); + } + break; + case MotionEvent.ACTION_MOVE: + final float h = y - mInitialExpandY; + addMovement(event); + final boolean openShadeWithoutHun = + mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown; + if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown + || openShadeWithoutHun) { + float hAbs = Math.abs(h); + float touchSlop = getTouchSlop(event); + if ((h < -touchSlop + || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop)) + && hAbs > Math.abs(x - mInitialExpandX)) { + cancelHeightAnimator(); + startExpandMotion(x, y, true /* startTracking */, mExpandedHeight); + return true; + } + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mVelocityTracker.clear(); + break; + } + return false; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (event.getDownTime() == mLastTouchDownTime) { + // An issue can occur when swiping down after unlock, where multiple down + // events are received in this handler with identical downTimes. Until the + // source of the issue can be located, detect this case and ignore. + // see b/193350347 + Log.w(TAG, "Duplicate down event detected... ignoring"); + return true; } + mLastTouchDownTime = event.getDownTime(); + } - if (mBlockTouches || (mQsFullyExpanded && mQs != null - && mQs.disallowPanelTouches())) { - return false; - } + if (mBlockTouches || (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches())) { + return false; + } - // Do not allow panel expansion if bouncer is scrimmed or showing over a dream, - // otherwise user would be able to pull down QS or expand the shade. - if (mCentralSurfaces.isBouncerShowingScrimmed() - || mCentralSurfaces.isBouncerShowingOverDream()) { - return false; - } + // Do not allow panel expansion if bouncer is scrimmed or showing over a dream, + // otherwise user would be able to pull down QS or expand the shade. + if (mCentralSurfaces.isBouncerShowingScrimmed() + || mCentralSurfaces.isBouncerShowingOverDream()) { + return false; + } - // Make sure the next touch won't the blocked after the current ends. - if (event.getAction() == MotionEvent.ACTION_UP - || event.getAction() == MotionEvent.ACTION_CANCEL) { - mBlockingExpansionForCurrentTouch = false; - } - // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately - // without any ACTION_MOVE event. - // In such case, simply expand the panel instead of being stuck at the bottom bar. - if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) { - expand(true /* animate */); - } - initDownStates(event); - - // If pulse is expanding already, let's give it the touch. There are situations - // where the panel starts expanding even though we're also pulsing - boolean pulseShouldGetTouch = (!mIsExpanding - && !shouldQuickSettingsIntercept(mDownX, mDownY, 0)) - || mPulseExpansionHandler.isExpanding(); - if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) { - // We're expanding all the other ones shouldn't get this anymore - mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event"); - return true; - } - if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp() - && !mNotificationStackScrollLayoutController.isLongPressInProgress() - && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { - mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); - } - boolean handled = mHeadsUpTouchHelper.onTouchEvent(event); + // Make sure the next touch won't the blocked after the current ends. + if (event.getAction() == MotionEvent.ACTION_UP + || event.getAction() == MotionEvent.ACTION_CANCEL) { + mBlockingExpansionForCurrentTouch = false; + } + // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately + // without any ACTION_MOVE event. + // In such case, simply expand the panel instead of being stuck at the bottom bar. + if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) { + expand(true /* animate */); + } + initDownStates(event); + + // If pulse is expanding already, let's give it the touch. There are situations + // where the panel starts expanding even though we're also pulsing + boolean pulseShouldGetTouch = (!mIsExpanding + && !shouldQuickSettingsIntercept(mDownX, mDownY, 0)) + || mPulseExpansionHandler.isExpanding(); + if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) { + // We're expanding all the other ones shouldn't get this anymore + mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event"); + return true; + } + if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp() + && !mNotificationStackScrollLayoutController.isLongPressInProgress() + && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { + mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); + } + boolean handled = mHeadsUpTouchHelper.onTouchEvent(event); - if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) { - mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event"); - return true; - } - if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) { - mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); - handled = true; + if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) { + mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event"); + return true; + } + if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) { + mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); + handled = true; + } + + if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded() + && mStatusBarKeyguardViewManager.isShowing()) { + mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX()); + } + handled |= handleTouch(v, event); + return !mDozing || mPulsing || handled; + } + + public boolean handleTouch(View v, MotionEvent event) { + if (mInstantExpanding) { + mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding"); + return false; + } + if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { + mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled"); + return false; + } + if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) { + mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted"); + return false; + } + + // If dragging should not expand the notifications shade, then return false. + if (!mNotificationsDragEnabled) { + if (mTracking) { + // Turn off tracking if it's on or the shade can get stuck in the down position. + onTrackingStopped(true /* expand */); } + mShadeLog.logMotionEvent(event, "onTouch: drag not enabled"); + return false; + } - if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded() - && mStatusBarKeyguardViewManager.isShowing()) { - mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX()); + // On expanding, single mouse click expands the panel instead of dragging. + if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) { + if (event.getAction() == MotionEvent.ACTION_UP) { + expand(true); } + return true; + } + + /* + * We capture touch events here and update the expand height here in case according to + * the users fingers. This also handles multi-touch. + * + * Flinging is also enabled in order to open or close the shade. + */ + + int pointerIndex = event.findPointerIndex(mTrackingPointer); + if (pointerIndex < 0) { + pointerIndex = 0; + mTrackingPointer = event.getPointerId(pointerIndex); + } + final float x = event.getX(pointerIndex); + final float y = event.getY(pointerIndex); + + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop(); + mIgnoreXTouchSlop = true; + } - handled |= super.onTouch(v, event); - return !mDozing || mPulsing || handled; + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); + mMinExpandHeight = 0.0f; + mPanelClosedOnDown = isFullyCollapsed(); + mHasLayoutedSinceDown = false; + mUpdateFlingOnLayout = false; + mMotionAborted = false; + mDownTime = mSystemClock.uptimeMillis(); + mTouchAboveFalsingThreshold = false; + mCollapsedAndHeadsUpOnDown = + isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp(); + addMovement(event); + boolean regularHeightAnimationRunning = mHeightAnimator != null + && !mHintAnimationRunning && !mIsSpringBackAnimation; + if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) { + mTouchSlopExceeded = regularHeightAnimationRunning + || mTouchSlopExceededBeforeDown; + cancelHeightAnimator(); + onTrackingStarted(); + } + if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp() + && !mCentralSurfaces.isBouncerShowing()) { + startOpening(event); + } + break; + + case MotionEvent.ACTION_POINTER_UP: + final int upPointer = event.getPointerId(event.getActionIndex()); + if (mTrackingPointer == upPointer) { + // gesture is ongoing, find a new pointer to track + final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; + final float newY = event.getY(newIndex); + final float newX = event.getX(newIndex); + mTrackingPointer = event.getPointerId(newIndex); + mHandlingPointerUp = true; + startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight); + mHandlingPointerUp = false; + } + break; + case MotionEvent.ACTION_POINTER_DOWN: + if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { + mMotionAborted = true; + endMotionEvent(event, x, y, true /* forceCancel */); + return false; + } + break; + case MotionEvent.ACTION_MOVE: + addMovement(event); + float h = y - mInitialTouchY; + + // If the panel was collapsed when touching, we only need to check for the + // y-component of the gesture, as we have no conflicting horizontal gesture. + if (Math.abs(h) > getTouchSlop(event) + && (Math.abs(h) > Math.abs(x - mInitialTouchX) + || mIgnoreXTouchSlop)) { + mTouchSlopExceeded = true; + if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) { + if (mInitialOffsetOnTouch != 0f) { + startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); + h = 0; + } + cancelHeightAnimator(); + onTrackingStarted(); + } + } + float newHeight = Math.max(0, h + mInitialOffsetOnTouch); + newHeight = Math.max(newHeight, mMinExpandHeight); + if (-h >= getFalsingThreshold()) { + mTouchAboveFalsingThreshold = true; + mUpwardsWhenThresholdReached = isDirectionUpwards(x, y); + } + if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) { + // Count h==0 as part of swipe-up, + // otherwise {@link NotificationStackScrollLayout} + // wrongly enables stack height updates at the start of lockscreen swipe-up + mAmbientState.setSwipingUp(h <= 0); + setExpandedHeightInternal(newHeight); + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + addMovement(event); + endMotionEvent(event, x, y, false /* forceCancel */); + // mHeightAnimator is null, there is no remaining frame, ends instrumenting. + if (mHeightAnimator == null) { + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + endJankMonitoring(); + } else { + cancelJankMonitoring(); + } + } + break; } - }; + return !mGestureWaitForTouchSlop || mTracking; + } } private final PhoneStatusBarView.TouchEventHandler mStatusBarViewTouchEventHandler = @@ -4316,8 +4866,7 @@ public final class NotificationPanelViewController extends PanelViewController { } }; - @Override - protected OnConfigurationChangedListener createOnConfigurationChangedListener() { + private OnConfigurationChangedListener createOnConfigurationChangedListener() { return new OnConfigurationChangedListener(); } @@ -4379,6 +4928,585 @@ public final class NotificationPanelViewController extends PanelViewController { .commitUpdate(mDisplayId); } + private void logf(String fmt, Object... args) { + Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); + } + + private void notifyExpandingStarted() { + if (!mExpanding) { + mExpanding = true; + onExpandingStarted(); + } + } + + private void notifyExpandingFinished() { + endClosing(); + if (mExpanding) { + mExpanding = false; + onExpandingFinished(); + } + } + + private float getTouchSlop(MotionEvent event) { + // Adjust the touch slop if another gesture may be being performed. + return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE + ? mTouchSlop * mSlopMultiplier + : mTouchSlop; + } + + private void addMovement(MotionEvent event) { + // Add movement to velocity tracker using raw screen X and Y coordinates instead + // of window coordinates because the window frame may be moving at the same time. + float deltaX = event.getRawX() - event.getX(); + float deltaY = event.getRawY() - event.getY(); + event.offsetLocation(deltaX, deltaY); + mVelocityTracker.addMovement(event); + event.offsetLocation(-deltaX, -deltaY); + } + + public void startExpandLatencyTracking() { + if (mLatencyTracker.isEnabled()) { + mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL); + mExpandLatencyTracking = true; + } + } + + private void startOpening(MotionEvent event) { + updatePanelExpansionAndVisibility(); + maybeVibrateOnOpening(); + + //TODO: keyguard opens QS a different way; log that too? + + // Log the position of the swipe that opened the panel + float width = mCentralSurfaces.getDisplayWidth(); + float height = mCentralSurfaces.getDisplayHeight(); + int rot = mCentralSurfaces.getRotation(); + + mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND, + (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot); + mLockscreenGestureLogger + .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND); + } + + private void maybeVibrateOnOpening() { + if (mVibrateOnOpening) { + mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); + } + } + + /** + * @return whether the swiping direction is upwards and above a 45 degree angle compared to the + * horizontal direction + */ + private boolean isDirectionUpwards(float x, float y) { + float xDiff = x - mInitialExpandX; + float yDiff = y - mInitialExpandY; + if (yDiff >= 0) { + return false; + } + return Math.abs(yDiff) >= Math.abs(xDiff); + } + + public void startExpandMotion(float newX, float newY, boolean startTracking, + float expandedHeight) { + if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) { + beginJankMonitoring(); + } + mInitialOffsetOnTouch = expandedHeight; + mInitialExpandY = newY; + mInitialExpandX = newX; + mInitialTouchFromKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD; + if (startTracking) { + mTouchSlopExceeded = true; + setExpandedHeight(mInitialOffsetOnTouch); + onTrackingStarted(); + } + } + + private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) { + mTrackingPointer = -1; + mAmbientState.setSwipingUp(false); + if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop + || Math.abs(y - mInitialExpandY) > mTouchSlop + || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { + mVelocityTracker.computeCurrentVelocity(1000); + float vel = mVelocityTracker.getYVelocity(); + float vectorVel = (float) Math.hypot( + mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + + final boolean onKeyguard = + mStatusBarStateController.getState() == StatusBarState.KEYGUARD; + + final boolean expand; + if (mKeyguardStateController.isKeyguardFadingAway() + || (mInitialTouchFromKeyguard && !onKeyguard)) { + // Don't expand for any touches that started from the keyguard and ended after the + // keyguard is gone. + expand = false; + } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { + if (onKeyguard) { + expand = true; + } else if (mCentralSurfaces.isBouncerShowingOverDream()) { + expand = false; + } else { + // If we get a cancel, put the shade back to the state it was in when the + // gesture started + expand = !mPanelClosedOnDown; + } + } else { + expand = flingExpands(vel, vectorVel, x, y); + } + + mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold, + mCentralSurfaces.isFalsingThresholdNeeded(), + mCentralSurfaces.isWakeUpComingFromTouch()); + // Log collapse gesture if on lock screen. + if (!expand && onKeyguard) { + float displayDensity = mCentralSurfaces.getDisplayDensity(); + int heightDp = (int) Math.abs((y - mInitialExpandY) / displayDensity); + int velocityDp = (int) Math.abs(vel / displayDensity); + mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp); + mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK); + } + @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC + : y - mInitialExpandY > 0 ? QUICK_SETTINGS + : (mKeyguardStateController.canDismissLockScreen() + ? UNLOCK : BOUNCER_UNLOCK); + + fling(vel, expand, isFalseTouch(x, y, interactionType)); + onTrackingStopped(expand); + mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown; + if (mUpdateFlingOnLayout) { + mUpdateFlingVelocity = vel; + } + } else if (!mCentralSurfaces.isBouncerShowing() + && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating() + && !mKeyguardStateController.isKeyguardGoingAway()) { + boolean expands = onEmptySpaceClick(); + onTrackingStopped(expands); + } + mVelocityTracker.clear(); + } + + private float getCurrentExpandVelocity() { + mVelocityTracker.computeCurrentVelocity(1000); + return mVelocityTracker.getYVelocity(); + } + + private void endClosing() { + if (mClosing) { + setIsClosing(false); + onClosingFinished(); + } + } + + /** + * @param x the final x-coordinate when the finger was lifted + * @param y the final y-coordinate when the finger was lifted + * @return whether this motion should be regarded as a false touch + */ + private boolean isFalseTouch(float x, float y, + @Classifier.InteractionType int interactionType) { + if (!mCentralSurfaces.isFalsingThresholdNeeded()) { + return false; + } + if (mFalsingManager.isClassifierEnabled()) { + return mFalsingManager.isFalseTouch(interactionType); + } + if (!mTouchAboveFalsingThreshold) { + return true; + } + if (mUpwardsWhenThresholdReached) { + return false; + } + return !isDirectionUpwards(x, y); + } + + private void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) { + fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing); + } + + private void fling(float vel, boolean expand, float collapseSpeedUpFactor, + boolean expandBecauseOfFalsing) { + float target = expand ? getMaxPanelHeight() : 0; + if (!expand) { + setIsClosing(true); + } + flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); + } + + private void springBack() { + if (mOverExpansion == 0) { + onFlingEnd(false /* cancelled */); + return; + } + mIsSpringBackAnimation = true; + ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0); + animator.addUpdateListener( + animation -> setOverExpansionInternal((float) animation.getAnimatedValue(), + false /* isFromGesture */)); + animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION); + animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + animator.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + mIsSpringBackAnimation = false; + onFlingEnd(mCancelled); + } + }); + setAnimator(animator); + animator.start(); + } + + public String getName() { + return mViewName; + } + + public void setExpandedHeight(float height) { + if (DEBUG) logf("setExpandedHeight(%.1f)", height); + setExpandedHeightInternal(height); + } + + private void requestPanelHeightUpdate() { + float currentMaxPanelHeight = getMaxPanelHeight(); + + if (isFullyCollapsed()) { + return; + } + + if (currentMaxPanelHeight == mExpandedHeight) { + return; + } + + if (mTracking && !isTrackingBlocked()) { + return; + } + + if (mHeightAnimator != null && !mIsSpringBackAnimation) { + mPanelUpdateWhenAnimatorEnds = true; + return; + } + + setExpandedHeight(currentMaxPanelHeight); + } + + public void setExpandedHeightInternal(float h) { + if (isNaN(h)) { + Log.wtf(TAG, "ExpandedHeight set to NaN"); + } + mNotificationShadeWindowController.batchApplyWindowLayoutParams(() -> { + if (mExpandLatencyTracking && h != 0f) { + DejankUtils.postAfterTraversal( + () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL)); + mExpandLatencyTracking = false; + } + float maxPanelHeight = getMaxPanelHeight(); + if (mHeightAnimator == null) { + // Split shade has its own overscroll logic + if (mTracking && !mInSplitShade) { + float overExpansionPixels = Math.max(0, h - maxPanelHeight); + setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */); + } + mExpandedHeight = Math.min(h, maxPanelHeight); + } else { + mExpandedHeight = h; + } + + // If we are closing the panel and we are almost there due to a slow decelerating + // interpolator, abort the animation. + if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) { + mExpandedHeight = 0f; + if (mHeightAnimator != null) { + mHeightAnimator.end(); + } + } + mExpansionDragDownAmountPx = h; + mExpandedFraction = Math.min(1f, + maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight); + mAmbientState.setExpansionFraction(mExpandedFraction); + onHeightUpdated(mExpandedHeight); + updatePanelExpansionAndVisibility(); + }); + } + + /** + * Set the current overexpansion + * + * @param overExpansion the amount of overexpansion to apply + * @param isFromGesture is this amount from a gesture and needs to be rubberBanded? + */ + private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) { + if (!isFromGesture) { + mLastGesturedOverExpansion = -1; + setOverExpansion(overExpansion); + } else if (mLastGesturedOverExpansion != overExpansion) { + mLastGesturedOverExpansion = overExpansion; + final float heightForFullOvershoot = mView.getHeight() / 3.0f; + float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot); + newExpansion = Interpolators.getOvershootInterpolation(newExpansion); + setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f); + } + } + + public void setExpandedFraction(float frac) { + setExpandedHeight(getMaxPanelHeight() * frac); + } + + public float getExpandedHeight() { + return mExpandedHeight; + } + + public float getExpandedFraction() { + return mExpandedFraction; + } + + public boolean isFullyExpanded() { + return mExpandedHeight >= getMaxPanelHeight(); + } + + public boolean isFullyCollapsed() { + return mExpandedFraction <= 0.0f; + } + + public boolean isCollapsing() { + return mClosing || mIsLaunchAnimationRunning; + } + + public boolean isFlinging() { + return mIsFlinging; + } + + public boolean isTracking() { + return mTracking; + } + + public boolean canPanelBeCollapsed() { + return !isFullyCollapsed() && !mTracking && !mClosing; + } + + private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */, + mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */); + + public void instantCollapse() { + abortAnimations(); + setExpandedFraction(0f); + if (mExpanding) { + notifyExpandingFinished(); + } + if (mInstantExpanding) { + mInstantExpanding = false; + updatePanelExpansionAndVisibility(); + } + } + + private void abortAnimations() { + cancelHeightAnimator(); + mView.removeCallbacks(mFlingCollapseRunnable); + } + + public boolean isUnlockHintRunning() { + return mHintAnimationRunning; + } + + /** + * Phase 1: Move everything upwards. + */ + private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) { + float target = Math.max(0, getMaxPanelHeight() - mHintDistance); + ValueAnimator animator = createHeightAnimator(target); + animator.setDuration(250); + animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + animator.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCancelled) { + setAnimator(null); + onAnimationFinished.run(); + } else { + startUnlockHintAnimationPhase2(onAnimationFinished); + } + } + }); + animator.start(); + setAnimator(animator); + + final List<ViewPropertyAnimator> indicationAnimators = + mKeyguardBottomArea.getIndicationAreaAnimators(); + for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) { + indicationAreaAnimator + .translationY(-mHintDistance) + .setDuration(250) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .withEndAction(() -> indicationAreaAnimator + .translationY(0) + .setDuration(450) + .setInterpolator(mBounceInterpolator) + .start()) + .start(); + } + } + + private void setAnimator(ValueAnimator animator) { + mHeightAnimator = animator; + if (animator == null && mPanelUpdateWhenAnimatorEnds) { + mPanelUpdateWhenAnimatorEnds = false; + requestPanelHeightUpdate(); + } + } + + /** + * Phase 2: Bounce down. + */ + private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) { + ValueAnimator animator = createHeightAnimator(getMaxPanelHeight()); + animator.setDuration(450); + animator.setInterpolator(mBounceInterpolator); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setAnimator(null); + onAnimationFinished.run(); + updatePanelExpansionAndVisibility(); + } + }); + animator.start(); + setAnimator(animator); + } + + private ValueAnimator createHeightAnimator(float targetHeight) { + return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */); + } + + /** + * Create an animator that can also overshoot + * + * @param targetHeight the target height + * @param overshootAmount the amount of overshoot desired + */ + private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) { + float startExpansion = mOverExpansion; + ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight); + animator.addUpdateListener( + animation -> { + if (overshootAmount > 0.0f + // Also remove the overExpansion when collapsing + || (targetHeight == 0.0f && startExpansion != 0)) { + final float expansion = MathUtils.lerp( + startExpansion, + mPanelFlingOvershootAmount * overshootAmount, + Interpolators.FAST_OUT_SLOW_IN.getInterpolation( + animator.getAnimatedFraction())); + setOverExpansionInternal(expansion, false /* isFromGesture */); + } + setExpandedHeightInternal((float) animation.getAnimatedValue()); + }); + return animator; + } + + /** Update the visibility of {@link PanelView} if necessary. */ + public void updateVisibility() { + mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE); + } + + /** + * Updates the panel expansion and {@link PanelView} visibility if necessary. + * + * TODO(b/200063118): Could public calls to this method be replaced with calls to + * {@link #updateVisibility()}? That would allow us to make this method private. + */ + public void updatePanelExpansionAndVisibility() { + mPanelExpansionStateManager.onPanelExpansionChanged( + mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); + updateVisibility(); + } + + public boolean isExpanded() { + return mExpandedFraction > 0f + || mInstantExpanding + || isPanelVisibleBecauseOfHeadsUp() + || mTracking + || mHeightAnimator != null + && !mIsSpringBackAnimation; + } + + /** + * Gets called when the user performs a click anywhere in the empty area of the panel. + * + * @return whether the panel will be expanded after the action performed by this method + */ + private boolean onEmptySpaceClick() { + if (mHintAnimationRunning) { + return true; + } + return onMiddleClicked(); + } + + @VisibleForTesting + boolean isClosing() { + return mClosing; + } + + public void collapseWithDuration(int animationDuration) { + mFixedDuration = animationDuration; + collapse(false /* delayed */, 1.0f /* speedUpFactor */); + mFixedDuration = NO_FIXED_DURATION; + } + + public ViewGroup getView() { + // TODO: remove this method, or at least reduce references to it. + return mView; + } + + private void beginJankMonitoring() { + if (mInteractionJankMonitor == null) { + return; + } + InteractionJankMonitor.Configuration.Builder builder = + InteractionJankMonitor.Configuration.Builder.withView( + InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + mView) + .setTag(isFullyCollapsed() ? "Expand" : "Collapse"); + mInteractionJankMonitor.begin(builder); + } + + private void endJankMonitoring() { + if (mInteractionJankMonitor == null) { + return; + } + InteractionJankMonitor.getInstance().end( + InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); + } + + private void cancelJankMonitoring() { + if (mInteractionJankMonitor == null) { + return; + } + InteractionJankMonitor.getInstance().cancel( + InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); + } + + private float getExpansionFraction() { + return mExpandedFraction; + } + + private PanelExpansionStateManager getPanelExpansionStateManager() { + return mPanelExpansionStateManager; + } + private class OnHeightChangedListener implements ExpandableView.OnHeightChangedListener { @Override public void onHeightChanged(ExpandableView view, boolean needsAnimation) { @@ -4785,13 +5913,19 @@ public final class NotificationPanelViewController extends PanelViewController { } } - private class OnLayoutChangeListener extends PanelViewController.OnLayoutChangeListener { + private class OnLayoutChangeListener implements View.OnLayoutChangeListener { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { DejankUtils.startDetectingBlockingIpcs("NVP#onLayout"); - super.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom); + requestPanelHeightUpdate(); + mHasLayoutedSinceDown = true; + if (mUpdateFlingOnLayout) { + abortAnimations(); + fling(mUpdateFlingVelocity, true /* expands */); + mUpdateFlingOnLayout = false; + } updateMaxDisplayedNotifications(!shouldAvoidChangingNotificationsCount()); setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth()); @@ -5050,4 +6184,12 @@ public final class NotificationPanelViewController extends PanelViewController { } } } + + public class OnConfigurationChangedListener implements + PanelView.OnConfigurationChangedListener { + @Override + public void onConfigurationChanged(Configuration newConfig) { + loadDimens(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/PanelView.java b/packages/SystemUI/src/com/android/systemui/shade/PanelView.java index efff0db742d7..4349d816b3c3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/PanelView.java +++ b/packages/SystemUI/src/com/android/systemui/shade/PanelView.java @@ -29,7 +29,7 @@ import com.android.systemui.statusbar.phone.KeyguardBottomAreaView; public abstract class PanelView extends FrameLayout { public static final boolean DEBUG = false; public static final String TAG = PanelView.class.getSimpleName(); - private PanelViewController.TouchHandler mTouchHandler; + private NotificationPanelViewController.TouchHandler mTouchHandler; protected CentralSurfaces mCentralSurfaces; protected HeadsUpManagerPhone mHeadsUpManager; @@ -49,7 +49,7 @@ public abstract class PanelView extends FrameLayout { super(context, attrs, defStyleAttr); } - public void setOnTouchListener(PanelViewController.TouchHandler touchHandler) { + public void setOnTouchListener(NotificationPanelViewController.TouchHandler touchHandler) { super.setOnTouchListener(touchHandler); mTouchHandler = touchHandler; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java deleted file mode 100644 index 73eaa852e345..000000000000 --- a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java +++ /dev/null @@ -1,1489 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.shade; - -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; - -import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; -import static com.android.systemui.classifier.Classifier.GENERIC; -import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; -import static com.android.systemui.classifier.Classifier.UNLOCK; -import static com.android.systemui.shade.PanelView.DEBUG; - -import static java.lang.Float.isNaN; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.os.VibrationEffect; -import android.util.Log; -import android.util.MathUtils; -import android.view.InputDevice; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewPropertyAnimator; -import android.view.ViewTreeObserver; -import android.view.animation.Interpolator; - -import com.android.internal.jank.InteractionJankMonitor; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.util.LatencyTracker; -import com.android.systemui.DejankUtils; -import com.android.systemui.R; -import com.android.systemui.animation.Interpolators; -import com.android.systemui.classifier.Classifier; -import com.android.systemui.doze.DozeLog; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.statusbar.NotificationShadeWindowController; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.SysuiStatusBarStateController; -import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.notification.stack.AmbientState; -import com.android.systemui.statusbar.phone.BounceInterpolator; -import com.android.systemui.statusbar.phone.CentralSurfaces; -import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; -import com.android.systemui.statusbar.phone.KeyguardBottomAreaView; -import com.android.systemui.statusbar.phone.LockscreenGestureLogger; -import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.time.SystemClock; -import com.android.wm.shell.animation.FlingAnimationUtils; - -import java.io.PrintWriter; -import java.util.List; - -public abstract class PanelViewController { - public static final String TAG = PanelView.class.getSimpleName(); - public static final float FLING_MAX_LENGTH_SECONDS = 0.6f; - public static final float FLING_SPEED_UP_FACTOR = 0.6f; - public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f; - public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f; - private static final int NO_FIXED_DURATION = -1; - private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L; - private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L; - - /** - * The factor of the usual high velocity that is needed in order to reach the maximum overshoot - * when flinging. A low value will make it that most flings will reach the maximum overshoot. - */ - private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f; - - protected long mDownTime; - protected boolean mTouchSlopExceededBeforeDown; - private float mMinExpandHeight; - private boolean mPanelUpdateWhenAnimatorEnds; - private final boolean mVibrateOnOpening; - protected boolean mIsLaunchAnimationRunning; - private int mFixedDuration = NO_FIXED_DURATION; - protected float mOverExpansion; - - /** - * The overshoot amount when the panel flings open - */ - private float mPanelFlingOvershootAmount; - - /** - * The amount of pixels that we have overexpanded the last time with a gesture - */ - private float mLastGesturedOverExpansion = -1; - - /** - * Is the current animator the spring back animation? - */ - private boolean mIsSpringBackAnimation; - - private boolean mInSplitShade; - - private void logf(String fmt, Object... args) { - Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); - } - - protected CentralSurfaces mCentralSurfaces; - protected HeadsUpManagerPhone mHeadsUpManager; - protected final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager; - - private float mHintDistance; - private float mInitialOffsetOnTouch; - private boolean mCollapsedAndHeadsUpOnDown; - private float mExpandedFraction = 0; - private float mExpansionDragDownAmountPx = 0; - protected float mExpandedHeight = 0; - private boolean mPanelClosedOnDown; - private boolean mHasLayoutedSinceDown; - private float mUpdateFlingVelocity; - private boolean mUpdateFlingOnLayout; - private boolean mClosing; - protected boolean mTracking; - private boolean mTouchSlopExceeded; - private int mTrackingPointer; - private int mTouchSlop; - private float mSlopMultiplier; - protected boolean mHintAnimationRunning; - private boolean mTouchAboveFalsingThreshold; - private int mUnlockFalsingThreshold; - private boolean mTouchStartedInEmptyArea; - private boolean mMotionAborted; - private boolean mUpwardsWhenThresholdReached; - private boolean mAnimatingOnDown; - private boolean mHandlingPointerUp; - - private ValueAnimator mHeightAnimator; - private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); - private final FlingAnimationUtils mFlingAnimationUtils; - private final FlingAnimationUtils mFlingAnimationUtilsClosing; - private final FlingAnimationUtils mFlingAnimationUtilsDismissing; - private final LatencyTracker mLatencyTracker; - private final FalsingManager mFalsingManager; - private final DozeLog mDozeLog; - private final VibratorHelper mVibratorHelper; - - /** - * Whether an instant expand request is currently pending and we are just waiting for layout. - */ - private boolean mInstantExpanding; - private boolean mAnimateAfterExpanding; - private boolean mIsFlinging; - - private String mViewName; - private float mInitialTouchY; - private float mInitialTouchX; - private boolean mTouchDisabled; - private boolean mInitialTouchFromKeyguard; - - /** - * Whether or not the PanelView can be expanded or collapsed with a drag. - */ - private final boolean mNotificationsDragEnabled; - - private final Interpolator mBounceInterpolator; - protected KeyguardBottomAreaView mKeyguardBottomArea; - - /** - * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. - */ - private float mNextCollapseSpeedUpFactor = 1.0f; - - protected boolean mExpanding; - private boolean mGestureWaitForTouchSlop; - private boolean mIgnoreXTouchSlop; - private boolean mExpandLatencyTracking; - private final PanelView mView; - private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private final NotificationShadeWindowController mNotificationShadeWindowController; - protected final Resources mResources; - protected final KeyguardStateController mKeyguardStateController; - protected final SysuiStatusBarStateController mStatusBarStateController; - protected final AmbientState mAmbientState; - protected final LockscreenGestureLogger mLockscreenGestureLogger; - private final PanelExpansionStateManager mPanelExpansionStateManager; - private final InteractionJankMonitor mInteractionJankMonitor; - protected final SystemClock mSystemClock; - - protected final ShadeLogger mShadeLog; - - protected abstract void onExpandingFinished(); - - protected void onExpandingStarted() { - } - - protected void notifyExpandingStarted() { - if (!mExpanding) { - mExpanding = true; - onExpandingStarted(); - } - } - - protected final void notifyExpandingFinished() { - endClosing(); - if (mExpanding) { - mExpanding = false; - onExpandingFinished(); - } - } - - protected AmbientState getAmbientState() { - return mAmbientState; - } - - public PanelViewController( - PanelView view, - FalsingManager falsingManager, - DozeLog dozeLog, - KeyguardStateController keyguardStateController, - SysuiStatusBarStateController statusBarStateController, - NotificationShadeWindowController notificationShadeWindowController, - VibratorHelper vibratorHelper, - StatusBarKeyguardViewManager statusBarKeyguardViewManager, - LatencyTracker latencyTracker, - FlingAnimationUtils.Builder flingAnimationUtilsBuilder, - StatusBarTouchableRegionManager statusBarTouchableRegionManager, - LockscreenGestureLogger lockscreenGestureLogger, - PanelExpansionStateManager panelExpansionStateManager, - AmbientState ambientState, - InteractionJankMonitor interactionJankMonitor, - ShadeLogger shadeLogger, - SystemClock systemClock) { - keyguardStateController.addCallback(new KeyguardStateController.Callback() { - @Override - public void onKeyguardFadingAwayChanged() { - requestPanelHeightUpdate(); - } - }); - mAmbientState = ambientState; - mView = view; - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; - mLockscreenGestureLogger = lockscreenGestureLogger; - mPanelExpansionStateManager = panelExpansionStateManager; - mShadeLog = shadeLogger; - TouchHandler touchHandler = createTouchHandler(); - mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - mViewName = mResources.getResourceName(mView.getId()); - } - - @Override - public void onViewDetachedFromWindow(View v) { - } - }); - - mView.addOnLayoutChangeListener(createLayoutChangeListener()); - mView.setOnTouchListener(touchHandler); - mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener()); - - mResources = mView.getResources(); - mKeyguardStateController = keyguardStateController; - mStatusBarStateController = statusBarStateController; - mNotificationShadeWindowController = notificationShadeWindowController; - mFlingAnimationUtils = flingAnimationUtilsBuilder - .reset() - .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS) - .setSpeedUpFactor(FLING_SPEED_UP_FACTOR) - .build(); - mFlingAnimationUtilsClosing = flingAnimationUtilsBuilder - .reset() - .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS) - .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR) - .build(); - mFlingAnimationUtilsDismissing = flingAnimationUtilsBuilder - .reset() - .setMaxLengthSeconds(0.5f) - .setSpeedUpFactor(0.6f) - .setX2(0.6f) - .setY2(0.84f) - .build(); - mLatencyTracker = latencyTracker; - mBounceInterpolator = new BounceInterpolator(); - mFalsingManager = falsingManager; - mDozeLog = dozeLog; - mNotificationsDragEnabled = mResources.getBoolean( - R.bool.config_enableNotificationShadeDrag); - mVibratorHelper = vibratorHelper; - mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation); - mStatusBarTouchableRegionManager = statusBarTouchableRegionManager; - mInteractionJankMonitor = interactionJankMonitor; - mSystemClock = systemClock; - } - - protected void loadDimens() { - final ViewConfiguration configuration = ViewConfiguration.get(mView.getContext()); - mTouchSlop = configuration.getScaledTouchSlop(); - mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier(); - mHintDistance = mResources.getDimension(R.dimen.hint_move_distance); - mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount); - mUnlockFalsingThreshold = - mResources.getDimensionPixelSize(R.dimen.unlock_falsing_threshold); - mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade); - } - - protected float getTouchSlop(MotionEvent event) { - // Adjust the touch slop if another gesture may be being performed. - return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE - ? mTouchSlop * mSlopMultiplier - : mTouchSlop; - } - - private void addMovement(MotionEvent event) { - // Add movement to velocity tracker using raw screen X and Y coordinates instead - // of window coordinates because the window frame may be moving at the same time. - float deltaX = event.getRawX() - event.getX(); - float deltaY = event.getRawY() - event.getY(); - event.offsetLocation(deltaX, deltaY); - mVelocityTracker.addMovement(event); - event.offsetLocation(-deltaX, -deltaY); - } - - public void setTouchAndAnimationDisabled(boolean disabled) { - mTouchDisabled = disabled; - if (mTouchDisabled) { - cancelHeightAnimator(); - if (mTracking) { - onTrackingStopped(true /* expanded */); - } - notifyExpandingFinished(); - } - } - - public void startExpandLatencyTracking() { - if (mLatencyTracker.isEnabled()) { - mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL); - mExpandLatencyTracking = true; - } - } - - private void startOpening(MotionEvent event) { - updatePanelExpansionAndVisibility(); - maybeVibrateOnOpening(); - - //TODO: keyguard opens QS a different way; log that too? - - // Log the position of the swipe that opened the panel - float width = mCentralSurfaces.getDisplayWidth(); - float height = mCentralSurfaces.getDisplayHeight(); - int rot = mCentralSurfaces.getRotation(); - - mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND, - (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot); - mLockscreenGestureLogger - .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND); - } - - protected void maybeVibrateOnOpening() { - if (mVibrateOnOpening) { - mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); - } - } - - protected abstract float getOpeningHeight(); - - /** - * @return whether the swiping direction is upwards and above a 45 degree angle compared to the - * horizontal direction - */ - private boolean isDirectionUpwards(float x, float y) { - float xDiff = x - mInitialTouchX; - float yDiff = y - mInitialTouchY; - if (yDiff >= 0) { - return false; - } - return Math.abs(yDiff) >= Math.abs(xDiff); - } - - public void startExpandMotion(float newX, float newY, boolean startTracking, - float expandedHeight) { - if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) { - beginJankMonitoring(); - } - mInitialOffsetOnTouch = expandedHeight; - mInitialTouchY = newY; - mInitialTouchX = newX; - mInitialTouchFromKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD; - if (startTracking) { - mTouchSlopExceeded = true; - setExpandedHeight(mInitialOffsetOnTouch); - onTrackingStarted(); - } - } - - private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) { - mTrackingPointer = -1; - mAmbientState.setSwipingUp(false); - if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialTouchX) > mTouchSlop - || Math.abs(y - mInitialTouchY) > mTouchSlop - || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { - mVelocityTracker.computeCurrentVelocity(1000); - float vel = mVelocityTracker.getYVelocity(); - float vectorVel = (float) Math.hypot( - mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); - - final boolean onKeyguard = - mStatusBarStateController.getState() == StatusBarState.KEYGUARD; - - final boolean expand; - if (mKeyguardStateController.isKeyguardFadingAway() - || (mInitialTouchFromKeyguard && !onKeyguard)) { - // Don't expand for any touches that started from the keyguard and ended after the - // keyguard is gone. - expand = false; - } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { - if (onKeyguard) { - expand = true; - } else if (mCentralSurfaces.isBouncerShowingOverDream()) { - expand = false; - } else { - // If we get a cancel, put the shade back to the state it was in when the - // gesture started - expand = !mPanelClosedOnDown; - } - } else { - expand = flingExpands(vel, vectorVel, x, y); - } - - mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold, - mCentralSurfaces.isFalsingThresholdNeeded(), - mCentralSurfaces.isWakeUpComingFromTouch()); - // Log collapse gesture if on lock screen. - if (!expand && onKeyguard) { - float displayDensity = mCentralSurfaces.getDisplayDensity(); - int heightDp = (int) Math.abs((y - mInitialTouchY) / displayDensity); - int velocityDp = (int) Math.abs(vel / displayDensity); - mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp); - mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK); - } - @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC - : y - mInitialTouchY > 0 ? QUICK_SETTINGS - : (mKeyguardStateController.canDismissLockScreen() - ? UNLOCK : BOUNCER_UNLOCK); - - fling(vel, expand, isFalseTouch(x, y, interactionType)); - onTrackingStopped(expand); - mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown; - if (mUpdateFlingOnLayout) { - mUpdateFlingVelocity = vel; - } - } else if (!mCentralSurfaces.isBouncerShowing() - && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating() - && !mKeyguardStateController.isKeyguardGoingAway()) { - boolean expands = onEmptySpaceClick(); - onTrackingStopped(expands); - } - mVelocityTracker.clear(); - } - - protected float getCurrentExpandVelocity() { - mVelocityTracker.computeCurrentVelocity(1000); - return mVelocityTracker.getYVelocity(); - } - - private int getFalsingThreshold() { - float factor = mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f; - return (int) (mUnlockFalsingThreshold * factor); - } - - protected abstract boolean shouldGestureWaitForTouchSlop(); - - protected void onTrackingStopped(boolean expand) { - mTracking = false; - mCentralSurfaces.onTrackingStopped(expand); - updatePanelExpansionAndVisibility(); - } - - protected void onTrackingStarted() { - endClosing(); - mTracking = true; - mCentralSurfaces.onTrackingStarted(); - notifyExpandingStarted(); - updatePanelExpansionAndVisibility(); - } - - /** - * @return Whether a pair of coordinates are inside the visible view content bounds. - */ - protected abstract boolean isInContentBounds(float x, float y); - - protected void cancelHeightAnimator() { - if (mHeightAnimator != null) { - if (mHeightAnimator.isRunning()) { - mPanelUpdateWhenAnimatorEnds = false; - } - mHeightAnimator.cancel(); - } - endClosing(); - } - - private void endClosing() { - if (mClosing) { - setIsClosing(false); - onClosingFinished(); - } - } - - protected boolean canCollapsePanelOnTouch() { - return true; - } - - protected float getContentHeight() { - return mExpandedHeight; - } - - /** - * @param vel the current vertical velocity of the motion - * @param vectorVel the length of the vectorial velocity - * @return whether a fling should expands the panel; contracts otherwise - */ - protected boolean flingExpands(float vel, float vectorVel, float x, float y) { - if (mFalsingManager.isUnlockingDisabled()) { - return true; - } - - @Classifier.InteractionType int interactionType = y - mInitialTouchY > 0 - ? QUICK_SETTINGS : ( - mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK); - - if (isFalseTouch(x, y, interactionType)) { - return true; - } - if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) { - return shouldExpandWhenNotFlinging(); - } else { - return vel > 0; - } - } - - protected boolean shouldExpandWhenNotFlinging() { - return getExpandedFraction() > 0.5f; - } - - /** - * @param x the final x-coordinate when the finger was lifted - * @param y the final y-coordinate when the finger was lifted - * @return whether this motion should be regarded as a false touch - */ - private boolean isFalseTouch(float x, float y, - @Classifier.InteractionType int interactionType) { - if (!mCentralSurfaces.isFalsingThresholdNeeded()) { - return false; - } - if (mFalsingManager.isClassifierEnabled()) { - return mFalsingManager.isFalseTouch(interactionType); - } - if (!mTouchAboveFalsingThreshold) { - return true; - } - if (mUpwardsWhenThresholdReached) { - return false; - } - return !isDirectionUpwards(x, y); - } - - protected void fling(float vel, boolean expand) { - fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false); - } - - protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) { - fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing); - } - - protected void fling(float vel, boolean expand, float collapseSpeedUpFactor, - boolean expandBecauseOfFalsing) { - float target = expand ? getMaxPanelHeight() : 0; - if (!expand) { - setIsClosing(true); - } - flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); - } - - protected void flingToHeight(float vel, boolean expand, float target, - float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) { - if (target == mExpandedHeight && mOverExpansion == 0.0f) { - // We're at the target and didn't fling and there's no overshoot - onFlingEnd(false /* cancelled */); - return; - } - mIsFlinging = true; - // we want to perform an overshoot animation when flinging open - final boolean addOverscroll = - expand - && !mInSplitShade // Split shade has its own overscroll logic - && mStatusBarStateController.getState() != StatusBarState.KEYGUARD - && mOverExpansion == 0.0f - && vel >= 0; - final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand); - float overshootAmount = 0.0f; - if (addOverscroll) { - // Let's overshoot depending on the amount of velocity - overshootAmount = MathUtils.lerp( - 0.2f, - 1.0f, - MathUtils.saturate(vel - / (mFlingAnimationUtils.getHighVelocityPxPerSecond() - * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT))); - overshootAmount += mOverExpansion / mPanelFlingOvershootAmount; - } - ValueAnimator animator = createHeightAnimator(target, overshootAmount); - if (expand) { - if (expandBecauseOfFalsing && vel < 0) { - vel = 0; - } - mFlingAnimationUtils.apply(animator, mExpandedHeight, - target + overshootAmount * mPanelFlingOvershootAmount, vel, mView.getHeight()); - if (vel == 0) { - animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION); - } - } else { - if (shouldUseDismissingAnimation()) { - if (vel == 0) { - animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); - long duration = (long) (200 + mExpandedHeight / mView.getHeight() * 100); - animator.setDuration(duration); - } else { - mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel, - mView.getHeight()); - } - } else { - mFlingAnimationUtilsClosing.apply( - animator, mExpandedHeight, target, vel, mView.getHeight()); - } - - // Make it shorter if we run a canned animation - if (vel == 0) { - animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor)); - } - if (mFixedDuration != NO_FIXED_DURATION) { - animator.setDuration(mFixedDuration); - } - } - animator.addListener(new AnimatorListenerAdapter() { - private boolean mCancelled; - - @Override - public void onAnimationStart(Animator animation) { - if (!mStatusBarStateController.isDozing()) { - beginJankMonitoring(); - } - } - - @Override - public void onAnimationCancel(Animator animation) { - mCancelled = true; - } - - @Override - public void onAnimationEnd(Animator animation) { - if (shouldSpringBack && !mCancelled) { - // After the shade is flinged open to an overscrolled state, spring back - // the shade by reducing section padding to 0. - springBack(); - } else { - onFlingEnd(mCancelled); - } - } - }); - setAnimator(animator); - animator.start(); - } - - private void springBack() { - if (mOverExpansion == 0) { - onFlingEnd(false /* cancelled */); - return; - } - mIsSpringBackAnimation = true; - ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0); - animator.addUpdateListener( - animation -> setOverExpansionInternal((float) animation.getAnimatedValue(), - false /* isFromGesture */)); - animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION); - animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - animator.addListener(new AnimatorListenerAdapter() { - private boolean mCancelled; - @Override - public void onAnimationCancel(Animator animation) { - mCancelled = true; - } - @Override - public void onAnimationEnd(Animator animation) { - mIsSpringBackAnimation = false; - onFlingEnd(mCancelled); - } - }); - setAnimator(animator); - animator.start(); - } - - protected void onFlingEnd(boolean cancelled) { - mIsFlinging = false; - // No overshoot when the animation ends - setOverExpansionInternal(0, false /* isFromGesture */); - setAnimator(null); - mKeyguardStateController.notifyPanelFlingEnd(); - if (!cancelled) { - endJankMonitoring(); - notifyExpandingFinished(); - } else { - cancelJankMonitoring(); - } - updatePanelExpansionAndVisibility(); - } - - protected abstract boolean shouldUseDismissingAnimation(); - - public String getName() { - return mViewName; - } - - public void setExpandedHeight(float height) { - if (DEBUG) logf("setExpandedHeight(%.1f)", height); - setExpandedHeightInternal(height); - } - - protected void requestPanelHeightUpdate() { - float currentMaxPanelHeight = getMaxPanelHeight(); - - if (isFullyCollapsed()) { - return; - } - - if (currentMaxPanelHeight == mExpandedHeight) { - return; - } - - if (mTracking && !isTrackingBlocked()) { - return; - } - - if (mHeightAnimator != null && !mIsSpringBackAnimation) { - mPanelUpdateWhenAnimatorEnds = true; - return; - } - - setExpandedHeight(currentMaxPanelHeight); - } - - public void setExpandedHeightInternal(float h) { - if (isNaN(h)) { - Log.wtf(TAG, "ExpandedHeight set to NaN"); - } - mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { - if (mExpandLatencyTracking && h != 0f) { - DejankUtils.postAfterTraversal( - () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL)); - mExpandLatencyTracking = false; - } - float maxPanelHeight = getMaxPanelHeight(); - if (mHeightAnimator == null) { - // Split shade has its own overscroll logic - if (mTracking && !mInSplitShade) { - float overExpansionPixels = Math.max(0, h - maxPanelHeight); - setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */); - } - mExpandedHeight = Math.min(h, maxPanelHeight); - } else { - mExpandedHeight = h; - } - - // If we are closing the panel and we are almost there due to a slow decelerating - // interpolator, abort the animation. - if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) { - mExpandedHeight = 0f; - if (mHeightAnimator != null) { - mHeightAnimator.end(); - } - } - mExpansionDragDownAmountPx = h; - mExpandedFraction = Math.min(1f, - maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight); - mAmbientState.setExpansionFraction(mExpandedFraction); - onHeightUpdated(mExpandedHeight); - updatePanelExpansionAndVisibility(); - }); - } - - /** - * @return true if the panel tracking should be temporarily blocked; this is used when a - * conflicting gesture (opening QS) is happening - */ - protected abstract boolean isTrackingBlocked(); - - protected void setOverExpansion(float overExpansion) { - mOverExpansion = overExpansion; - } - - /** - * Set the current overexpansion - * - * @param overExpansion the amount of overexpansion to apply - * @param isFromGesture is this amount from a gesture and needs to be rubberBanded? - */ - private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) { - if (!isFromGesture) { - mLastGesturedOverExpansion = -1; - setOverExpansion(overExpansion); - } else if (mLastGesturedOverExpansion != overExpansion) { - mLastGesturedOverExpansion = overExpansion; - final float heightForFullOvershoot = mView.getHeight() / 3.0f; - float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot); - newExpansion = Interpolators.getOvershootInterpolation(newExpansion); - setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f); - } - } - - protected abstract void onHeightUpdated(float expandedHeight); - - /** - * This returns the maximum height of the panel. Children should override this if their - * desired height is not the full height. - * - * @return the default implementation simply returns the maximum height. - */ - protected abstract int getMaxPanelHeight(); - - public void setExpandedFraction(float frac) { - setExpandedHeight(getMaxPanelHeight() * frac); - } - - public float getExpandedHeight() { - return mExpandedHeight; - } - - public float getExpandedFraction() { - return mExpandedFraction; - } - - public boolean isFullyExpanded() { - return mExpandedHeight >= getMaxPanelHeight(); - } - - public boolean isFullyCollapsed() { - return mExpandedFraction <= 0.0f; - } - - public boolean isCollapsing() { - return mClosing || mIsLaunchAnimationRunning; - } - - public boolean isFlinging() { - return mIsFlinging; - } - - public boolean isTracking() { - return mTracking; - } - - public void collapse(boolean delayed, float speedUpFactor) { - if (DEBUG) logf("collapse: " + this); - if (canPanelBeCollapsed()) { - cancelHeightAnimator(); - notifyExpandingStarted(); - - // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state. - setIsClosing(true); - if (delayed) { - mNextCollapseSpeedUpFactor = speedUpFactor; - mView.postDelayed(mFlingCollapseRunnable, 120); - } else { - fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */); - } - } - } - - public boolean canPanelBeCollapsed() { - return !isFullyCollapsed() && !mTracking && !mClosing; - } - - private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */, - mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */); - - public void expand(final boolean animate) { - if (!isFullyCollapsed() && !isCollapsing()) { - return; - } - - mInstantExpanding = true; - mAnimateAfterExpanding = animate; - mUpdateFlingOnLayout = false; - abortAnimations(); - if (mTracking) { - onTrackingStopped(true /* expands */); // The panel is expanded after this call. - } - if (mExpanding) { - notifyExpandingFinished(); - } - updatePanelExpansionAndVisibility(); - - // Wait for window manager to pickup the change, so we know the maximum height of the panel - // then. - mView.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - if (!mInstantExpanding) { - mView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - return; - } - if (mCentralSurfaces.getNotificationShadeWindowView().isVisibleToUser()) { - mView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - if (mAnimateAfterExpanding) { - notifyExpandingStarted(); - beginJankMonitoring(); - fling(0, true /* expand */); - } else { - setExpandedFraction(1f); - } - mInstantExpanding = false; - } - } - }); - - // Make sure a layout really happens. - mView.requestLayout(); - } - - public void instantCollapse() { - abortAnimations(); - setExpandedFraction(0f); - if (mExpanding) { - notifyExpandingFinished(); - } - if (mInstantExpanding) { - mInstantExpanding = false; - updatePanelExpansionAndVisibility(); - } - } - - private void abortAnimations() { - cancelHeightAnimator(); - mView.removeCallbacks(mFlingCollapseRunnable); - } - - protected abstract void onClosingFinished(); - - protected void startUnlockHintAnimation() { - - // We don't need to hint the user if an animation is already running or the user is changing - // the expansion. - if (mHeightAnimator != null || mTracking) { - return; - } - notifyExpandingStarted(); - startUnlockHintAnimationPhase1(() -> { - notifyExpandingFinished(); - onUnlockHintFinished(); - mHintAnimationRunning = false; - }); - onUnlockHintStarted(); - mHintAnimationRunning = true; - } - - protected void onUnlockHintFinished() { - mCentralSurfaces.onHintFinished(); - } - - protected void onUnlockHintStarted() { - mCentralSurfaces.onUnlockHintStarted(); - } - - public boolean isUnlockHintRunning() { - return mHintAnimationRunning; - } - - /** - * Phase 1: Move everything upwards. - */ - private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) { - float target = Math.max(0, getMaxPanelHeight() - mHintDistance); - ValueAnimator animator = createHeightAnimator(target); - animator.setDuration(250); - animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - animator.addListener(new AnimatorListenerAdapter() { - private boolean mCancelled; - - @Override - public void onAnimationCancel(Animator animation) { - mCancelled = true; - } - - @Override - public void onAnimationEnd(Animator animation) { - if (mCancelled) { - setAnimator(null); - onAnimationFinished.run(); - } else { - startUnlockHintAnimationPhase2(onAnimationFinished); - } - } - }); - animator.start(); - setAnimator(animator); - - final List<ViewPropertyAnimator> indicationAnimators = - mKeyguardBottomArea.getIndicationAreaAnimators(); - for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) { - indicationAreaAnimator - .translationY(-mHintDistance) - .setDuration(250) - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .withEndAction(() -> indicationAreaAnimator - .translationY(0) - .setDuration(450) - .setInterpolator(mBounceInterpolator) - .start()) - .start(); - } - } - - private void setAnimator(ValueAnimator animator) { - mHeightAnimator = animator; - if (animator == null && mPanelUpdateWhenAnimatorEnds) { - mPanelUpdateWhenAnimatorEnds = false; - requestPanelHeightUpdate(); - } - } - - /** - * Phase 2: Bounce down. - */ - private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) { - ValueAnimator animator = createHeightAnimator(getMaxPanelHeight()); - animator.setDuration(450); - animator.setInterpolator(mBounceInterpolator); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - setAnimator(null); - onAnimationFinished.run(); - updatePanelExpansionAndVisibility(); - } - }); - animator.start(); - setAnimator(animator); - } - - private ValueAnimator createHeightAnimator(float targetHeight) { - return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */); - } - - /** - * Create an animator that can also overshoot - * - * @param targetHeight the target height - * @param overshootAmount the amount of overshoot desired - */ - private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) { - float startExpansion = mOverExpansion; - ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight); - animator.addUpdateListener( - animation -> { - if (overshootAmount > 0.0f - // Also remove the overExpansion when collapsing - || (targetHeight == 0.0f && startExpansion != 0)) { - final float expansion = MathUtils.lerp( - startExpansion, - mPanelFlingOvershootAmount * overshootAmount, - Interpolators.FAST_OUT_SLOW_IN.getInterpolation( - animator.getAnimatedFraction())); - setOverExpansionInternal(expansion, false /* isFromGesture */); - } - setExpandedHeightInternal((float) animation.getAnimatedValue()); - }); - return animator; - } - - /** Update the visibility of {@link PanelView} if necessary. */ - public void updateVisibility() { - mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE); - } - - /** Returns true if {@link PanelView} should be visible. */ - abstract protected boolean shouldPanelBeVisible(); - - /** - * Updates the panel expansion and {@link PanelView} visibility if necessary. - * - * TODO(b/200063118): Could public calls to this method be replaced with calls to - * {@link #updateVisibility()}? That would allow us to make this method private. - */ - public void updatePanelExpansionAndVisibility() { - mPanelExpansionStateManager.onPanelExpansionChanged( - mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); - updateVisibility(); - } - - public boolean isExpanded() { - return mExpandedFraction > 0f - || mInstantExpanding - || isPanelVisibleBecauseOfHeadsUp() - || mTracking - || mHeightAnimator != null - && !mIsSpringBackAnimation; - } - - protected abstract boolean isPanelVisibleBecauseOfHeadsUp(); - - /** - * Gets called when the user performs a click anywhere in the empty area of the panel. - * - * @return whether the panel will be expanded after the action performed by this method - */ - protected boolean onEmptySpaceClick() { - if (mHintAnimationRunning) { - return true; - } - return onMiddleClicked(); - } - - protected abstract boolean onMiddleClicked(); - - protected abstract boolean isDozing(); - - public void dump(PrintWriter pw, String[] args) { - pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s" - + " tracking=%s timeAnim=%s%s " - + "touchDisabled=%s" + "]", - this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(), - mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator, - ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""), - mTouchDisabled ? "T" : "f")); - } - - public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) { - mHeadsUpManager = headsUpManager; - } - - public void setIsLaunchAnimationRunning(boolean running) { - mIsLaunchAnimationRunning = running; - } - - protected void setIsClosing(boolean isClosing) { - mClosing = isClosing; - } - - protected boolean isClosing() { - return mClosing; - } - - public void collapseWithDuration(int animationDuration) { - mFixedDuration = animationDuration; - collapse(false /* delayed */, 1.0f /* speedUpFactor */); - mFixedDuration = NO_FIXED_DURATION; - } - - public ViewGroup getView() { - // TODO: remove this method, or at least reduce references to it. - return mView; - } - - public OnLayoutChangeListener createLayoutChangeListener() { - return new OnLayoutChangeListener(); - } - - protected abstract TouchHandler createTouchHandler(); - - protected OnConfigurationChangedListener createOnConfigurationChangedListener() { - return new OnConfigurationChangedListener(); - } - - public class TouchHandler implements View.OnTouchListener { - - public boolean onInterceptTouchEvent(MotionEvent event) { - if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted - && event.getActionMasked() != MotionEvent.ACTION_DOWN)) { - return false; - } - - /* - * If the user drags anywhere inside the panel we intercept it if the movement is - * upwards. This allows closing the shade from anywhere inside the panel. - * - * We only do this if the current content is scrolled to the bottom, - * i.e canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling - * gesture - * possible. - */ - int pointerIndex = event.findPointerIndex(mTrackingPointer); - if (pointerIndex < 0) { - pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); - } - final float x = event.getX(pointerIndex); - final float y = event.getY(pointerIndex); - boolean canCollapsePanel = canCollapsePanelOnTouch(); - - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mCentralSurfaces.userActivity(); - mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation; - mMinExpandHeight = 0.0f; - mDownTime = mSystemClock.uptimeMillis(); - if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) { - cancelHeightAnimator(); - mTouchSlopExceeded = true; - return true; - } - mInitialTouchY = y; - mInitialTouchX = x; - mTouchStartedInEmptyArea = !isInContentBounds(x, y); - mTouchSlopExceeded = mTouchSlopExceededBeforeDown; - mMotionAborted = false; - mPanelClosedOnDown = isFullyCollapsed(); - mCollapsedAndHeadsUpOnDown = false; - mHasLayoutedSinceDown = false; - mUpdateFlingOnLayout = false; - mTouchAboveFalsingThreshold = false; - addMovement(event); - break; - case MotionEvent.ACTION_POINTER_UP: - final int upPointer = event.getPointerId(event.getActionIndex()); - if (mTrackingPointer == upPointer) { - // gesture is ongoing, find a new pointer to track - final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; - mTrackingPointer = event.getPointerId(newIndex); - mInitialTouchX = event.getX(newIndex); - mInitialTouchY = event.getY(newIndex); - } - break; - case MotionEvent.ACTION_POINTER_DOWN: - if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { - mMotionAborted = true; - mVelocityTracker.clear(); - } - break; - case MotionEvent.ACTION_MOVE: - final float h = y - mInitialTouchY; - addMovement(event); - final boolean openShadeWithoutHun = - mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown; - if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown - || openShadeWithoutHun) { - float hAbs = Math.abs(h); - float touchSlop = getTouchSlop(event); - if ((h < -touchSlop - || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop)) - && hAbs > Math.abs(x - mInitialTouchX)) { - cancelHeightAnimator(); - startExpandMotion(x, y, true /* startTracking */, mExpandedHeight); - return true; - } - } - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - mVelocityTracker.clear(); - break; - } - return false; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (mInstantExpanding) { - mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding"); - return false; - } - if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { - mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled"); - return false; - } - if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) { - mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted"); - return false; - } - - // If dragging should not expand the notifications shade, then return false. - if (!mNotificationsDragEnabled) { - if (mTracking) { - // Turn off tracking if it's on or the shade can get stuck in the down position. - onTrackingStopped(true /* expand */); - } - mShadeLog.logMotionEvent(event, "onTouch: drag not enabled"); - return false; - } - - // On expanding, single mouse click expands the panel instead of dragging. - if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) { - if (event.getAction() == MotionEvent.ACTION_UP) { - expand(true); - } - return true; - } - - /* - * We capture touch events here and update the expand height here in case according to - * the users fingers. This also handles multi-touch. - * - * Flinging is also enabled in order to open or close the shade. - */ - - int pointerIndex = event.findPointerIndex(mTrackingPointer); - if (pointerIndex < 0) { - pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); - } - final float x = event.getX(pointerIndex); - final float y = event.getY(pointerIndex); - - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop(); - mIgnoreXTouchSlop = true; - } - - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); - mMinExpandHeight = 0.0f; - mPanelClosedOnDown = isFullyCollapsed(); - mHasLayoutedSinceDown = false; - mUpdateFlingOnLayout = false; - mMotionAborted = false; - mDownTime = mSystemClock.uptimeMillis(); - mTouchAboveFalsingThreshold = false; - mCollapsedAndHeadsUpOnDown = - isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp(); - addMovement(event); - boolean regularHeightAnimationRunning = mHeightAnimator != null - && !mHintAnimationRunning && !mIsSpringBackAnimation; - if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) { - mTouchSlopExceeded = regularHeightAnimationRunning - || mTouchSlopExceededBeforeDown; - cancelHeightAnimator(); - onTrackingStarted(); - } - if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp() - && !mCentralSurfaces.isBouncerShowing()) { - startOpening(event); - } - break; - - case MotionEvent.ACTION_POINTER_UP: - final int upPointer = event.getPointerId(event.getActionIndex()); - if (mTrackingPointer == upPointer) { - // gesture is ongoing, find a new pointer to track - final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; - final float newY = event.getY(newIndex); - final float newX = event.getX(newIndex); - mTrackingPointer = event.getPointerId(newIndex); - mHandlingPointerUp = true; - startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight); - mHandlingPointerUp = false; - } - break; - case MotionEvent.ACTION_POINTER_DOWN: - if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { - mMotionAborted = true; - endMotionEvent(event, x, y, true /* forceCancel */); - return false; - } - break; - case MotionEvent.ACTION_MOVE: - addMovement(event); - float h = y - mInitialTouchY; - - // If the panel was collapsed when touching, we only need to check for the - // y-component of the gesture, as we have no conflicting horizontal gesture. - if (Math.abs(h) > getTouchSlop(event) - && (Math.abs(h) > Math.abs(x - mInitialTouchX) - || mIgnoreXTouchSlop)) { - mTouchSlopExceeded = true; - if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) { - if (mInitialOffsetOnTouch != 0f) { - startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); - h = 0; - } - cancelHeightAnimator(); - onTrackingStarted(); - } - } - float newHeight = Math.max(0, h + mInitialOffsetOnTouch); - newHeight = Math.max(newHeight, mMinExpandHeight); - if (-h >= getFalsingThreshold()) { - mTouchAboveFalsingThreshold = true; - mUpwardsWhenThresholdReached = isDirectionUpwards(x, y); - } - if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) { - // Count h==0 as part of swipe-up, - // otherwise {@link NotificationStackScrollLayout} - // wrongly enables stack height updates at the start of lockscreen swipe-up - mAmbientState.setSwipingUp(h <= 0); - setExpandedHeightInternal(newHeight); - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - addMovement(event); - endMotionEvent(event, x, y, false /* forceCancel */); - // mHeightAnimator is null, there is no remaining frame, ends instrumenting. - if (mHeightAnimator == null) { - if (event.getActionMasked() == MotionEvent.ACTION_UP) { - endJankMonitoring(); - } else { - cancelJankMonitoring(); - } - } - break; - } - return !mGestureWaitForTouchSlop || mTracking; - } - } - - public class OnLayoutChangeListener implements View.OnLayoutChangeListener { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { - requestPanelHeightUpdate(); - mHasLayoutedSinceDown = true; - if (mUpdateFlingOnLayout) { - abortAnimations(); - fling(mUpdateFlingVelocity, true /* expands */); - mUpdateFlingOnLayout = false; - } - } - } - - public class OnConfigurationChangedListener implements - PanelView.OnConfigurationChangedListener { - @Override - public void onConfigurationChanged(Configuration newConfig) { - loadDimens(); - } - } - - private void beginJankMonitoring() { - if (mInteractionJankMonitor == null) { - return; - } - InteractionJankMonitor.Configuration.Builder builder = - InteractionJankMonitor.Configuration.Builder.withView( - InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, - mView) - .setTag(isFullyCollapsed() ? "Expand" : "Collapse"); - mInteractionJankMonitor.begin(builder); - } - - private void endJankMonitoring() { - if (mInteractionJankMonitor == null) { - return; - } - InteractionJankMonitor.getInstance().end( - InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); - } - - private void cancelJankMonitoring() { - if (mInteractionJankMonitor == null) { - return; - } - InteractionJankMonitor.getInstance().cancel( - InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); - } - - protected float getExpansionFraction() { - return mExpandedFraction; - } - - protected PanelExpansionStateManager getPanelExpansionStateManager() { - return mPanelExpansionStateManager; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java index 359272e8a7e0..662f70ef269e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaOptimizedFrameLayout.java @@ -20,12 +20,28 @@ import android.content.Context; import android.util.AttributeSet; import android.widget.FrameLayout; +import com.android.systemui.animation.LaunchableView; +import com.android.systemui.animation.LaunchableViewDelegate; + +import kotlin.Unit; + /** * A frame layout which does not have overlapping renderings commands and therefore does not need a * layer when alpha is changed. */ -public class AlphaOptimizedFrameLayout extends FrameLayout +public class AlphaOptimizedFrameLayout extends FrameLayout implements LaunchableView { + private final LaunchableViewDelegate mLaunchableViewDelegate = new LaunchableViewDelegate( + this, + visibility -> { + super.setVisibility(visibility); + return Unit.INSTANCE; + }, + visibility -> { + super.setTransitionVisibility(visibility); + return Unit.INSTANCE; + }); + public AlphaOptimizedFrameLayout(Context context) { super(context); } @@ -47,4 +63,19 @@ public class AlphaOptimizedFrameLayout extends FrameLayout public boolean hasOverlappingRendering() { return false; } + + @Override + public void setShouldBlockVisibilityChanges(boolean block) { + mLaunchableViewDelegate.setShouldBlockVisibilityChanges(block); + } + + @Override + public void setVisibility(int visibility) { + mLaunchableViewDelegate.setVisibility(visibility); + } + + @Override + public void setTransitionVisibility(int visibility) { + mLaunchableViewDelegate.setTransitionVisibility(visibility); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt index 9e5dab1152ec..f8449ae8807b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt @@ -90,6 +90,18 @@ data class ListAttachState private constructor( stableIndex = -1 } + /** + * Erases bookkeeping traces stored on an entry when it is removed from the notif list. + * This can happen if the entry is removed from a group that was broken up or if the entry was + * filtered out during any of the filtering steps. + */ + fun detach() { + parent = null + section = null + promoter = null + // stableIndex = -1 // TODO(b/241229236): Clear this once we fix the stability fragility + } + companion object { @JvmStatic fun create(): ListAttachState { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 14cc6bf1ea41..3eaa988e8389 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -958,9 +958,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { * filtered out during any of the filtering steps. */ private void annulAddition(ListEntry entry) { - entry.setParent(null); - entry.getAttachState().setSection(null); - entry.getAttachState().setPromoter(null); + entry.getAttachState().detach(); } private void assignSections() { @@ -1198,9 +1196,9 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { o2.getSectionIndex()); if (cmp != 0) return cmp; - int index1 = canReorder(o1) ? -1 : o1.getPreviousAttachState().getStableIndex(); - int index2 = canReorder(o2) ? -1 : o2.getPreviousAttachState().getStableIndex(); - cmp = Integer.compare(index1, index2); + cmp = Integer.compare( + getStableOrderIndex(o1), + getStableOrderIndex(o2)); if (cmp != 0) return cmp; NotifComparator sectionComparator = getSectionComparator(o1, o2); @@ -1214,31 +1212,32 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { if (cmp != 0) return cmp; } - final NotificationEntry rep1 = o1.getRepresentativeEntry(); - final NotificationEntry rep2 = o2.getRepresentativeEntry(); - cmp = rep1.getRanking().getRank() - rep2.getRanking().getRank(); + cmp = Integer.compare( + o1.getRepresentativeEntry().getRanking().getRank(), + o2.getRepresentativeEntry().getRanking().getRank()); if (cmp != 0) return cmp; - cmp = Long.compare( - rep2.getSbn().getNotification().when, - rep1.getSbn().getNotification().when); + cmp = -1 * Long.compare( + o1.getRepresentativeEntry().getSbn().getNotification().when, + o2.getRepresentativeEntry().getSbn().getNotification().when); return cmp; }; private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { - int index1 = canReorder(o1) ? -1 : o1.getPreviousAttachState().getStableIndex(); - int index2 = canReorder(o2) ? -1 : o2.getPreviousAttachState().getStableIndex(); - int cmp = Integer.compare(index1, index2); + int cmp = Integer.compare( + getStableOrderIndex(o1), + getStableOrderIndex(o2)); if (cmp != 0) return cmp; - cmp = o1.getRepresentativeEntry().getRanking().getRank() - - o2.getRepresentativeEntry().getRanking().getRank(); + cmp = Integer.compare( + o1.getRepresentativeEntry().getRanking().getRank(), + o2.getRepresentativeEntry().getRanking().getRank()); if (cmp != 0) return cmp; - cmp = Long.compare( - o2.getRepresentativeEntry().getSbn().getNotification().when, - o1.getRepresentativeEntry().getSbn().getNotification().when); + cmp = -1 * Long.compare( + o1.getRepresentativeEntry().getSbn().getNotification().when, + o2.getRepresentativeEntry().getSbn().getNotification().when); return cmp; }; @@ -1248,8 +1247,16 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { */ private boolean mForceReorderable = false; - private boolean canReorder(ListEntry entry) { - return mForceReorderable || getStabilityManager().isEntryReorderingAllowed(entry); + private int getStableOrderIndex(ListEntry entry) { + if (mForceReorderable) { + // this is used to determine if the list is correctly sorted + return -1; + } + if (getStabilityManager().isEntryReorderingAllowed(entry)) { + // let the stability manager constrain or allow reordering + return -1; + } + return entry.getPreviousAttachState().getStableIndex(); } private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java index 5168533cd2b7..365fbace1608 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java @@ -29,6 +29,7 @@ import com.android.systemui.R; /** * Container for image of the multi user switcher (tappable). */ +// TODO(b/242040009): Remove this file. public class MultiUserSwitch extends FrameLayout { public MultiUserSwitch(Context context, AttributeSet attrs) { super(context, attrs); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java index 799e5feb1586..4d6168989691 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java @@ -40,6 +40,7 @@ import com.android.systemui.util.ViewController; import javax.inject.Inject; /** View Controller for {@link MultiUserSwitch}. */ +// TODO(b/242040009): Remove this file. public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { private final UserManager mUserManager; private final UserSwitcherController mUserSwitcherController; diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt index 74d51112deeb..80c55c0bad07 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt @@ -35,6 +35,7 @@ import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.TextView +import androidx.activity.ComponentActivity import androidx.constraintlayout.helper.widget.Flow import com.android.internal.annotations.VisibleForTesting import com.android.internal.util.UserIcons @@ -51,7 +52,6 @@ import com.android.systemui.statusbar.policy.UserSwitcherController.BaseUserAdap import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA import com.android.systemui.statusbar.policy.UserSwitcherController.UserRecord -import com.android.systemui.util.LifecycleActivity import javax.inject.Inject import kotlin.math.ceil @@ -68,7 +68,7 @@ class UserSwitcherActivity @Inject constructor( private val falsingManager: FalsingManager, private val userManager: UserManager, private val userTracker: UserTracker -) : LifecycleActivity() { +) : ComponentActivity() { private lateinit var parent: UserSwitcherRootView private lateinit var broadcastReceiver: BroadcastReceiver diff --git a/packages/SystemUI/src/com/android/systemui/util/LifecycleActivity.kt b/packages/SystemUI/src/com/android/systemui/util/LifecycleActivity.kt deleted file mode 100644 index e4b7a20aab37..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/LifecycleActivity.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.util - -import android.app.Activity -import android.os.Bundle -import android.os.PersistableBundle -import androidx.lifecycle.LifecycleOwner -import com.android.settingslib.core.lifecycle.Lifecycle - -open class LifecycleActivity : Activity(), LifecycleOwner { - - private val lifecycle = Lifecycle(this) - - override fun getLifecycle() = lifecycle - - override fun onCreate(savedInstanceState: Bundle?) { - lifecycle.onAttach(this) - lifecycle.onCreate(savedInstanceState) - lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_CREATE) - super.onCreate(savedInstanceState) - } - - override fun onCreate( - savedInstanceState: Bundle?, - persistentState: PersistableBundle? - ) { - lifecycle.onAttach(this) - lifecycle.onCreate(savedInstanceState) - lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_CREATE) - super.onCreate(savedInstanceState, persistentState) - } - - override fun onStart() { - lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_START) - super.onStart() - } - - override fun onResume() { - lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_RESUME) - super.onResume() - } - - override fun onPause() { - lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_PAUSE) - super.onPause() - } - - override fun onStop() { - lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_STOP) - super.onStop() - } - - override fun onDestroy() { - lifecycle.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_DESTROY) - super.onDestroy() - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java index e3cd98996a41..d03148cee335 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletActivity.java @@ -32,7 +32,9 @@ import android.view.Window; import android.view.WindowManager; import android.widget.Toolbar; +import androidx.activity.ComponentActivity; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardUpdateMonitor; @@ -48,7 +50,6 @@ import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.LifecycleActivity; import java.util.concurrent.Executor; @@ -57,7 +58,7 @@ import javax.inject.Inject; /** * Displays Wallet carousel screen inside an activity. */ -public class WalletActivity extends LifecycleActivity implements +public class WalletActivity extends ComponentActivity implements QuickAccessWalletClient.WalletServiceEventListener { private static final String TAG = "WalletActivity"; @@ -105,7 +106,7 @@ public class WalletActivity extends LifecycleActivity implements } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt index 0f112415df0d..e2790e47fe06 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt @@ -10,12 +10,10 @@ import android.graphics.Rect import android.os.Looper import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper -import android.util.Log import android.view.IRemoteAnimationFinishedCallback import android.view.RemoteAnimationAdapter import android.view.RemoteAnimationTarget import android.view.SurfaceControl -import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import androidx.test.filters.SmallTest @@ -51,7 +49,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { @Mock lateinit var listener: ActivityLaunchAnimator.Listener @Spy private val controller = TestLaunchAnimatorController(launchContainer) @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback - @Mock lateinit var failHandler: Log.TerribleFailureHandler private lateinit var activityLaunchAnimator: ActivityLaunchAnimator @get:Rule val rule = MockitoJUnit.rule() @@ -187,13 +184,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { verify(controller).onLaunchAnimationStart(anyBoolean()) } - @Test - fun controllerFromOrphanViewReturnsNullAndIsATerribleFailure() { - Log.setWtfHandler(failHandler) - assertNull(ActivityLaunchAnimator.Controller.fromView(View(mContext))) - verify(failHandler).onTerribleFailure(any(), any(), anyBoolean()) - } - private fun fakeWindow(): RemoteAnimationTarget { val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */) val taskInfo = ActivityManager.RunningTaskInfo() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt index 328ad39cddd5..58d906907488 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt @@ -41,11 +41,13 @@ import org.mockito.junit.MockitoJUnit @SmallTest class AuthBiometricFingerprintAndFaceViewTest : SysuiTestCase() { - @JvmField @Rule + @JvmField + @Rule var mockitoRule = MockitoJUnit.rule() @Mock private lateinit var callback: AuthBiometricView.Callback + @Mock private lateinit var panelController: AuthPanelController @@ -67,6 +69,7 @@ class AuthBiometricFingerprintAndFaceViewTest : SysuiTestCase() { fun fingerprintSuccessDoesNotRequireExplicitConfirmation() { biometricView.onDialogAnimatedIn() biometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT) + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() assertThat(biometricView.isAuthenticated).isTrue() @@ -86,6 +89,7 @@ class AuthBiometricFingerprintAndFaceViewTest : SysuiTestCase() { // icon acts as confirm button biometricView.mIconView.performClick() + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() assertThat(biometricView.isAuthenticated).isTrue() @@ -102,6 +106,7 @@ class AuthBiometricFingerprintAndFaceViewTest : SysuiTestCase() { verify(callback, never()).onAction(AuthBiometricView.Callback.ACTION_ERROR) biometricView.onError(TYPE_FINGERPRINT, "that's a nope") + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() verify(callback).onAction(AuthBiometricView.Callback.ACTION_ERROR) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt index 687cb517b2f4..bce98cf116d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt @@ -42,20 +42,23 @@ import org.mockito.junit.MockitoJUnit @SmallTest class AuthBiometricFingerprintViewTest : SysuiTestCase() { - @JvmField @Rule + @JvmField + @Rule val mockitoRule = MockitoJUnit.rule() @Mock private lateinit var callback: AuthBiometricView.Callback + @Mock private lateinit var panelController: AuthPanelController private lateinit var biometricView: AuthBiometricView private fun createView(allowDeviceCredential: Boolean = false): AuthBiometricFingerprintView { - val view = R.layout.auth_biometric_fingerprint_view.asTestAuthBiometricView( + val view: AuthBiometricFingerprintView = + R.layout.auth_biometric_fingerprint_view.asTestAuthBiometricView( mContext, callback, panelController, allowDeviceCredential = allowDeviceCredential - ) as AuthBiometricFingerprintView + ) waitForIdleSync() return view } @@ -73,6 +76,7 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { @Test fun testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() { biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() assertThat(biometricView.isAuthenticated).isTrue() @@ -83,6 +87,7 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { fun testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() { biometricView.setRequireConfirmation(true) biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() // TODO: this should be tested in the subclasses @@ -104,6 +109,7 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { @Test fun testPositiveButton_sendsActionAuthenticated() { biometricView.mConfirmButton.performClick() + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() verify(callback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED) @@ -114,6 +120,7 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { fun testNegativeButton_beforeAuthentication_sendsActionButtonNegative() { biometricView.onDialogAnimatedIn() biometricView.mNegativeButton.performClick() + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() verify(callback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE) @@ -126,6 +133,7 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { assertThat(biometricView.mNegativeButton.visibility).isEqualTo(View.GONE) biometricView.mCancelButton.performClick() + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() verify(callback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED) @@ -134,6 +142,7 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { @Test fun testTryAgainButton_sendsActionTryAgain() { biometricView.mTryAgainButton.performClick() + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() verify(callback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN) @@ -144,6 +153,7 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { @Test fun testOnErrorSendsActionError() { biometricView.onError(BiometricAuthenticator.TYPE_FACE, "testError") + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() verify(callback).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR)) @@ -156,6 +166,7 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { val message = "another error" biometricView.onError(BiometricAuthenticator.TYPE_FACE, message) + TestableLooper.get(this).moveTimeForward(1000) waitForIdleSync() assertThat(biometricView.isAuthenticating).isFalse() @@ -178,6 +189,7 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { val view = View(mContext) biometricView.setBackgroundView(view) biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) + waitForIdleSync() view.performClick() verify(callback, never()) @@ -225,14 +237,14 @@ class AuthBiometricFingerprintViewTest : SysuiTestCase() { biometricView.onSaveState(state) assertThat(biometricView.mTryAgainButton.visibility).isEqualTo(View.GONE) assertThat(state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)) - .isEqualTo(View.GONE) + .isEqualTo(View.GONE) assertThat(state.getInt(AuthDialog.KEY_BIOMETRIC_STATE)) - .isEqualTo(AuthBiometricView.STATE_ERROR) + .isEqualTo(AuthBiometricView.STATE_ERROR) assertThat(biometricView.mIndicatorView.visibility).isEqualTo(View.VISIBLE) assertThat(state.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)).isTrue() assertThat(biometricView.mIndicatorView.text).isEqualTo(failureMessage) assertThat(state.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING)) - .isEqualTo(failureMessage) + .isEqualTo(failureMessage) // TODO: Test dialog size. Should move requireConfirmation to buildBiometricPromptBundle diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt index 7795d2caf091..434cb48bc422 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.broadcast import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent import android.content.IntentFilter import android.os.Handler import android.os.Looper @@ -32,22 +33,27 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor import junit.framework.Assert.assertSame +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.anyInt import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.util.concurrent.Executor @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper @@ -381,6 +387,39 @@ class BroadcastDispatcherTest : SysuiTestCase() { .clearPendingRemoval(broadcastReceiver, user1.identifier) } + @Test + fun testBroadcastFlow() = runBlockingTest { + val flow = broadcastDispatcher.broadcastFlow(intentFilter, user1) { intent, receiver -> + intent to receiver + } + + // Collect the values into collectedValues. + val collectedValues = mutableListOf<Pair<Intent, BroadcastReceiver>>() + val job = launch { + flow.collect { collectedValues.add(it) } + } + + testableLooper.processAllMessages() + verify(mockUBRUser1).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG)) + val receiver = argumentCaptor.value.receiver + + // Simulate fake broadcasted intents. + val fakeIntents = listOf<Intent>(mock(), mock(), mock()) + fakeIntents.forEach { receiver.onReceive(mockContext, it) } + + // The intents should have been collected. + advanceUntilIdle() + + val expectedValues = fakeIntents.map { it to receiver } + assertThat(collectedValues).containsExactlyElementsIn(expectedValues) + + // Stop the collection. + job.cancel() + + testableLooper.processAllMessages() + verify(mockUBRUser1).unregisterReceiver(receiver) + } + private fun setUserMock(mockContext: Context, user: UserHandle) { `when`(mockContext.user).thenReturn(user) `when`(mockContext.userId).thenReturn(user.identifier) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java index 2927669020c8..7bae115d2edd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.qs; +import static android.os.PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -34,6 +36,7 @@ import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.os.Binder; import android.os.RemoteException; +import android.os.UserHandle; import android.provider.DeviceConfig; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -185,9 +188,9 @@ public class FgsManagerControllerTest extends SysuiTestCase { public void testChangesSinceLastDialog() throws RemoteException { setUserProfiles(0); - Assert.assertFalse(mFmc.getChangesSinceDialog()); + Assert.assertFalse(mFmc.getNewChangesSinceDialogWasDismissed()); mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg", 0, true); - Assert.assertTrue(mFmc.getChangesSinceDialog()); + Assert.assertTrue(mFmc.getNewChangesSinceDialogWasDismissed()); } @Test @@ -222,7 +225,41 @@ public class FgsManagerControllerTest extends SysuiTestCase { Assert.assertEquals(2, mFmc.getNumRunningPackages()); } + @Test + public void testButtonVisibilityOnShowAllowlistButtonFlagChange() throws Exception { + setUserProfiles(0); + setBackgroundRestrictionExemptionReason("pkg", 12345, REASON_ALLOWLISTED_PACKAGE); + + final Binder binder = new Binder(); + setShowStopButtonForUserAllowlistedApps(true); + mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, true); + Assert.assertEquals(1, mFmc.visibleButtonsCount()); + + mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, false); + Assert.assertEquals(0, mFmc.visibleButtonsCount()); + + setShowStopButtonForUserAllowlistedApps(false); + mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, true); + Assert.assertEquals(0, mFmc.visibleButtonsCount()); + } + private void setShowStopButtonForUserAllowlistedApps(boolean enable) { + mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS, + enable ? "true" : "false", false); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + } + + private void setBackgroundRestrictionExemptionReason(String pkgName, int uid, int reason) + throws Exception { + Mockito.doReturn(uid) + .when(mPackageManager) + .getPackageUidAsUser(pkgName, UserHandle.getUserId(uid)); + Mockito.doReturn(reason) + .when(mIActivityManager) + .getBackgroundRestrictionExemptionReason(uid); + } FgsManagerController createFgsManagerController() throws RemoteException { ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor = @@ -232,7 +269,7 @@ public class FgsManagerControllerTest extends SysuiTestCase { ArgumentCaptor<BroadcastReceiver> showFgsManagerReceiverArgumentCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); - FgsManagerController result = new FgsManagerController( + FgsManagerController result = new FgsManagerControllerImpl( mContext, mMainExecutor, mBackgroundExecutor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index 10f6ce8c0ec9..f08ad2453c5f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -45,12 +45,15 @@ import com.android.systemui.R; import com.android.systemui.SysuiBaseFragmentTest; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSFragmentComponent; import com.android.systemui.qs.external.TileServiceRequestController; +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -390,6 +393,8 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { setUpMedia(); setUpOther(); + FakeFeatureFlags featureFlags = new FakeFeatureFlags(); + featureFlags.set(Flags.NEW_FOOTER_ACTIONS, false); return new QSFragment( new RemoteInputQuickSettingsDisabler( context, commandQueue, mock(ConfigurationController.class)), @@ -402,7 +407,10 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { mQsComponentFactory, mock(QSFragmentDisableFlagsLogger.class), mFalsingManager, - mock(DumpManager.class)); + mock(DumpManager.class), + featureFlags, + mock(NewFooterActionsController.class), + mock(FooterActionsViewModel.Factory.class)); } private void setUpOther() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java index c1c0f78ecd6b..233c267c3be0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java @@ -100,6 +100,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { private TextView mFooterText; private TestableImageView mPrimaryFooterIcon; private QSSecurityFooter mFooter; + private QSSecurityFooterUtils mFooterUtils; @Mock private SecurityController mSecurityController; @Mock @@ -118,13 +119,16 @@ public class QSSecurityFooterTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mTestableLooper = TestableLooper.get(this); Looper looper = mTestableLooper.getLooper(); + Handler mainHandler = new Handler(looper); when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class)); mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext) .replace("ImageView", TestableImageView.class) .build().inflate(R.layout.quick_settings_security_footer, null, false); - mFooter = new QSSecurityFooter(mRootView, mUserTracker, new Handler(looper), - mActivityStarter, mSecurityController, mDialogLaunchAnimator, looper, - mBroadcastDispatcher); + mFooterUtils = new QSSecurityFooterUtils(getContext(), + getContext().getSystemService(DevicePolicyManager.class), mUserTracker, + mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator); + mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper, + mBroadcastDispatcher, mFooterUtils); mFooterText = mRootView.findViewById(R.id.footer_text); mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon); @@ -520,7 +524,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { when(mSecurityController.isDeviceManaged()).thenReturn(true); assertEquals(mContext.getString(R.string.monitoring_title_device_owned), - mFooter.getManagementTitle(MANAGING_ORGANIZATION)); + mFooterUtils.getManagementTitle(MANAGING_ORGANIZATION)); } @Test @@ -531,12 +535,12 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString(R.string.monitoring_title_financed_device, MANAGING_ORGANIZATION), - mFooter.getManagementTitle(MANAGING_ORGANIZATION)); + mFooterUtils.getManagementTitle(MANAGING_ORGANIZATION)); } @Test public void testGetManagementMessage_noManagement() { - assertEquals(null, mFooter.getManagementMessage( + assertEquals(null, mFooterUtils.getManagementMessage( /* isDeviceManaged= */ false, MANAGING_ORGANIZATION)); } @@ -544,10 +548,10 @@ public class QSSecurityFooterTest extends SysuiTestCase { public void testGetManagementMessage_deviceOwner() { assertEquals(mContext.getString(R.string.monitoring_description_named_management, MANAGING_ORGANIZATION), - mFooter.getManagementMessage( + mFooterUtils.getManagementMessage( /* isDeviceManaged= */ true, MANAGING_ORGANIZATION)); assertEquals(mContext.getString(R.string.monitoring_description_management), - mFooter.getManagementMessage( + mFooterUtils.getManagementMessage( /* isDeviceManaged= */ true, /* organizationName= */ null)); } @@ -560,68 +564,68 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management, MANAGING_ORGANIZATION, MANAGING_ORGANIZATION), - mFooter.getManagementMessage( + mFooterUtils.getManagementMessage( /* isDeviceManaged= */ true, MANAGING_ORGANIZATION)); } @Test public void testGetCaCertsMessage() { - assertEquals(null, mFooter.getCaCertsMessage(true, false, false)); - assertEquals(null, mFooter.getCaCertsMessage(false, false, false)); + assertEquals(null, mFooterUtils.getCaCertsMessage(true, false, false)); + assertEquals(null, mFooterUtils.getCaCertsMessage(false, false, false)); assertEquals(mContext.getString(R.string.monitoring_description_management_ca_certificate), - mFooter.getCaCertsMessage(true, true, true)); + mFooterUtils.getCaCertsMessage(true, true, true)); assertEquals(mContext.getString(R.string.monitoring_description_management_ca_certificate), - mFooter.getCaCertsMessage(true, false, true)); + mFooterUtils.getCaCertsMessage(true, false, true)); assertEquals(mContext.getString( R.string.monitoring_description_managed_profile_ca_certificate), - mFooter.getCaCertsMessage(false, false, true)); + mFooterUtils.getCaCertsMessage(false, false, true)); assertEquals(mContext.getString( R.string.monitoring_description_ca_certificate), - mFooter.getCaCertsMessage(false, true, false)); + mFooterUtils.getCaCertsMessage(false, true, false)); } @Test public void testGetNetworkLoggingMessage() { // Test network logging message on a device with a device owner. // Network traffic may be monitored on the device. - assertEquals(null, mFooter.getNetworkLoggingMessage(true, false)); + assertEquals(null, mFooterUtils.getNetworkLoggingMessage(true, false)); assertEquals(mContext.getString(R.string.monitoring_description_management_network_logging), - mFooter.getNetworkLoggingMessage(true, true)); + mFooterUtils.getNetworkLoggingMessage(true, true)); // Test network logging message on a device with a managed profile owner // Network traffic may be monitored on the work profile. - assertEquals(null, mFooter.getNetworkLoggingMessage(false, false)); + assertEquals(null, mFooterUtils.getNetworkLoggingMessage(false, false)); assertEquals( mContext.getString(R.string.monitoring_description_managed_profile_network_logging), - mFooter.getNetworkLoggingMessage(false, true)); + mFooterUtils.getNetworkLoggingMessage(false, true)); } @Test public void testGetVpnMessage() { - assertEquals(null, mFooter.getVpnMessage(true, true, null, null)); + assertEquals(null, mFooterUtils.getVpnMessage(true, true, null, null)); assertEquals(addLink(mContext.getString(R.string.monitoring_description_two_named_vpns, VPN_PACKAGE, VPN_PACKAGE_2)), - mFooter.getVpnMessage(true, true, VPN_PACKAGE, VPN_PACKAGE_2)); + mFooterUtils.getVpnMessage(true, true, VPN_PACKAGE, VPN_PACKAGE_2)); assertEquals(addLink(mContext.getString(R.string.monitoring_description_two_named_vpns, VPN_PACKAGE, VPN_PACKAGE_2)), - mFooter.getVpnMessage(false, true, VPN_PACKAGE, VPN_PACKAGE_2)); + mFooterUtils.getVpnMessage(false, true, VPN_PACKAGE, VPN_PACKAGE_2)); assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn, VPN_PACKAGE)), - mFooter.getVpnMessage(true, false, VPN_PACKAGE, null)); + mFooterUtils.getVpnMessage(true, false, VPN_PACKAGE, null)); assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn, VPN_PACKAGE)), - mFooter.getVpnMessage(false, false, VPN_PACKAGE, null)); + mFooterUtils.getVpnMessage(false, false, VPN_PACKAGE, null)); assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn, VPN_PACKAGE_2)), - mFooter.getVpnMessage(true, true, null, VPN_PACKAGE_2)); + mFooterUtils.getVpnMessage(true, true, null, VPN_PACKAGE_2)); assertEquals(addLink(mContext.getString( R.string.monitoring_description_managed_profile_named_vpn, VPN_PACKAGE_2)), - mFooter.getVpnMessage(false, true, null, VPN_PACKAGE_2)); + mFooterUtils.getVpnMessage(false, true, null, VPN_PACKAGE_2)); assertEquals(addLink(mContext.getString( R.string.monitoring_description_personal_profile_named_vpn, VPN_PACKAGE)), - mFooter.getVpnMessage(false, true, VPN_PACKAGE, null)); + mFooterUtils.getVpnMessage(false, true, VPN_PACKAGE, null)); } @Test @@ -631,19 +635,19 @@ public class QSSecurityFooterTest extends SysuiTestCase { // Device Management subtitle should be shown when there is Device Management section only // Other sections visibility will be set somewhere else so it will not be tested here - mFooter.configSubtitleVisibility(true, false, false, false, view); + mFooterUtils.configSubtitleVisibility(true, false, false, false, view); assertEquals(View.VISIBLE, view.findViewById(R.id.device_management_subtitle).getVisibility()); // If there are multiple sections, all subtitles should be shown - mFooter.configSubtitleVisibility(true, true, false, false, view); + mFooterUtils.configSubtitleVisibility(true, true, false, false, view); assertEquals(View.VISIBLE, view.findViewById(R.id.device_management_subtitle).getVisibility()); assertEquals(View.VISIBLE, view.findViewById(R.id.ca_certs_subtitle).getVisibility()); // If there are multiple sections, all subtitles should be shown - mFooter.configSubtitleVisibility(true, true, true, true, view); + mFooterUtils.configSubtitleVisibility(true, true, true, true, view); assertEquals(View.VISIBLE, view.findViewById(R.id.device_management_subtitle).getVisibility()); assertEquals(View.VISIBLE, @@ -655,7 +659,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { // If there are multiple sections, all subtitles should be shown, event if there is no // Device Management section - mFooter.configSubtitleVisibility(false, true, true, true, view); + mFooterUtils.configSubtitleVisibility(false, true, true, true, view); assertEquals(View.VISIBLE, view.findViewById(R.id.ca_certs_subtitle).getVisibility()); assertEquals(View.VISIBLE, @@ -664,13 +668,13 @@ public class QSSecurityFooterTest extends SysuiTestCase { view.findViewById(R.id.vpn_subtitle).getVisibility()); // If there is only 1 section, the title should be hidden - mFooter.configSubtitleVisibility(false, true, false, false, view); + mFooterUtils.configSubtitleVisibility(false, true, false, false, view); assertEquals(View.GONE, view.findViewById(R.id.ca_certs_subtitle).getVisibility()); - mFooter.configSubtitleVisibility(false, false, true, false, view); + mFooterUtils.configSubtitleVisibility(false, false, true, false, view); assertEquals(View.GONE, view.findViewById(R.id.network_logging_subtitle).getVisibility()); - mFooter.configSubtitleVisibility(false, false, false, true, view); + mFooterUtils.configSubtitleVisibility(false, false, false, true, view); assertEquals(View.GONE, view.findViewById(R.id.vpn_subtitle).getVisibility()); } @@ -690,6 +694,9 @@ public class QSSecurityFooterTest extends SysuiTestCase { @Test public void testParentalControls() { + // Make sure the security footer is visible, so that the images are updated. + when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true); + when(mSecurityController.isParentalControlsEnabled()).thenReturn(true); Drawable testDrawable = new VectorDrawable(); @@ -719,7 +726,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { when(mSecurityController.isParentalControlsEnabled()).thenReturn(true); when(mSecurityController.getLabel(any())).thenReturn(PARENTAL_CONTROLS_LABEL); - View view = mFooter.createDialogView(); + View view = mFooterUtils.createDialogView(); TextView textView = (TextView) view.findViewById(R.id.parental_controls_title); assertEquals(PARENTAL_CONTROLS_LABEL, textView.getText()); } @@ -742,7 +749,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT)) .thenReturn(DEVICE_OWNER_TYPE_FINANCED); - View view = mFooter.createDialogView(); + View view = mFooterUtils.createDialogView(); TextView managementSubtitle = view.findViewById(R.id.device_management_subtitle); assertEquals(View.VISIBLE, managementSubtitle.getVisibility()); @@ -753,7 +760,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management, MANAGING_ORGANIZATION, MANAGING_ORGANIZATION), managementMessage.getText()); assertEquals(mContext.getString(R.string.monitoring_button_view_policies), - mFooter.getSettingsButton()); + mFooterUtils.getSettingsButton()); } @Test @@ -773,7 +780,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { AlertDialog dialog = dialogCaptor.getValue(); dialog.create(); - assertEquals(mFooter.getSettingsButton(), + assertEquals(mFooterUtils.getSettingsButton(), dialog.getButton(DialogInterface.BUTTON_NEGATIVE).getText()); dialog.dismiss(); @@ -816,8 +823,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG)); mTestableLooper.processAllMessages(); - assertTrue(mFooter.getDialog().isShowing()); - mFooter.getDialog().dismiss(); + assertTrue(mFooterUtils.getDialog().isShowing()); + mFooterUtils.getDialog().dismiss(); } private CharSequence addLink(CharSequence description) { @@ -825,7 +832,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { message.append(description); message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator)); message.append(mContext.getString(R.string.monitoring_description_vpn_settings), - mFooter.new VpnSpan(), 0); + mFooterUtils.new VpnSpan(), 0); return message; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt new file mode 100644 index 000000000000..3c258077c29d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.domain.interactor + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.UserHandle +import android.provider.Settings +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import androidx.test.filters.SmallTest +import com.android.internal.logging.nano.MetricsProto +import com.android.internal.logging.testing.FakeMetricsLogger +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.globalactions.GlobalActionsDialogLite +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.QSSecurityFooterUtils +import com.android.systemui.qs.footer.FooterActionsTestUtils +import com.android.systemui.qs.user.UserSwitchDialogController +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.truth.correspondence.FakeUiEvent +import com.android.systemui.truth.correspondence.LogMaker +import com.android.systemui.user.UserSwitcherActivity +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class FooterActionsInteractorTest : SysuiTestCase() { + private lateinit var utils: FooterActionsTestUtils + + @Before + fun setUp() { + utils = FooterActionsTestUtils(context, TestableLooper.get(this)) + } + + @Test + fun showDeviceMonitoringDialog() { + val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() + val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils) + + val quickSettingsContext = mock<Context>() + underTest.showDeviceMonitoringDialog(quickSettingsContext) + verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null) + + val view = mock<View>() + whenever(view.context).thenReturn(quickSettingsContext) + underTest.showDeviceMonitoringDialog(view) + verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null) + } + + @Test + fun showPowerMenuDialog() { + val uiEventLogger = UiEventLoggerFake() + val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger) + + val globalActionsDialogLite = mock<GlobalActionsDialogLite>() + val view = mock<View>() + underTest.showPowerMenuDialog(globalActionsDialogLite, view) + + // Event is logged. + val logs = uiEventLogger.logs + assertThat(logs) + .comparingElementsUsing(FakeUiEvent.EVENT_ID) + .containsExactly(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS.id) + + // Dialog is shown. + verify(globalActionsDialogLite) + .showOrHideDialog( + /* keyguardShowing= */ false, + /* isDeviceProvisioned= */ true, + view, + ) + } + + @Test + fun showSettings_userSetUp() { + val activityStarter = mock<ActivityStarter>() + val deviceProvisionedController = mock<DeviceProvisionedController>() + val metricsLogger = FakeMetricsLogger() + + // User is set up. + whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true) + + val underTest = + utils.footerActionsInteractor( + activityStarter = activityStarter, + deviceProvisionedController = deviceProvisionedController, + metricsLogger = metricsLogger, + ) + + underTest.showSettings(mock()) + + // Event is logged. + assertThat(metricsLogger.logs.toList()) + .comparingElementsUsing(LogMaker.CATEGORY) + .containsExactly(MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH) + + // Activity is started. + val intentCaptor = argumentCaptor<Intent>() + verify(activityStarter) + .startActivity( + intentCaptor.capture(), + /* dismissShade= */ eq(true), + nullable() as? ActivityLaunchAnimator.Controller, + ) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SETTINGS) + } + + @Test + fun showSettings_userNotSetUp() { + val activityStarter = mock<ActivityStarter>() + val deviceProvisionedController = mock<DeviceProvisionedController>() + + // User is not set up. + whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(false) + + val underTest = + utils.footerActionsInteractor( + activityStarter = activityStarter, + deviceProvisionedController = deviceProvisionedController, + ) + + underTest.showSettings(mock()) + + // We only unlock the device. + verify(activityStarter).postQSRunnableDismissingKeyguard(any()) + } + + @Test + fun showUserSwitcher_fullScreenDisabled() { + val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } + val userSwitchDialogController = mock<UserSwitchDialogController>() + val underTest = + utils.footerActionsInteractor( + featureFlags = featureFlags, + userSwitchDialogController = userSwitchDialogController, + ) + + val view = mock<View>() + underTest.showUserSwitcher(view) + + // Dialog is shown. + verify(userSwitchDialogController).showDialog(view) + } + + @Test + fun showUserSwitcher_fullScreenEnabled() { + val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) } + val activityStarter = mock<ActivityStarter>() + val underTest = + utils.footerActionsInteractor( + featureFlags = featureFlags, + activityStarter = activityStarter, + ) + + // The clicked view. The context is necessary because it's used to build the intent, that + // we check below. + val view = mock<View>() + whenever(view.context).thenReturn(context) + + underTest.showUserSwitcher(view) + + // Dialog is shown. + val intentCaptor = argumentCaptor<Intent>() + verify(activityStarter) + .startActivity( + intentCaptor.capture(), + /* dismissShade= */ eq(true), + /* ActivityLaunchAnimator.Controller= */ nullable(), + /* showOverLockscreenWhenLocked= */ eq(true), + eq(UserHandle.SYSTEM), + ) + assertThat(intentCaptor.value.component) + .isEqualTo( + ComponentName( + context, + UserSwitcherActivity::class.java, + ) + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt new file mode 100644 index 000000000000..e4751d135035 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer.ui.viewmodel + +import android.graphics.drawable.Drawable +import android.os.UserManager +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.settingslib.Utils +import com.android.settingslib.drawable.UserIconDrawable +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.FakeFgsManagerController +import com.android.systemui.qs.QSSecurityFooterUtils +import com.android.systemui.qs.footer.FooterActionsTestUtils +import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig +import com.android.systemui.security.data.model.SecurityModel +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.statusbar.policy.FakeSecurityController +import com.android.systemui.statusbar.policy.FakeUserInfoController +import com.android.systemui.statusbar.policy.FakeUserInfoController.FakeInfo +import com.android.systemui.statusbar.policy.MockUserSwitcherControllerWrapper +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class FooterActionsViewModelTest : SysuiTestCase() { + private lateinit var utils: FooterActionsTestUtils + + @Before + fun setUp() { + utils = FooterActionsTestUtils(context, TestableLooper.get(this)) + } + + @Test + fun settingsButton() = runBlockingTest { + val underTest = utils.footerActionsViewModel(showPowerButton = false) + val settings = underTest.settings + + assertThat(settings.contentDescription) + .isEqualTo(ContentDescription.Resource(R.string.accessibility_quick_settings_settings)) + assertThat(settings.icon).isEqualTo(Icon.Resource(R.drawable.ic_settings)) + assertThat(settings.background).isEqualTo(R.drawable.qs_footer_action_circle) + assertThat(settings.iconTint).isNull() + } + + @Test + fun powerButton() = runBlockingTest { + // Without power button. + val underTestWithoutPower = utils.footerActionsViewModel(showPowerButton = false) + assertThat(underTestWithoutPower.power).isNull() + + // With power button. + val underTestWithPower = utils.footerActionsViewModel(showPowerButton = true) + val power = underTestWithPower.power + assertThat(power).isNotNull() + assertThat(power!!.contentDescription) + .isEqualTo( + ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu) + ) + assertThat(power.icon).isEqualTo(Icon.Resource(android.R.drawable.ic_lock_power_off)) + assertThat(power.background).isEqualTo(R.drawable.qs_footer_action_circle_color) + assertThat(power.iconTint) + .isEqualTo( + Utils.getColorAttrDefaultColor( + context, + com.android.internal.R.attr.textColorOnAccent, + ), + ) + } + + @Test + fun userSwitcher() = runBlockingTest { + val picture: Drawable = mock() + val userInfoController = FakeUserInfoController(FakeInfo(picture = picture)) + val settings = FakeSettings() + val userId = 42 + val userTracker = FakeUserTracker(userId) + val userSwitcherControllerWrapper = + MockUserSwitcherControllerWrapper(currentUserName = "foo") + + // Mock UserManager. + val userManager = mock<UserManager>() + var isUserSwitcherEnabled = false + var isGuestUser = false + whenever(userManager.isUserSwitcherEnabled(any())).thenAnswer { isUserSwitcherEnabled } + whenever(userManager.isGuestUser(any())).thenAnswer { isGuestUser } + + val underTest = + utils.footerActionsViewModel( + showPowerButton = false, + footerActionsInteractor = + utils.footerActionsInteractor( + userSwitcherRepository = + utils.userSwitcherRepository( + userTracker = userTracker, + settings = settings, + userManager = userManager, + userInfoController = userInfoController, + userSwitcherController = userSwitcherControllerWrapper.controller, + ), + ) + ) + + // Collect the user switcher into currentUserSwitcher. + var currentUserSwitcher: FooterActionsButtonViewModel? = null + val job = launch { underTest.userSwitcher.collect { currentUserSwitcher = it } } + fun currentUserSwitcher(): FooterActionsButtonViewModel? { + // Make sure we finish collecting the current user switcher. This is necessary because + // combined flows launch multiple coroutines in the current scope so we need to make + // sure we process all coroutines triggered by our flow collection before we make + // assertions on the current buttons. + advanceUntilIdle() + return currentUserSwitcher + } + + // The user switcher is disabled. + assertThat(currentUserSwitcher()).isNull() + + // Make the user manager return that the User Switcher is enabled. A change of the setting + // for the current user will be fired to notify us of that change. + isUserSwitcherEnabled = true + + // Update the setting for a random user: nothing should change, given that at this point we + // weren't notified of the change yet. + utils.setUserSwitcherEnabled(settings, true, 3) + assertThat(currentUserSwitcher()).isNull() + + // Update the setting for the observed user: now we will be notified and the button should + // be there. + utils.setUserSwitcherEnabled(settings, true, userId) + val userSwitcher = currentUserSwitcher() + assertThat(userSwitcher).isNotNull() + assertThat(userSwitcher!!.contentDescription) + .isEqualTo(ContentDescription.Loaded("Signed in as foo")) + assertThat(userSwitcher.icon).isEqualTo(Icon.Loaded(picture)) + assertThat(userSwitcher.background).isEqualTo(R.drawable.qs_footer_action_circle) + + // Change the current user name. + userSwitcherControllerWrapper.currentUserName = "bar" + assertThat(currentUserSwitcher()?.contentDescription) + .isEqualTo(ContentDescription.Loaded("Signed in as bar")) + + fun iconTint(): Int? = currentUserSwitcher()!!.iconTint + + // We tint the icon if the current user is not the guest. + assertThat(iconTint()).isNull() + + // Make the UserManager return that the current user is the guest. A change of the user + // info will be fired to notify us of that change. + isGuestUser = true + + // At this point, there was no change of the user info yet so we still didn't pick the + // UserManager change. + assertThat(iconTint()).isNull() + + // Trigger a user info change: there should now be a tint. + userInfoController.updateInfo { userAccount = "doe" } + assertThat(iconTint()) + .isEqualTo( + Utils.getColorAttrDefaultColor( + context, + android.R.attr.colorForeground, + ) + ) + + // Make sure we don't tint the icon if it is a user image (and not the default image), even + // in guest mode. + userInfoController.updateInfo { this.picture = mock<UserIconDrawable>() } + assertThat(iconTint()).isNull() + + job.cancel() + } + + @Test + fun security() = runBlockingTest { + val securityController = FakeSecurityController() + val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() + + // Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the + // logic in securityToConfig. + var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null } + whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer { + securityToConfig(it.arguments.first() as SecurityModel) + } + + val underTest = + utils.footerActionsViewModel( + footerActionsInteractor = + utils.footerActionsInteractor( + qsSecurityFooterUtils = qsSecurityFooterUtils, + securityRepository = + utils.securityRepository( + securityController = securityController, + ), + ), + ) + + // Collect the security model into currentSecurity. + var currentSecurity: FooterActionsSecurityButtonViewModel? = null + val job = launch { underTest.security.collect { currentSecurity = it } } + fun currentSecurity(): FooterActionsSecurityButtonViewModel? { + advanceUntilIdle() + return currentSecurity + } + + // By default, we always return a null SecurityButtonConfig. + assertThat(currentSecurity()).isNull() + + // Map any SecurityModel into a non-null SecurityButtonConfig. + val buttonConfig = + SecurityButtonConfig( + icon = Icon.Resource(0), + text = "foo", + isClickable = true, + ) + securityToConfig = { buttonConfig } + + // There was no change of the security info yet, so the mapper was not called yet. + assertThat(currentSecurity()).isNull() + + // Trigger a SecurityModel change, which will call the mapper and add a button. + securityController.updateState {} + var security = currentSecurity() + assertThat(security).isNotNull() + assertThat(security!!.icon).isEqualTo(buttonConfig.icon) + assertThat(security.text).isEqualTo(buttonConfig.text) + assertThat(security.onClick).isNotNull() + + // If the config.clickable = false, then onClick should be null. + securityToConfig = { buttonConfig.copy(isClickable = false) } + securityController.updateState {} + security = currentSecurity() + assertThat(security).isNotNull() + assertThat(security!!.onClick).isNull() + + job.cancel() + } + + @Test + fun foregroundServices() = runBlockingTest { + val securityController = FakeSecurityController() + val fgsManagerController = + FakeFgsManagerController( + isAvailable = true, + showFooterDot = false, + numRunningPackages = 0, + ) + val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() + + // Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the + // logic in securityToConfig. + var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null } + whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer { + securityToConfig(it.arguments.first() as SecurityModel) + } + + val underTest = + utils.footerActionsViewModel( + footerActionsInteractor = + utils.footerActionsInteractor( + qsSecurityFooterUtils = qsSecurityFooterUtils, + securityRepository = utils.securityRepository(securityController), + foregroundServicesRepository = + utils.foregroundServicesRepository(fgsManagerController), + ), + ) + + // Collect the security model into currentSecurity. + var currentForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null + val job = launch { underTest.foregroundServices.collect { currentForegroundServices = it } } + fun currentForegroundServices(): FooterActionsForegroundServicesButtonViewModel? { + advanceUntilIdle() + return currentForegroundServices + } + + // We don't show the foreground services button if the number of running packages is not + // > 1. + assertThat(currentForegroundServices()).isNull() + + // We show it at soon as the number of services is at least 1. Given that there is no + // security, it should be displayed with text. + fgsManagerController.numRunningPackages = 1 + val foregroundServices = currentForegroundServices() + assertThat(foregroundServices).isNotNull() + assertThat(foregroundServices!!.foregroundServicesCount).isEqualTo(1) + assertThat(foregroundServices.text).isEqualTo("1 app is active") + assertThat(foregroundServices.displayText).isTrue() + assertThat(foregroundServices.onClick).isNotNull() + + // We handle plurals correctly. + fgsManagerController.numRunningPackages = 3 + assertThat(currentForegroundServices()?.text).isEqualTo("3 apps are active") + + // Showing new changes (the footer dot) is currently disabled. + assertThat(foregroundServices.hasNewChanges).isFalse() + + // Enabling it will show the new changes. + fgsManagerController.showFooterDot.value = true + assertThat(currentForegroundServices()?.hasNewChanges).isTrue() + + // Dismissing the dialog should remove the new changes dot. + fgsManagerController.simulateDialogDismiss() + assertThat(currentForegroundServices()?.hasNewChanges).isFalse() + + // Showing the security button will make this show as a simple button without text. + assertThat(foregroundServices.displayText).isTrue() + securityToConfig = { + SecurityButtonConfig( + icon = Icon.Resource(0), + text = "foo", + isClickable = true, + ) + } + securityController.updateState {} + assertThat(currentForegroundServices()?.displayText).isFalse() + + job.cancel() + } + + @Test + fun observeDeviceMonitoringDialogRequests() = runBlockingTest { + val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() + val broadcastDispatcher = mock<BroadcastDispatcher>() + + // Return a fake broadcastFlow that emits 3 fake events when collected. + val broadcastFlow = flowOf(Unit, Unit, Unit) + whenever( + broadcastDispatcher.broadcastFlow( + any(), + nullable(), + anyInt(), + nullable(), + ) + ) + .thenAnswer { broadcastFlow } + + // Increment nDialogRequests whenever a request to show the dialog is made by the + // FooterActionsInteractor. + var nDialogRequests = 0 + whenever(qsSecurityFooterUtils.showDeviceMonitoringDialog(any(), nullable())).then { + nDialogRequests++ + } + + val underTest = + utils.footerActionsViewModel( + footerActionsInteractor = + utils.footerActionsInteractor( + qsSecurityFooterUtils = qsSecurityFooterUtils, + broadcastDispatcher = broadcastDispatcher, + ), + ) + + val job = launch { + underTest.observeDeviceMonitoringDialogRequests(quickSettingsContext = mock()) + } + + advanceUntilIdle() + assertThat(nDialogRequests).isEqualTo(3) + + job.cancel() + } + + @Test + fun isVisible() { + val underTest = utils.footerActionsViewModel() + assertThat(underTest.isVisible.value).isTrue() + + underTest.onVisibilityChangeRequested(visible = false) + assertThat(underTest.isVisible.value).isFalse() + + underTest.onVisibilityChangeRequested(visible = true) + assertThat(underTest.isVisible.value).isTrue() + } +} 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 7d28871e340c..98389c2c7a6f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -259,7 +259,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { private DisplayMetrics mDisplayMetrics = new DisplayMetrics(); @Mock private KeyguardClockSwitch mKeyguardClockSwitch; - private PanelViewController.TouchHandler mTouchHandler; + private NotificationPanelViewController.TouchHandler mTouchHandler; private ConfigurationController mConfigurationController; @Mock private MediaHierarchyManager mMediaHiearchyManager; @@ -454,7 +454,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { doAnswer((Answer<Void>) invocation -> { mTouchHandler = invocation.getArgument(0); return null; - }).when(mView).setOnTouchListener(any(PanelViewController.TouchHandler.class)); + }).when(mView).setOnTouchListener(any(NotificationPanelViewController.TouchHandler.class)); NotificationWakeUpCoordinator coordinator = new NotificationWakeUpCoordinator( @@ -1407,7 +1407,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mQsFrame.getWidth()).thenReturn(1000); when(mQsHeader.getTop()).thenReturn(0); when(mQsHeader.getBottom()).thenReturn(1000); - PanelViewController.TouchHandler touchHandler = + NotificationPanelViewController.TouchHandler touchHandler = mNotificationPanelViewController.createTouchHandler(); mNotificationPanelViewController.setExpandedFraction(1f); @@ -1427,7 +1427,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mQsFrame.getWidth()).thenReturn(1000); when(mQsHeader.getTop()).thenReturn(0); when(mQsHeader.getBottom()).thenReturn(1000); - PanelViewController.TouchHandler touchHandler = + NotificationPanelViewController.TouchHandler touchHandler = mNotificationPanelViewController.createTouchHandler(); mNotificationPanelViewController.setExpandedFraction(1f); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 9892448aed03..a61fba5c4000 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -26,22 +26,22 @@ import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.shade.PanelViewController +import com.android.systemui.shade.NotificationPanelViewController import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.unfold.SysUIUnfoldComponent import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider -import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.mockito.any +import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.mockito.ArgumentCaptor import org.mockito.Mock -import org.mockito.Mockito.spy -import org.mockito.Mockito.mock import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import java.util.Optional @@ -52,7 +52,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { private val touchEventHandler = TestTouchEventHandler() @Mock - private lateinit var panelViewController: PanelViewController + private lateinit var notificationPanelViewController: NotificationPanelViewController @Mock private lateinit var panelView: ViewGroup @Mock @@ -76,7 +76,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - `when`(panelViewController.view).thenReturn(panelView) + `when`(notificationPanelViewController.view).thenReturn(panelView) `when`(sysuiUnfoldComponent.getStatusBarMoveFromCenterAnimationController()) .thenReturn(moveFromCenterAnimation) // create the view on main thread as it requires main looper diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt index d6c995bef229..5aa7f92d22e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt @@ -20,7 +20,7 @@ import android.view.MotionEvent import android.view.ViewGroup import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.shade.PanelViewController +import com.android.systemui.shade.NotificationPanelViewController import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -32,7 +32,7 @@ import org.mockito.MockitoAnnotations class PhoneStatusBarViewTest : SysuiTestCase() { @Mock - private lateinit var panelViewController: PanelViewController + private lateinit var notificationPanelViewController: NotificationPanelViewController @Mock private lateinit var panelView: ViewGroup @@ -43,7 +43,7 @@ class PhoneStatusBarViewTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) // TODO(b/197137564): Setting up a panel view and its controller feels unnecessary when // testing just [PhoneStatusBarView]. - `when`(panelViewController.view).thenReturn(panelView) + `when`(notificationPanelViewController.view).thenReturn(panelView) view = PhoneStatusBarView(mContext, null) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt index 7b1455cb2e46..b53ad0a3726f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlags.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt @@ -56,7 +56,6 @@ class FakeFeatureFlags : FeatureFlags { stringFlags.put(flag.id, value) } - override fun isEnabled(flag: UnreleasedFlag): Boolean = requireBooleanValue(flag.id) override fun isEnabled(flag: ReleasedFlag): Boolean = requireBooleanValue(flag.id) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt new file mode 100644 index 000000000000..527258579372 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs + +import android.view.View +import com.android.systemui.qs.FgsManagerController.OnDialogDismissedListener +import com.android.systemui.qs.FgsManagerController.OnNumberOfPackagesChangedListener +import kotlinx.coroutines.flow.MutableStateFlow + +/** A fake [FgsManagerController] to be used in tests. */ +class FakeFgsManagerController( + isAvailable: Boolean = true, + showFooterDot: Boolean = false, + numRunningPackages: Int = 0, +) : FgsManagerController { + override val isAvailable: MutableStateFlow<Boolean> = MutableStateFlow(isAvailable) + + override var numRunningPackages = numRunningPackages + set(value) { + if (value != field) { + field = value + newChangesSinceDialogWasDismissed = true + numRunningPackagesListeners.forEach { it.onNumberOfPackagesChanged(value) } + } + } + + override var newChangesSinceDialogWasDismissed = false + private set + + override val showFooterDot: MutableStateFlow<Boolean> = MutableStateFlow(showFooterDot) + + private val numRunningPackagesListeners = LinkedHashSet<OnNumberOfPackagesChangedListener>() + private val dialogDismissedListeners = LinkedHashSet<OnDialogDismissedListener>() + + /** Simulate that a fgs dialog was just dismissed. */ + fun simulateDialogDismiss() { + newChangesSinceDialogWasDismissed = false + dialogDismissedListeners.forEach { it.onDialogDismissed() } + } + + override fun init() {} + + override fun showDialog(viewLaunchedFrom: View?) {} + + override fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) { + numRunningPackagesListeners.add(listener) + } + + override fun removeOnNumberOfPackagesChangedListener( + listener: OnNumberOfPackagesChangedListener + ) { + numRunningPackagesListeners.remove(listener) + } + + override fun addOnDialogDismissedListener(listener: OnDialogDismissedListener) { + dialogDismissedListeners.add(listener) + } + + override fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener) { + dialogDismissedListeners.remove(listener) + } + + override fun shouldUpdateFooterVisibility(): Boolean = false + + override fun visibleButtonsCount(): Int = 0 +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt new file mode 100644 index 000000000000..2a9aeddc9aa8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.footer + +import android.content.Context +import android.os.Handler +import android.os.UserManager +import android.provider.Settings +import android.testing.TestableLooper +import com.android.internal.logging.MetricsLogger +import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.testing.FakeMetricsLogger +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.classifier.FalsingManagerFake +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.globalactions.GlobalActionsDialogLite +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.FakeFgsManagerController +import com.android.systemui.qs.FgsManagerController +import com.android.systemui.qs.QSSecurityFooterUtils +import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository +import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl +import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository +import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl +import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor +import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel +import com.android.systemui.qs.user.UserSwitchDialogController +import com.android.systemui.security.data.repository.SecurityRepository +import com.android.systemui.security.data.repository.SecurityRepositoryImpl +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.statusbar.policy.FakeSecurityController +import com.android.systemui.statusbar.policy.FakeUserInfoController +import com.android.systemui.statusbar.policy.SecurityController +import com.android.systemui.statusbar.policy.UserInfoController +import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.time.FakeSystemClock +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.test.TestCoroutineDispatcher + +/** + * Util class to create real implementations of the FooterActions repositories, viewModel and + * interactor to be used in tests. + */ +class FooterActionsTestUtils( + private val context: Context, + private val testableLooper: TestableLooper, + private val fakeClock: FakeSystemClock = FakeSystemClock(), +) { + /** Enable or disable the user switcher in the settings. */ + fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) { + settings.putBoolForUser(Settings.Global.USER_SWITCHER_ENABLED, enabled, userId) + + // The settings listener is processing messages on the bgHandler (usually backed by a + // testableLooper in tests), so let's make sure we process the callback before continuing. + testableLooper.processAllMessages() + } + + /** Create a [FooterActionsViewModel] to be used in tests. */ + fun footerActionsViewModel( + @Application context: Context = this.context.applicationContext, + footerActionsInteractor: FooterActionsInteractor = footerActionsInteractor(), + falsingManager: FalsingManager = FalsingManagerFake(), + globalActionsDialogLite: GlobalActionsDialogLite = mock(), + showPowerButton: Boolean = true, + ): FooterActionsViewModel { + return FooterActionsViewModel( + context, + footerActionsInteractor, + falsingManager, + globalActionsDialogLite, + showPowerButton, + ) + } + + /** Create a [FooterActionsInteractor] to be used in tests. */ + fun footerActionsInteractor( + activityStarter: ActivityStarter = mock(), + featureFlags: FeatureFlags = FakeFeatureFlags(), + metricsLogger: MetricsLogger = FakeMetricsLogger(), + uiEventLogger: UiEventLogger = UiEventLoggerFake(), + deviceProvisionedController: DeviceProvisionedController = mock(), + qsSecurityFooterUtils: QSSecurityFooterUtils = mock(), + fgsManagerController: FgsManagerController = mock(), + userSwitchDialogController: UserSwitchDialogController = mock(), + securityRepository: SecurityRepository = securityRepository(), + foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(), + userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(), + broadcastDispatcher: BroadcastDispatcher = mock(), + bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + ): FooterActionsInteractor { + return FooterActionsInteractorImpl( + activityStarter, + featureFlags, + metricsLogger, + uiEventLogger, + deviceProvisionedController, + qsSecurityFooterUtils, + fgsManagerController, + userSwitchDialogController, + securityRepository, + foregroundServicesRepository, + userSwitcherRepository, + broadcastDispatcher, + bgDispatcher, + ) + } + + /** Create a [SecurityRepository] to be used in tests. */ + fun securityRepository( + securityController: SecurityController = FakeSecurityController(), + bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + ): SecurityRepository { + return SecurityRepositoryImpl( + securityController, + bgDispatcher, + ) + } + + /** Create a [SecurityRepository] to be used in tests. */ + fun foregroundServicesRepository( + fgsManagerController: FakeFgsManagerController = FakeFgsManagerController(), + ): ForegroundServicesRepository { + return ForegroundServicesRepositoryImpl(fgsManagerController) + } + + /** Create a [UserSwitcherRepository] to be used in tests. */ + fun userSwitcherRepository( + @Application context: Context = this.context.applicationContext, + bgHandler: Handler = Handler(testableLooper.looper), + bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(), + userManager: UserManager = mock(), + userTracker: UserTracker = FakeUserTracker(), + userSwitcherController: UserSwitcherController = mock(), + userInfoController: UserInfoController = FakeUserInfoController(), + settings: GlobalSettings = FakeSettings(), + ): UserSwitcherRepository { + return UserSwitcherRepositoryImpl( + context, + bgHandler, + bgDispatcher, + userManager, + userTracker, + userSwitcherController, + userInfoController, + settings, + ) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt new file mode 100644 index 000000000000..b2b176420e40 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings + +import android.content.ContentResolver +import android.content.Context +import android.content.pm.UserInfo +import android.os.UserHandle +import android.test.mock.MockContentResolver +import com.android.systemui.util.mockito.mock +import java.util.concurrent.Executor + +/** A fake [UserTracker] to be used in tests. */ +class FakeUserTracker( + userId: Int = 0, + userHandle: UserHandle = UserHandle.of(userId), + userInfo: UserInfo = mock(), + userProfiles: List<UserInfo> = emptyList(), + userContentResolver: ContentResolver = MockContentResolver(), + userContext: Context = mock(), + private val onCreateCurrentUserContext: (Context) -> Context = { mock() }, +) : UserTracker { + val callbacks = mutableListOf<UserTracker.Callback>() + + override val userId: Int = userId + override val userHandle: UserHandle = userHandle + override val userInfo: UserInfo = userInfo + override val userProfiles: List<UserInfo> = userProfiles + + override val userContentResolver: ContentResolver = userContentResolver + override val userContext: Context = userContext + + override fun addCallback(callback: UserTracker.Callback, executor: Executor) { + callbacks.add(callback) + } + + override fun removeCallback(callback: UserTracker.Callback) { + callbacks.remove(callback) + } + + override fun createCurrentUserContext(context: Context): Context { + return onCreateCurrentUserContext(context) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt new file mode 100644 index 000000000000..c6aa3952c4b1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import android.app.admin.DeviceAdminInfo +import android.content.ComponentName +import android.graphics.drawable.Drawable +import java.io.PrintWriter + +/** A fake [SecurityController] to be used in tests. */ +class FakeSecurityController( + private val fakeState: FakeState = FakeState(), +) : SecurityController { + private val callbacks = LinkedHashSet<SecurityController.SecurityControllerCallback>() + + override fun addCallback(callback: SecurityController.SecurityControllerCallback) { + callbacks.add(callback) + } + + override fun removeCallback(callback: SecurityController.SecurityControllerCallback) { + callbacks.remove(callback) + } + + /** Update [fakeState], then notify the callbacks. */ + fun updateState(f: FakeState.() -> Unit) { + fakeState.f() + callbacks.forEach { it.onStateChanged() } + } + + override fun dump(pw: PrintWriter, args: Array<out String>) {} + + override fun isDeviceManaged(): Boolean = fakeState.isDeviceManaged + + override fun hasProfileOwner(): Boolean = fakeState.hasProfileOwner + + override fun hasWorkProfile(): Boolean = fakeState.hasWorkProfile + + override fun isWorkProfileOn(): Boolean = fakeState.isWorkProfileOn + + override fun isProfileOwnerOfOrganizationOwnedDevice(): Boolean = + fakeState.isProfileOwnerOfOrganizationOwnedDevice + + override fun getDeviceOwnerName(): String? = fakeState.deviceOwnerName + + override fun getProfileOwnerName(): String? = fakeState.profileOwnerName + + override fun getDeviceOwnerOrganizationName(): String? = fakeState.deviceOwnerOrganizationName + + override fun getWorkProfileOrganizationName(): String? = fakeState.workProfileOrganizationName + + override fun getDeviceOwnerComponentOnAnyUser(): ComponentName? = + fakeState.deviceOwnerComponentOnAnyUser + + override fun getDeviceOwnerType(admin: ComponentName?): Int = 0 + + override fun isNetworkLoggingEnabled(): Boolean = fakeState.isNetworkLoggingEnabled + + override fun isVpnEnabled(): Boolean = fakeState.isVpnEnabled + + override fun isVpnRestricted(): Boolean = fakeState.isVpnRestricted + + override fun isVpnBranded(): Boolean = fakeState.isVpnBranded + + override fun getPrimaryVpnName(): String? = fakeState.primaryVpnName + + override fun getWorkProfileVpnName(): String? = fakeState.workProfileVpnName + + override fun hasCACertInCurrentUser(): Boolean = fakeState.hasCACertInCurrentUser + + override fun hasCACertInWorkProfile(): Boolean = fakeState.hasCACertInWorkProfile + + override fun onUserSwitched(newUserId: Int) {} + + override fun isParentalControlsEnabled(): Boolean = fakeState.isParentalControlsEnabled + + override fun getDeviceAdminInfo(): DeviceAdminInfo? = fakeState.deviceAdminInfo + + override fun getIcon(info: DeviceAdminInfo?): Drawable? = null + + override fun getLabel(info: DeviceAdminInfo?): CharSequence? = null + + class FakeState( + var isDeviceManaged: Boolean = false, + var hasProfileOwner: Boolean = false, + var hasWorkProfile: Boolean = false, + var isWorkProfileOn: Boolean = false, + var isProfileOwnerOfOrganizationOwnedDevice: Boolean = false, + var deviceOwnerName: String? = null, + var profileOwnerName: String? = null, + var deviceOwnerOrganizationName: String? = null, + var workProfileOrganizationName: String? = null, + var deviceOwnerComponentOnAnyUser: ComponentName? = null, + var isNetworkLoggingEnabled: Boolean = false, + var isVpnEnabled: Boolean = false, + var isVpnRestricted: Boolean = false, + var isVpnBranded: Boolean = false, + var primaryVpnName: String? = null, + var workProfileVpnName: String? = null, + var hasCACertInCurrentUser: Boolean = false, + var hasCACertInWorkProfile: Boolean = false, + var isParentalControlsEnabled: Boolean = false, + var deviceAdminInfo: DeviceAdminInfo? = null, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeUserInfoController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeUserInfoController.kt new file mode 100644 index 000000000000..32b9a371674f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeUserInfoController.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import android.graphics.drawable.Drawable +import com.android.systemui.util.mockito.mock + +/** A fake [UserInfoController] to be used in tests. */ +class FakeUserInfoController( + private val fakeInfo: FakeInfo = FakeInfo(), +) : UserInfoController { + private val listeners = LinkedHashSet<UserInfoController.OnUserInfoChangedListener>() + + /** Update [fakeInfo], then notify the listeners. */ + fun updateInfo(f: FakeInfo.() -> Unit) { + fakeInfo.f() + notifyListeners() + } + + private fun notifyListeners() { + listeners.forEach { listener -> + listener.onUserInfoChanged(fakeInfo.name, fakeInfo.picture, fakeInfo.userAccount) + } + } + + override fun addCallback(listener: UserInfoController.OnUserInfoChangedListener) { + listeners.add(listener) + + // The actual implementation notifies the listener when adding it. + listener.onUserInfoChanged(fakeInfo.name, fakeInfo.picture, fakeInfo.userAccount) + } + + override fun removeCallback(listener: UserInfoController.OnUserInfoChangedListener) { + listeners.remove(listener) + } + + override fun reloadUserInfo() {} + + class FakeInfo( + var name: String = "", + var picture: Drawable = mock(), + var userAccount: String = "", + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/MockUserSwitcherControllerWrapper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/MockUserSwitcherControllerWrapper.kt new file mode 100644 index 000000000000..1304a12a5f7f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/MockUserSwitcherControllerWrapper.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import com.android.systemui.statusbar.policy.UserSwitcherController.UserSwitchCallback +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import org.mockito.Mockito.`when` as whenever + +/** + * A wrapper around a mocked [UserSwitcherController] to be used in tests. + * + * Note that this was implemented as a mock wrapper instead of fake implementation of a common + * interface given how big the UserSwitcherController grew. + */ +class MockUserSwitcherControllerWrapper( + currentUserName: String = "", +) { + val controller: UserSwitcherController = mock() + private val callbacks = LinkedHashSet<UserSwitchCallback>() + + var currentUserName = currentUserName + set(value) { + if (value != field) { + field = value + notifyCallbacks() + } + } + + private fun notifyCallbacks() { + callbacks.forEach { it.onUserSwitched() } + } + + init { + whenever(controller.addUserSwitchCallback(any())).then { invocation -> + callbacks.add(invocation.arguments.first() as UserSwitchCallback) + } + + whenever(controller.removeUserSwitchCallback(any())).then { invocation -> + callbacks.remove(invocation.arguments.first() as UserSwitchCallback) + } + + whenever(controller.currentUserName).thenAnswer { this.currentUserName } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt new file mode 100644 index 000000000000..48cd345b1f68 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.truth.correspondence + +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.logging.testing.UiEventLoggerFake.FakeUiEvent +import com.google.common.truth.Correspondence + +/** Instances of [Correspondence] to match a [UiEventLoggerFake.FakeUiEvent] with Truth. */ +object FakeUiEvent { + val EVENT_ID = + Correspondence.transforming<FakeUiEvent, Int>( + { it?.eventId }, + "has a eventId of", + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt new file mode 100644 index 000000000000..3f0a95248d9c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.truth.correspondence + +import android.metrics.LogMaker +import com.google.common.truth.Correspondence + +/** Instances of [Correspondence] to match a [LogMaker] with Truth. */ +object LogMaker { + val CATEGORY = + Correspondence.transforming<LogMaker, Int>( + { it?.category }, + "has a category of", + ) +} diff --git a/services/core/java/com/android/server/am/UidObserverController.java b/services/core/java/com/android/server/am/UidObserverController.java index e8c1b545eb96..51cb9878c0b3 100644 --- a/services/core/java/com/android/server/am/UidObserverController.java +++ b/services/core/java/com/android/server/am/UidObserverController.java @@ -15,6 +15,7 @@ */ package com.android.server.am; +import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_UID_OBSERVERS; @@ -25,6 +26,7 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManagerProto; import android.app.IUidObserver; +import android.content.pm.PackageManager; import android.os.Handler; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -81,7 +83,9 @@ public class UidObserverController { @NonNull String callingPackage, int callingUid) { synchronized (mLock) { mUidObservers.register(observer, new UidObserverRegistration(callingUid, - callingPackage, which, cutpoint)); + callingPackage, which, cutpoint, + ActivityManager.checkUidPermission(INTERACT_ACROSS_USERS_FULL, callingUid) + == PackageManager.PERMISSION_GRANTED)); } } @@ -252,6 +256,11 @@ public class UidObserverController { final ChangeRecord item = mActiveUidChanges[j]; final long start = SystemClock.uptimeMillis(); final int change = item.change; + // Does the user have permission? Don't send a non user UID change otherwise + if (UserHandle.getUserId(item.uid) != UserHandle.getUserId(reg.mUid) + && !reg.mCanInteractAcrossUsers) { + continue; + } if (change == UidRecord.CHANGE_PROCSTATE && (reg.mWhich & ActivityManager.UID_OBSERVER_PROCSTATE) == 0) { // No-op common case: no significant change, the observer is not @@ -437,6 +446,7 @@ public class UidObserverController { private final String mPkg; private final int mWhich; private final int mCutpoint; + private final boolean mCanInteractAcrossUsers; /** * Total # of callback calls that took more than {@link #SLOW_UID_OBSERVER_THRESHOLD_MS}. @@ -467,11 +477,13 @@ public class UidObserverController { ActivityManagerProto.UID_OBSERVER_FLAG_PROC_OOM_ADJ, }; - UidObserverRegistration(int uid, @NonNull String pkg, int which, int cutpoint) { + UidObserverRegistration(int uid, @NonNull String pkg, int which, int cutpoint, + boolean canInteractAcrossUsers) { this.mUid = uid; this.mPkg = pkg; this.mWhich = which; this.mCutpoint = cutpoint; + this.mCanInteractAcrossUsers = canInteractAcrossUsers; mLastProcStates = cutpoint >= ActivityManager.MIN_PROCESS_STATE ? new SparseIntArray() : null; } diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java b/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java index 0d789f7a1840..3566b430b680 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java @@ -25,6 +25,7 @@ import static android.hardware.biometrics.BiometricStateListener.STATE_KEYGUARD_ import android.annotation.NonNull; import android.hardware.biometrics.BiometricStateListener; import android.hardware.biometrics.IBiometricStateListener; +import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -35,7 +36,7 @@ import java.util.concurrent.CopyOnWriteArrayList; /** * A callback for receiving notifications about biometric sensor state changes. */ -public class BiometricStateCallback implements ClientMonitorCallback { +public class BiometricStateCallback implements ClientMonitorCallback, IBinder.DeathRecipient { private static final String TAG = "BiometricStateCallback"; @@ -153,5 +154,25 @@ public class BiometricStateCallback implements ClientMonitorCallback { */ public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) { mBiometricStateListeners.add(listener); + try { + listener.asBinder().linkToDeath(this, 0 /* flags */); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death", e); + } + } + + @Override + public void binderDied() { + // Do nothing, handled below + } + + @Override + public void binderDied(IBinder who) { + Slog.w(TAG, "Callback binder died: " + who); + if (mBiometricStateListeners.removeIf(listener -> listener.asBinder().equals(who))) { + Slog.w(TAG, "Removed dead listener for " + who); + } else { + Slog.w(TAG, "No dead listeners found"); + } } } diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java new file mode 100644 index 000000000000..48112c452f02 --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.broadcastradio.hal2; + +import android.util.IndentingPrintWriter; +import android.util.LocalLog; +import android.util.Log; +import android.util.Slog; + +final class RadioEventLogger { + private final String mTag; + private final LocalLog mEventLogger; + + RadioEventLogger(String tag, int loggerQueueSize) { + mTag = tag; + mEventLogger = new LocalLog(loggerQueueSize); + } + + void logRadioEvent(String logFormat, Object... args) { + String log = String.format(logFormat, args); + mEventLogger.log(log); + if (Log.isLoggable(mTag, Log.DEBUG)) { + Slog.d(mTag, log); + } + } + + void dump(IndentingPrintWriter pw) { + mEventLogger.dump(pw); + } +} diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java index 852aa66866f0..0a23e385d67a 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java @@ -55,12 +55,14 @@ import java.util.stream.Collectors; class RadioModule { private static final String TAG = "BcRadio2Srv.module"; + private static final int RADIO_EVENT_LOGGER_QUEUE_SIZE = 25; @NonNull private final IBroadcastRadio mService; @NonNull public final RadioManager.ModuleProperties mProperties; private final Object mLock; @NonNull private final Handler mHandler; + @NonNull private final RadioEventLogger mEventLogger; @GuardedBy("mLock") private ITunerSession mHalTunerSession; @@ -138,6 +140,7 @@ class RadioModule { mService = Objects.requireNonNull(service); mLock = Objects.requireNonNull(lock); mHandler = new Handler(Looper.getMainLooper()); + mEventLogger = new RadioEventLogger(TAG, RADIO_EVENT_LOGGER_QUEUE_SIZE); } public static @Nullable RadioModule tryLoadingModule(int idx, @NonNull String fqName, @@ -176,13 +179,14 @@ class RadioModule { public @NonNull TunerSession openSession(@NonNull android.hardware.radio.ITunerCallback userCb) throws RemoteException { - Slog.i(TAG, "Open TunerSession"); + mEventLogger.logRadioEvent("Open TunerSession"); synchronized (mLock) { if (mHalTunerSession == null) { Mutable<ITunerSession> hwSession = new Mutable<>(); mService.openSession(mHalTunerCallback, (result, session) -> { Convert.throwOnError("openSession", result); hwSession.value = session; + mEventLogger.logRadioEvent("New HIDL 2.0 tuner session is opened"); }); mHalTunerSession = Objects.requireNonNull(hwSession.value); } @@ -207,7 +211,7 @@ class RadioModule { // Copy the contents of mAidlTunerSessions into a local array because TunerSession.close() // must be called without mAidlTunerSessions locked because it can call // onTunerSessionClosed(). - Slog.i(TAG, "Close TunerSessions"); + mEventLogger.logRadioEvent("Close TunerSessions"); TunerSession[] tunerSessions; synchronized (mLock) { tunerSessions = new TunerSession[mAidlTunerSessions.size()]; @@ -320,7 +324,7 @@ class RadioModule { } onTunerSessionProgramListFilterChanged(null); if (mAidlTunerSessions.isEmpty() && mHalTunerSession != null) { - Slog.i(TAG, "Closing HAL tuner session"); + mEventLogger.logRadioEvent("Closing HAL tuner session"); try { mHalTunerSession.close(); } catch (RemoteException ex) { @@ -372,7 +376,7 @@ class RadioModule { public android.hardware.radio.ICloseHandle addAnnouncementListener(@NonNull int[] enabledTypes, @NonNull android.hardware.radio.IAnnouncementListener listener) throws RemoteException { - Slog.i(TAG, "Add AnnouncementListener"); + mEventLogger.logRadioEvent("Add AnnouncementListener"); ArrayList<Byte> enabledList = new ArrayList<>(); for (int type : enabledTypes) { enabledList.add((byte)type); @@ -409,7 +413,7 @@ class RadioModule { } Bitmap getImage(int id) { - Slog.i(TAG, "Get image for id " + id); + mEventLogger.logRadioEvent("Get image for id %d", id); if (id == 0) throw new IllegalArgumentException("Image ID is missing"); byte[] rawImage; @@ -449,6 +453,10 @@ class RadioModule { } pw.decreaseIndent(); } + pw.printf("Radio module events:\n"); + pw.increaseIndent(); + mEventLogger.dump(pw); + pw.decreaseIndent(); pw.decreaseIndent(); } } diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java index 41f753c11216..918dc98e3a9e 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java @@ -28,7 +28,6 @@ import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.os.RemoteException; import android.util.IndentingPrintWriter; -import android.util.Log; import android.util.MutableBoolean; import android.util.MutableInt; import android.util.Slog; @@ -41,8 +40,10 @@ import java.util.Objects; class TunerSession extends ITuner.Stub { private static final String TAG = "BcRadio2Srv.session"; private static final String kAudioDeviceName = "Radio tuner source"; + private static final int TUNER_EVENT_LOGGER_QUEUE_SIZE = 25; private final Object mLock; + @NonNull private final RadioEventLogger mEventLogger; private final RadioModule mModule; private final ITunerSession mHwSession; @@ -61,15 +62,12 @@ class TunerSession extends ITuner.Stub { mHwSession = Objects.requireNonNull(hwSession); mCallback = Objects.requireNonNull(callback); mLock = Objects.requireNonNull(lock); - } - - private boolean isDebugEnabled() { - return Log.isLoggable(TAG, Log.DEBUG); + mEventLogger = new RadioEventLogger(TAG, TUNER_EVENT_LOGGER_QUEUE_SIZE); } @Override public void close() { - if (isDebugEnabled()) Slog.d(TAG, "Close"); + mEventLogger.logRadioEvent("Close"); close(null); } @@ -81,7 +79,7 @@ class TunerSession extends ITuner.Stub { * @param error Optional error to send to client before session is closed. */ public void close(@Nullable Integer error) { - if (isDebugEnabled()) Slog.d(TAG, "Close on error " + error); + mEventLogger.logRadioEvent("Close on error %d", error); synchronized (mLock) { if (mIsClosed) return; if (error != null) { @@ -145,10 +143,8 @@ class TunerSession extends ITuner.Stub { @Override public void step(boolean directionDown, boolean skipSubChannel) throws RemoteException { - if (isDebugEnabled()) { - Slog.d(TAG, "Step with directionDown " + directionDown - + " skipSubChannel " + skipSubChannel); - } + mEventLogger.logRadioEvent("Step with direction %s, skipSubChannel? %s", + directionDown ? "down" : "up", skipSubChannel ? "yes" : "no"); synchronized (mLock) { checkNotClosedLocked(); int halResult = mHwSession.step(!directionDown); @@ -158,10 +154,8 @@ class TunerSession extends ITuner.Stub { @Override public void scan(boolean directionDown, boolean skipSubChannel) throws RemoteException { - if (isDebugEnabled()) { - Slog.d(TAG, "Scan with directionDown " + directionDown - + " skipSubChannel " + skipSubChannel); - } + mEventLogger.logRadioEvent("Scan with direction %s, skipSubChannel? %s", + directionDown ? "down" : "up", skipSubChannel ? "yes" : "no"); synchronized (mLock) { checkNotClosedLocked(); int halResult = mHwSession.scan(!directionDown, skipSubChannel); @@ -171,7 +165,7 @@ class TunerSession extends ITuner.Stub { @Override public void tune(ProgramSelector selector) throws RemoteException { - if (isDebugEnabled()) Slog.d(TAG, "Tune with selector " + selector); + mEventLogger.logRadioEvent("Tune with selector %s", selector); synchronized (mLock) { checkNotClosedLocked(); int halResult = mHwSession.tune(Convert.programSelectorToHal(selector)); @@ -195,7 +189,7 @@ class TunerSession extends ITuner.Stub { @Override public Bitmap getImage(int id) { - if (isDebugEnabled()) Slog.d(TAG, "Get image for " + id); + mEventLogger.logRadioEvent("Get image for %d", id); return mModule.getImage(id); } @@ -208,7 +202,7 @@ class TunerSession extends ITuner.Stub { @Override public void startProgramListUpdates(ProgramList.Filter filter) throws RemoteException { - if (isDebugEnabled()) Slog.d(TAG, "start programList updates " + filter); + mEventLogger.logRadioEvent("start programList updates %s", filter); // If the AIDL client provides a null filter, it wants all updates, so use the most broad // filter. if (filter == null) { @@ -267,7 +261,7 @@ class TunerSession extends ITuner.Stub { @Override public void stopProgramListUpdates() throws RemoteException { - if (isDebugEnabled()) Slog.d(TAG, "Stop programList updates"); + mEventLogger.logRadioEvent("Stop programList updates"); synchronized (mLock) { checkNotClosedLocked(); mProgramInfoCache = null; @@ -291,7 +285,7 @@ class TunerSession extends ITuner.Stub { @Override public boolean isConfigFlagSet(int flag) { - if (isDebugEnabled()) Slog.d(TAG, "Is ConfigFlagSet for " + ConfigFlag.toString(flag)); + mEventLogger.logRadioEvent("Is ConfigFlagSet for %s", ConfigFlag.toString(flag)); synchronized (mLock) { checkNotClosedLocked(); @@ -313,9 +307,7 @@ class TunerSession extends ITuner.Stub { @Override public void setConfigFlag(int flag, boolean value) throws RemoteException { - if (isDebugEnabled()) { - Slog.d(TAG, "Set ConfigFlag " + ConfigFlag.toString(flag) + " = " + value); - } + mEventLogger.logRadioEvent("Set ConfigFlag %s = %b", ConfigFlag.toString(flag), value); synchronized (mLock) { checkNotClosedLocked(); int halResult = mHwSession.setConfigFlag(flag, value); @@ -351,6 +343,10 @@ class TunerSession extends ITuner.Stub { pw.printf("ProgramInfoCache: %s\n", mProgramInfoCache); pw.printf("Config: %s\n", mDummyConfig); } + pw.printf("Tuner session events:\n"); + pw.increaseIndent(); + mEventLogger.dump(pw); + pw.decreaseIndent(); pw.decreaseIndent(); } } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 9691b20066c7..98238ccd93c3 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -4040,6 +4040,7 @@ public class Vpn { mConfig.proxyInfo = profile.proxy; mConfig.requiresInternetValidation = profile.requiresInternetValidation; mConfig.excludeLocalRoutes = profile.excludeLocalRoutes; + mConfig.allowBypass = profile.isBypassable; switch (profile.type) { case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java index c835d2fe1bbd..25d0752844fd 100644 --- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java +++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java @@ -116,10 +116,8 @@ public abstract class BrightnessMappingStrategy { luxLevels = getLuxLevels(resources.getIntArray( com.android.internal.R.array.config_autoBrightnessLevelsIdle)); } else { - brightnessLevelsNits = getFloatArray(resources.obtainTypedArray( - com.android.internal.R.array.config_autoBrightnessDisplayValuesNits)); - luxLevels = getLuxLevels(resources.getIntArray( - com.android.internal.R.array.config_autoBrightnessLevels)); + brightnessLevelsNits = displayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(); + luxLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(); } // Display independent, mode independent values diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 4f3fd6409cd8..3b627ef6a786 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.content.res.TypedArray; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayManagerInternal.RefreshRateLimitation; import android.os.Environment; @@ -149,12 +150,22 @@ import javax.xml.datatype.DatatypeConfigurationException; * </quirks> * * <autoBrightness> - * <brighteningLightDebounceMillis> + * <brighteningLightDebounceMillis> * 2000 - * </brighteningLightDebounceMillis> + * </brighteningLightDebounceMillis> * <darkeningLightDebounceMillis> * 1000 * </darkeningLightDebounceMillis> + * <displayBrightnessMapping> + * <displayBrightnessPoint> + * <lux>50</lux> + * <nits>45</nits> + * </displayBrightnessPoint> + * <displayBrightnessPoint> + * <lux>80</lux> + * <nits>75</nits> + * </displayBrightnessPoint> + * </displayBrightnessMapping> * </autoBrightness> * * <screenBrightnessRampFastDecrease>0.01</screenBrightnessRampFastDecrease> @@ -268,6 +279,39 @@ public class DisplayDeviceConfig { // for the corresponding values above private float[] mBrightness; + + /** + * Array of desired screen brightness in nits corresponding to the lux values + * in the mBrightnessLevelsLux array. The display brightness is defined as the + * measured brightness of an all-white image. The brightness values must be non-negative and + * non-decreasing. This must be overridden in platform specific overlays + */ + private float[] mBrightnessLevelsNits; + + /** + * Array of light sensor lux values to define our levels for auto backlight + * brightness support. + * The N entries of this array define N + 1 control points as follows: + * (1-based arrays) + * + * Point 1: (0, value[1]): lux <= 0 + * Point 2: (level[1], value[2]): 0 < lux <= level[1] + * Point 3: (level[2], value[3]): level[2] < lux <= level[3] + * ... + * Point N+1: (level[N], value[N+1]): level[N] < lux + * + * The control points must be strictly increasing. Each control point + * corresponds to an entry in the brightness backlight values arrays. + * For example, if lux == level[1] (first element of the levels array) + * then the brightness will be determined by value[2] (second element + * of the brightness values array). + * + * Spline interpolation is used to determine the auto-brightness + * backlight values for lux levels between these control points. + * + */ + private float[] mBrightnessLevelsLux; + private float mBacklightMinimum = Float.NaN; private float mBacklightMaximum = Float.NaN; private float mBrightnessDefault = Float.NaN; @@ -661,6 +705,20 @@ public class DisplayDeviceConfig { return mAutoBrightnessBrighteningLightDebounce; } + /** + * @return Auto brightness brightening ambient lux levels + */ + public float[] getAutoBrightnessBrighteningLevelsLux() { + return mBrightnessLevelsLux; + } + + /** + * @return Auto brightness brightening nits levels + */ + public float[] getAutoBrightnessBrighteningLevelsNits() { + return mBrightnessLevelsNits; + } + @Override public String toString() { return "DisplayDeviceConfig{" @@ -703,6 +761,8 @@ public class DisplayDeviceConfig { + mAutoBrightnessBrighteningLightDebounce + ", mAutoBrightnessDarkeningLightDebounce= " + mAutoBrightnessDarkeningLightDebounce + + ", mBrightnessLevelsLux= " + Arrays.toString(mBrightnessLevelsLux) + + ", mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits) + "}"; } @@ -779,6 +839,7 @@ public class DisplayDeviceConfig { loadBrightnessRampsFromConfigXml(); loadAmbientLightSensorFromConfigXml(); setProxSensorUnspecified(); + loadAutoBrightnessConfigsFromConfigXml(); mLoadedFrom = "<config.xml>"; } @@ -991,6 +1052,7 @@ public class DisplayDeviceConfig { private void loadAutoBrightnessConfigValues(DisplayConfiguration config) { loadAutoBrightnessBrighteningLightDebounce(config.getAutoBrightness()); loadAutoBrightnessDarkeningLightDebounce(config.getAutoBrightness()); + loadAutoBrightnessDisplayBrightnessMapping(config.getAutoBrightness()); } /** @@ -1023,6 +1085,33 @@ public class DisplayDeviceConfig { } } + /** + * Loads the auto-brightness display brightness mappings. Internally, this takes care of + * loading the value from the display config, and if not present, falls back to config.xml. + */ + private void loadAutoBrightnessDisplayBrightnessMapping(AutoBrightness autoBrightnessConfig) { + if (autoBrightnessConfig == null + || autoBrightnessConfig.getDisplayBrightnessMapping() == null) { + mBrightnessLevelsNits = getFloatArray(mContext.getResources() + .obtainTypedArray(com.android.internal.R.array + .config_autoBrightnessDisplayValuesNits)); + mBrightnessLevelsLux = getFloatArray(mContext.getResources() + .obtainTypedArray(com.android.internal.R.array + .config_autoBrightnessLevels)); + } else { + final int size = autoBrightnessConfig.getDisplayBrightnessMapping() + .getDisplayBrightnessPoint().size(); + mBrightnessLevelsNits = new float[size]; + mBrightnessLevelsLux = new float[size]; + for (int i = 0; i < size; i++) { + mBrightnessLevelsNits[i] = autoBrightnessConfig.getDisplayBrightnessMapping() + .getDisplayBrightnessPoint().get(i).getNits().floatValue(); + mBrightnessLevelsLux[i] = autoBrightnessConfig.getDisplayBrightnessMapping() + .getDisplayBrightnessPoint().get(i).getLux().floatValue(); + } + } + } + private void loadBrightnessMapFromConfigXml() { // Use the config.xml mapping final Resources res = mContext.getResources(); @@ -1248,6 +1337,10 @@ public class DisplayDeviceConfig { com.android.internal.R.string.config_displayLightSensorType); } + private void loadAutoBrightnessConfigsFromConfigXml() { + loadAutoBrightnessDisplayBrightnessMapping(null /*AutoBrightnessConfig*/); + } + private void loadAmbientLightSensorFromDdc(DisplayConfiguration config) { final SensorDetails sensorDetails = config.getLightSensor(); if (sensorDetails != null) { @@ -1390,6 +1483,22 @@ public class DisplayDeviceConfig { } } + /** + * Extracts a float array from the specified {@link TypedArray}. + * + * @param array The array to convert. + * @return the given array as a float array. + */ + public static float[] getFloatArray(TypedArray array) { + final int n = array.length(); + float[] vals = new float[n]; + for (int i = 0; i < n; i++) { + vals[i] = array.getFloat(i, PowerManager.BRIGHTNESS_OFF_FLOAT); + } + array.recycle(); + return vals; + } + static class SensorData { public String type; public String name; diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java index 7e36aed81d4a..db0ce2ef6fe2 100644 --- a/services/core/java/com/android/server/notification/ZenModeFiltering.java +++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java @@ -149,8 +149,13 @@ public class ZenModeFiltering { */ public boolean shouldIntercept(int zen, NotificationManager.Policy policy, NotificationRecord record) { - // Zen mode is ignored for critical notifications. - if (zen == ZEN_MODE_OFF || isCritical(record)) { + if (zen == ZEN_MODE_OFF) { + return false; + } + + if (isCritical(record)) { + // Zen mode is ignored for critical notifications. + ZenLog.traceNotIntercepted(record, "criticalNotification"); return false; } // Make an exception to policy for the notification saying that policy has changed @@ -168,6 +173,7 @@ public class ZenModeFiltering { case Global.ZEN_MODE_ALARMS: if (isAlarm(record)) { // Alarms only + ZenLog.traceNotIntercepted(record, "alarm"); return false; } ZenLog.traceIntercepted(record, "alarmsOnly"); @@ -184,6 +190,7 @@ public class ZenModeFiltering { ZenLog.traceIntercepted(record, "!allowAlarms"); return true; } + ZenLog.traceNotIntercepted(record, "allowedAlarm"); return false; } if (isEvent(record)) { @@ -191,6 +198,7 @@ public class ZenModeFiltering { ZenLog.traceIntercepted(record, "!allowEvents"); return true; } + ZenLog.traceNotIntercepted(record, "allowedEvent"); return false; } if (isReminder(record)) { @@ -198,6 +206,7 @@ public class ZenModeFiltering { ZenLog.traceIntercepted(record, "!allowReminders"); return true; } + ZenLog.traceNotIntercepted(record, "allowedReminder"); return false; } if (isMedia(record)) { @@ -205,6 +214,7 @@ public class ZenModeFiltering { ZenLog.traceIntercepted(record, "!allowMedia"); return true; } + ZenLog.traceNotIntercepted(record, "allowedMedia"); return false; } if (isSystem(record)) { @@ -212,6 +222,7 @@ public class ZenModeFiltering { ZenLog.traceIntercepted(record, "!allowSystem"); return true; } + ZenLog.traceNotIntercepted(record, "allowedSystem"); return false; } if (isConversation(record)) { @@ -253,6 +264,7 @@ public class ZenModeFiltering { ZenLog.traceIntercepted(record, "!priority"); return true; default: + ZenLog.traceNotIntercepted(record, "unknownZenMode"); return false; } } @@ -271,10 +283,12 @@ public class ZenModeFiltering { } private static boolean shouldInterceptAudience(int source, NotificationRecord record) { - if (!audienceMatches(source, record.getContactAffinity())) { - ZenLog.traceIntercepted(record, "!audienceMatches"); + float affinity = record.getContactAffinity(); + if (!audienceMatches(source, affinity)) { + ZenLog.traceIntercepted(record, "!audienceMatches,affinity=" + affinity); return true; } + ZenLog.traceNotIntercepted(record, "affinity=" + affinity); return false; } diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index dad9584c6722..5a2fb18673ac 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -571,8 +571,7 @@ public class Notifier { /** * Called when there has been user activity. */ - public void onUserActivity(int displayGroupId, @PowerManager.UserActivityEvent int event, - int uid) { + public void onUserActivity(int displayGroupId, int event, int uid) { if (DEBUG) { Slog.d(TAG, "onUserActivity: event=" + event + ", uid=" + uid); } diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java index 9fe53fbfaf25..fec61ac8f2cf 100644 --- a/services/core/java/com/android/server/power/PowerGroup.java +++ b/services/core/java/com/android/server/power/PowerGroup.java @@ -74,8 +74,6 @@ public class PowerGroup { private long mLastPowerOnTime; private long mLastUserActivityTime; private long mLastUserActivityTimeNoChangeLights; - @PowerManager.UserActivityEvent - private int mLastUserActivityEvent; /** Timestamp (milliseconds since boot) of the last time the power group was awoken.*/ private long mLastWakeTime; /** Timestamp (milliseconds since boot) of the last time the power group was put to sleep. */ @@ -246,7 +244,7 @@ public class PowerGroup { return true; } - boolean dozeLocked(long eventTime, int uid, @PowerManager.GoToSleepReason int reason) { + boolean dozeLocked(long eventTime, int uid, int reason) { if (eventTime < getLastWakeTimeLocked() || !isInteractive(mWakefulness)) { return false; } @@ -255,14 +253,9 @@ public class PowerGroup { try { reason = Math.min(PowerManager.GO_TO_SLEEP_REASON_MAX, Math.max(reason, PowerManager.GO_TO_SLEEP_REASON_MIN)); - long millisSinceLastUserActivity = eventTime - Math.max( - mLastUserActivityTimeNoChangeLights, mLastUserActivityTime); Slog.i(TAG, "Powering off display group due to " - + PowerManager.sleepReasonToString(reason) - + " (groupId= " + getGroupId() + ", uid= " + uid - + ", millisSinceLastUserActivity=" + millisSinceLastUserActivity - + ", lastUserActivityEvent=" + PowerManager.userActivityEventToString( - mLastUserActivityEvent) + ")..."); + + PowerManager.sleepReasonToString(reason) + " (groupId= " + getGroupId() + + ", uid= " + uid + ")..."); setSandmanSummonedLocked(/* isSandmanSummoned= */ true); setWakefulnessLocked(WAKEFULNESS_DOZING, eventTime, uid, reason, /* opUid= */ 0, @@ -273,16 +266,14 @@ public class PowerGroup { return true; } - boolean sleepLocked(long eventTime, int uid, @PowerManager.GoToSleepReason int reason) { + boolean sleepLocked(long eventTime, int uid, int reason) { if (eventTime < mLastWakeTime || getWakefulnessLocked() == WAKEFULNESS_ASLEEP) { return false; } Trace.traceBegin(Trace.TRACE_TAG_POWER, "sleepPowerGroup"); try { - Slog.i(TAG, - "Sleeping power group (groupId=" + getGroupId() + ", uid=" + uid + ", reason=" - + PowerManager.sleepReasonToString(reason) + ")..."); + Slog.i(TAG, "Sleeping power group (groupId=" + getGroupId() + ", uid=" + uid + ")..."); setSandmanSummonedLocked(/* isSandmanSummoned= */ true); setWakefulnessLocked(WAKEFULNESS_ASLEEP, eventTime, uid, reason, /* opUid= */0, /* opPackageName= */ null, /* details= */ null); @@ -296,20 +287,16 @@ public class PowerGroup { return mLastUserActivityTime; } - void setLastUserActivityTimeLocked(long lastUserActivityTime, - @PowerManager.UserActivityEvent int event) { + void setLastUserActivityTimeLocked(long lastUserActivityTime) { mLastUserActivityTime = lastUserActivityTime; - mLastUserActivityEvent = event; } public long getLastUserActivityTimeNoChangeLightsLocked() { return mLastUserActivityTimeNoChangeLights; } - public void setLastUserActivityTimeNoChangeLightsLocked(long time, - @PowerManager.UserActivityEvent int event) { + public void setLastUserActivityTimeNoChangeLightsLocked(long time) { mLastUserActivityTimeNoChangeLights = time; - mLastUserActivityEvent = event; } public int getUserActivitySummaryLocked() { diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index bc93cb30e449..dbf05f1cd7c7 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1169,7 +1169,6 @@ public final class PowerManagerService extends SystemService return; } - Slog.i(TAG, "onFlip(): Face " + (isFaceDown ? "down." : "up.")); mIsFaceDown = isFaceDown; if (isFaceDown) { final long currentTime = mClock.uptimeMillis(); @@ -1889,13 +1888,12 @@ public final class PowerManagerService extends SystemService // Called from native code. @SuppressWarnings("unused") - private void userActivityFromNative(long eventTime, @PowerManager.UserActivityEvent int event, - int displayId, int flags) { + private void userActivityFromNative(long eventTime, int event, int displayId, int flags) { userActivityInternal(displayId, eventTime, event, flags, Process.SYSTEM_UID); } - private void userActivityInternal(int displayId, long eventTime, - @PowerManager.UserActivityEvent int event, int flags, int uid) { + private void userActivityInternal(int displayId, long eventTime, int event, int flags, + int uid) { synchronized (mLock) { if (displayId == Display.INVALID_DISPLAY) { if (userActivityNoUpdateLocked(eventTime, event, flags, uid)) { @@ -1946,12 +1944,11 @@ public final class PowerManagerService extends SystemService @GuardedBy("mLock") private boolean userActivityNoUpdateLocked(final PowerGroup powerGroup, long eventTime, - @PowerManager.UserActivityEvent int event, int flags, int uid) { + int event, int flags, int uid) { final int groupId = powerGroup.getGroupId(); if (DEBUG_SPEW) { Slog.d(TAG, "userActivityNoUpdateLocked: groupId=" + groupId - + ", eventTime=" + eventTime - + ", event=" + PowerManager.userActivityEventToString(event) + + ", eventTime=" + eventTime + ", event=" + event + ", flags=0x" + Integer.toHexString(flags) + ", uid=" + uid); } @@ -1986,7 +1983,7 @@ public final class PowerManagerService extends SystemService if ((flags & PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS) != 0) { if (eventTime > powerGroup.getLastUserActivityTimeNoChangeLightsLocked() && eventTime > powerGroup.getLastUserActivityTimeLocked()) { - powerGroup.setLastUserActivityTimeNoChangeLightsLocked(eventTime, event); + powerGroup.setLastUserActivityTimeNoChangeLightsLocked(eventTime); mDirty |= DIRTY_USER_ACTIVITY; if (event == PowerManager.USER_ACTIVITY_EVENT_BUTTON) { mDirty |= DIRTY_QUIESCENT; @@ -1996,7 +1993,7 @@ public final class PowerManagerService extends SystemService } } else { if (eventTime > powerGroup.getLastUserActivityTimeLocked()) { - powerGroup.setLastUserActivityTimeLocked(eventTime, event); + powerGroup.setLastUserActivityTimeLocked(eventTime); mDirty |= DIRTY_USER_ACTIVITY; if (event == PowerManager.USER_ACTIVITY_EVENT_BUTTON) { mDirty |= DIRTY_QUIESCENT; @@ -2023,8 +2020,7 @@ public final class PowerManagerService extends SystemService @WakeReason int reason, String details, int uid, String opPackageName, int opUid) { if (DEBUG_SPEW) { Slog.d(TAG, "wakePowerGroupLocked: eventTime=" + eventTime - + ", groupId=" + powerGroup.getGroupId() - + ", reason=" + PowerManager.wakeReasonToString(reason) + ", uid=" + uid); + + ", groupId=" + powerGroup.getGroupId() + ", uid=" + uid); } if (mForceSuspendActive || !mSystemReady) { return; @@ -2047,11 +2043,11 @@ public final class PowerManagerService extends SystemService @GuardedBy("mLock") private boolean dozePowerGroupLocked(final PowerGroup powerGroup, long eventTime, - @GoToSleepReason int reason, int uid) { + int reason, int uid) { if (DEBUG_SPEW) { Slog.d(TAG, "dozePowerGroup: eventTime=" + eventTime - + ", groupId=" + powerGroup.getGroupId() - + ", reason=" + PowerManager.sleepReasonToString(reason) + ", uid=" + uid); + + ", groupId=" + powerGroup.getGroupId() + ", reason=" + reason + + ", uid=" + uid); } if (!mSystemReady || !mBootCompleted) { @@ -2062,12 +2058,10 @@ public final class PowerManagerService extends SystemService } @GuardedBy("mLock") - private boolean sleepPowerGroupLocked(final PowerGroup powerGroup, long eventTime, - @GoToSleepReason int reason, int uid) { + private boolean sleepPowerGroupLocked(final PowerGroup powerGroup, long eventTime, int reason, + int uid) { if (DEBUG_SPEW) { - Slog.d(TAG, "sleepPowerGroup: eventTime=" + eventTime - + ", groupId=" + powerGroup.getGroupId() - + ", reason=" + PowerManager.sleepReasonToString(reason) + ", uid=" + uid); + Slog.d(TAG, "sleepPowerGroup: eventTime=" + eventTime + ", uid=" + uid); } if (!mBootCompleted || !mSystemReady) { return false; @@ -2128,10 +2122,7 @@ public final class PowerManagerService extends SystemService case WAKEFULNESS_DOZING: traceMethodName = "goToSleep"; Slog.i(TAG, "Going to sleep due to " + PowerManager.sleepReasonToString(reason) - + " (uid " + uid + ", screenOffTimeout=" + mScreenOffTimeoutSetting - + ", activityTimeoutWM=" + mUserActivityTimeoutOverrideFromWindowManager - + ", maxDimRatio=" + mMaximumScreenDimRatioConfig - + ", maxDimDur=" + mMaximumScreenDimDurationConfig + ")..."); + + " (uid " + uid + ")..."); mLastGlobalSleepTime = eventTime; mLastGlobalSleepReason = reason; @@ -4216,7 +4207,7 @@ public final class PowerManagerService extends SystemService void onUserActivity() { synchronized (mLock) { mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP).setLastUserActivityTimeLocked( - mClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_OTHER); + mClock.uptimeMillis()); } } @@ -5599,8 +5590,7 @@ public final class PowerManagerService extends SystemService } @Override // Binder call - public void userActivity(int displayId, long eventTime, - @PowerManager.UserActivityEvent int event, int flags) { + public void userActivity(int displayId, long eventTime, int event, int flags) { final long now = mClock.uptimeMillis(); if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER) != PackageManager.PERMISSION_GRANTED diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 635cf0e61685..a174c54eae98 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -313,7 +313,6 @@ import android.util.TypedXmlPullParser; import android.util.TypedXmlSerializer; import android.util.proto.ProtoOutputStream; import android.view.AppTransitionAnimationSpec; -import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.IAppTransitionAnimationSpecsFuture; import android.view.InputApplicationHandle; @@ -357,6 +356,7 @@ import com.android.server.wm.ActivityMetricsLogger.TransitionInfoSnapshot; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.WindowManagerService.H; import com.android.server.wm.utils.InsetUtils; +import com.android.server.wm.utils.WmDisplayCutout; import dalvik.annotation.optimization.NeverCompile; @@ -9694,9 +9694,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270); final int dw = rotated ? display.mBaseDisplayHeight : display.mBaseDisplayWidth; final int dh = rotated ? display.mBaseDisplayWidth : display.mBaseDisplayHeight; - final DisplayCutout cutout = display.calculateDisplayCutoutForRotation(rotation) - .getDisplayCutout(); - policy.getNonDecorInsetsLw(rotation, cutout, mNonDecorInsets[rotation]); + final WmDisplayCutout cutout = display.calculateDisplayCutoutForRotation(rotation); + policy.getNonDecorInsetsLw(rotation, dw, dh, cutout, mNonDecorInsets[rotation]); mStableInsets[rotation].set(mNonDecorInsets[rotation]); policy.convertNonDecorInsetsToStableInsets(mStableInsets[rotation], rotation); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index d4bbc86c4850..31f87416db67 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -226,6 +226,7 @@ import android.view.IWindowFocusObserver; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; import android.view.WindowManager; +import android.window.BackAnimationAdaptor; import android.window.BackNavigationInfo; import android.window.IWindowOrganizerController; import android.window.SplashScreenView.SplashScreenViewParcelable; @@ -457,7 +458,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { private final ClientLifecycleManager mLifecycleManager; @Nullable - private final BackNavigationController mBackNavigationController; + final BackNavigationController mBackNavigationController; private TaskChangeNotificationController mTaskChangeNotificationController; /** The controller for all operations related to locktask. */ @@ -1836,13 +1837,14 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @Override public BackNavigationInfo startBackNavigation(boolean requestAnimation, - IWindowFocusObserver observer) { + IWindowFocusObserver observer, BackAnimationAdaptor backAnimationAdaptor) { mAmInternal.enforceCallingPermission(START_TASKS_FROM_RECENTS, "startBackNavigation()"); if (mBackNavigationController == null) { return null; } - return mBackNavigationController.startBackNavigation(requestAnimation, observer); + return mBackNavigationController.startBackNavigation( + requestAnimation, observer, backAnimationAdaptor); } /** diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index 9a94a4f54b61..d3452277a29f 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -22,6 +22,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE; import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Trace; import android.util.ArraySet; import android.util.Slog; @@ -63,6 +64,15 @@ import java.util.ArrayList; class BLASTSyncEngine { private static final String TAG = "BLASTSyncEngine"; + /** No specific method. Used by override specifiers. */ + public static final int METHOD_UNDEFINED = -1; + + /** No sync method. Apps will draw/present internally and just report. */ + public static final int METHOD_NONE = 0; + + /** Sync with BLAST. Apps will draw and then send the buffer to be applied in sync. */ + public static final int METHOD_BLAST = 1; + interface TransactionReadyListener { void onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction); } @@ -85,6 +95,7 @@ class BLASTSyncEngine { */ class SyncGroup { final int mSyncId; + final int mSyncMethod; final TransactionReadyListener mListener; final Runnable mOnTimeout; boolean mReady = false; @@ -92,8 +103,9 @@ class BLASTSyncEngine { private SurfaceControl.Transaction mOrphanTransaction = null; private String mTraceName; - private SyncGroup(TransactionReadyListener listener, int id, String name) { + private SyncGroup(TransactionReadyListener listener, int id, String name, int method) { mSyncId = id; + mSyncMethod = method; mListener = listener; mOnTimeout = () -> { Slog.w(TAG, "Sync group " + mSyncId + " timeout"); @@ -271,16 +283,13 @@ class BLASTSyncEngine { * Prepares a {@link SyncGroup} that is not active yet. Caller must call {@link #startSyncSet} * before calling {@link #addToSyncSet(int, WindowContainer)} on any {@link WindowContainer}. */ - SyncGroup prepareSyncSet(TransactionReadyListener listener, String name) { - return new SyncGroup(listener, mNextSyncId++, name); + SyncGroup prepareSyncSet(TransactionReadyListener listener, String name, int method) { + return new SyncGroup(listener, mNextSyncId++, name, method); } - int startSyncSet(TransactionReadyListener listener) { - return startSyncSet(listener, BLAST_TIMEOUT_DURATION, ""); - } - - int startSyncSet(TransactionReadyListener listener, long timeoutMs, String name) { - final SyncGroup s = prepareSyncSet(listener, name); + int startSyncSet(TransactionReadyListener listener, long timeoutMs, String name, + int method) { + final SyncGroup s = prepareSyncSet(listener, name, method); startSyncSet(s, timeoutMs); return s.mSyncId; } @@ -302,6 +311,11 @@ class BLASTSyncEngine { scheduleTimeout(s, timeoutMs); } + @Nullable + SyncGroup getSyncSet(int id) { + return mActiveSyncs.get(id); + } + boolean hasActiveSync() { return mActiveSyncs.size() != 0; } diff --git a/services/core/java/com/android/server/wm/BackNaviAnimationController.java b/services/core/java/com/android/server/wm/BackNaviAnimationController.java new file mode 100644 index 000000000000..ecc7534ad386 --- /dev/null +++ b/services/core/java/com/android/server/wm/BackNaviAnimationController.java @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; + +import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; +import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; + +import android.annotation.NonNull; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.WindowInsets; +import android.window.BackNavigationInfo; +import android.window.IBackAnimationRunner; +import android.window.IBackNaviAnimationController; + +import com.android.server.wm.utils.InsetUtils; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Controls the back navigation animation. + * This is throw-away code and should only be used for Android T, most code is duplicated from + * RecentsAnimationController which should be stable to handle animation leash resources/flicker/ + * fixed rotation, etc. Remove this class at U and migrate to shell transition. + */ +public class BackNaviAnimationController implements IBinder.DeathRecipient { + private static final String TAG = BackNavigationController.TAG; + // Constant for a yet-to-be-calculated {@link RemoteAnimationTarget#Mode} state + private static final int MODE_UNKNOWN = -1; + + // The activity which host this animation + private ActivityRecord mTargetActivityRecord; + // The original top activity + private ActivityRecord mTopActivity; + + private final DisplayContent mDisplayContent; + private final WindowManagerService mWindowManagerService; + private final BackNavigationController mBackNavigationController; + + // We start the BackAnimationController in a pending-start state since we need to wait for + // the wallpaper/activity to draw before we can give control to the handler to start animating + // the visible task surfaces + private boolean mPendingStart; + private IBackAnimationRunner mRunner; + final IBackNaviAnimationController mRemoteController; + private boolean mLinkedToDeathOfRunner; + + private final ArrayList<TaskAnimationAdapter> mPendingAnimations = new ArrayList<>(); + + BackNaviAnimationController(IBackAnimationRunner runner, + BackNavigationController backNavigationController, int displayId) { + mRunner = runner; + mBackNavigationController = backNavigationController; + mWindowManagerService = mBackNavigationController.mWindowManagerService; + mDisplayContent = mWindowManagerService.mRoot.getDisplayContent(displayId); + + mRemoteController = new IBackNaviAnimationController.Stub() { + @Override + public void finish(boolean triggerBack) { + synchronized (mWindowManagerService.getWindowManagerLock()) { + final long token = Binder.clearCallingIdentity(); + try { + mWindowManagerService.inSurfaceTransaction(() -> { + mWindowManagerService.mAtmService.deferWindowLayout(); + try { + if (triggerBack) { + mDisplayContent.mFixedRotationTransitionListener + .notifyRecentsWillBeTop(); + if (mTopActivity != null) { + mWindowManagerService.mTaskSnapshotController + .recordTaskSnapshot(mTopActivity.getTask(), false); + // TODO consume moveTaskToBack? + mTopActivity.commitVisibility(false, false, true); + } + } else { + mTargetActivityRecord.mTaskSupervisor + .scheduleLaunchTaskBehindComplete( + mTargetActivityRecord.token); + } + cleanupAnimation(); + } finally { + mWindowManagerService.mAtmService.continueWindowLayout(); + } + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + }; + } + + /** + * @param targetActivity The home or opening activity which should host the wallpaper + * @param topActivity The current top activity before animation start. + */ + void initialize(ActivityRecord targetActivity, ActivityRecord topActivity) { + mTargetActivityRecord = targetActivity; + mTopActivity = topActivity; + final Task topTask = mTopActivity.getTask(); + + createAnimationAdapter(topTask, (type, anim) -> topTask.forAllWindows( + win -> { + win.onAnimationFinished(type, anim); + }, true)); + final Task homeTask = mTargetActivityRecord.getRootTask(); + createAnimationAdapter(homeTask, (type, anim) -> homeTask.forAllWindows( + win -> { + win.onAnimationFinished(type, anim); + }, true)); + try { + linkToDeathOfRunner(); + } catch (RemoteException e) { + cancelAnimation(); + return; + } + + if (targetActivity.windowsCanBeWallpaperTarget()) { + mDisplayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; + mDisplayContent.setLayoutNeeded(); + } + + mWindowManagerService.mWindowPlacerLocked.performSurfacePlacement(); + + mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(targetActivity); + mPendingStart = true; + } + + void cleanupAnimation() { + for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { + final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i); + + removeAnimationAdapter(taskAdapter); + taskAdapter.onCleanup(); + } + mTargetActivityRecord.mLaunchTaskBehind = false; + // Clear references to the runner + unlinkToDeathOfRunner(); + mRunner = null; + + // Update the input windows after the animation is complete + final InputMonitor inputMonitor = mDisplayContent.getInputMonitor(); + inputMonitor.updateInputWindowsLw(true /*force*/); + + mDisplayContent.mFixedRotationTransitionListener.onFinishRecentsAnimation(); + mBackNavigationController.finishAnimation(); + } + + void removeAnimationAdapter(TaskAnimationAdapter taskAdapter) { + taskAdapter.onRemove(); + mPendingAnimations.remove(taskAdapter); + } + + void checkAnimationReady(WallpaperController wallpaperController) { + if (mPendingStart) { + final boolean wallpaperReady = !isTargetOverWallpaper() + || (wallpaperController.getWallpaperTarget() != null + && wallpaperController.wallpaperTransitionReady()); + if (wallpaperReady) { + startAnimation(); + } + } + } + + boolean isWallpaperVisible(WindowState w) { + return w != null && w.mAttrs.type == TYPE_BASE_APPLICATION + && ((w.mActivityRecord != null && mTargetActivityRecord == w.mActivityRecord) + || isAnimatingTask(w.getTask())) + && isTargetOverWallpaper() && w.isOnScreen(); + } + + boolean isAnimatingTask(Task task) { + for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { + if (task == mPendingAnimations.get(i).mTask) { + return true; + } + } + return false; + } + + void linkFixedRotationTransformIfNeeded(@NonNull WindowToken wallpaper) { + if (mTargetActivityRecord == null) { + return; + } + wallpaper.linkFixedRotationTransform(mTargetActivityRecord); + } + + private void linkToDeathOfRunner() throws RemoteException { + if (!mLinkedToDeathOfRunner) { + mRunner.asBinder().linkToDeath(this, 0); + mLinkedToDeathOfRunner = true; + } + } + + private void unlinkToDeathOfRunner() { + if (mLinkedToDeathOfRunner) { + mRunner.asBinder().unlinkToDeath(this, 0); + mLinkedToDeathOfRunner = false; + } + } + + void startAnimation() { + if (!mPendingStart) { + // Skip starting if we've already started or canceled the animation + return; + } + // Create the app targets + final RemoteAnimationTarget[] appTargets = createAppAnimations(); + + // Skip the animation if there is nothing to animate + if (appTargets.length == 0) { + cancelAnimation(); + return; + } + + mPendingStart = false; + + try { + mRunner.onAnimationStart(mRemoteController, BackNavigationInfo.TYPE_RETURN_TO_HOME, + appTargets, null /* wallpapers */, null /*nonApps*/); + } catch (RemoteException e) { + cancelAnimation(); + } + } + + @Override + public void binderDied() { + cancelAnimation(); + } + + TaskAnimationAdapter createAnimationAdapter(Task task, + SurfaceAnimator.OnAnimationFinishedCallback finishedCallback) { + final TaskAnimationAdapter taskAdapter = new TaskAnimationAdapter(task, + mTargetActivityRecord, this::cancelAnimation); + // borrow from recents since we cannot start back animation if recents is playing + task.startAnimation(task.getPendingTransaction(), taskAdapter, false /* hidden */, + ANIMATION_TYPE_RECENTS, finishedCallback); + task.commitPendingTransaction(); + mPendingAnimations.add(taskAdapter); + return taskAdapter; + } + + private RemoteAnimationTarget[] createAppAnimations() { + final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>(); + for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { + final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i); + final RemoteAnimationTarget target = + taskAdapter.createRemoteAnimationTarget(INVALID_TASK_ID, MODE_UNKNOWN); + if (target != null) { + targets.add(target); + } else { + removeAnimationAdapter(taskAdapter); + } + } + return targets.toArray(new RemoteAnimationTarget[targets.size()]); + } + + private void cancelAnimation() { + synchronized (mWindowManagerService.getWindowManagerLock()) { + // Notify the runner and clean up the animation immediately + // Note: In the fallback case, this can trigger multiple onAnimationCancel() calls + // to the runner if we this actually triggers cancel twice on the caller + try { + mRunner.onAnimationCancelled(); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to cancel recents animation", e); + } + cleanupAnimation(); + } + } + + private boolean isTargetOverWallpaper() { + if (mTargetActivityRecord == null) { + return false; + } + return mTargetActivityRecord.windowsCanBeWallpaperTarget(); + } + + private static class TaskAnimationAdapter implements AnimationAdapter { + private final Task mTask; + private SurfaceControl mCapturedLeash; + private SurfaceAnimator.OnAnimationFinishedCallback mCapturedFinishCallback; + @SurfaceAnimator.AnimationType private int mLastAnimationType; + private RemoteAnimationTarget mTarget; + private final ActivityRecord mTargetActivityRecord; + private final Runnable mCancelCallback; + + private final Rect mBounds = new Rect(); + // The bounds of the target relative to its parent. + private final Rect mLocalBounds = new Rect(); + + TaskAnimationAdapter(Task task, ActivityRecord target, Runnable cancelCallback) { + mTask = task; + mBounds.set(mTask.getBounds()); + + mLocalBounds.set(mBounds); + Point tmpPos = new Point(); + mTask.getRelativePosition(tmpPos); + mLocalBounds.offsetTo(tmpPos.x, tmpPos.y); + mTargetActivityRecord = target; + mCancelCallback = cancelCallback; + } + + // Keep overrideTaskId and overrideMode now, if we need to add other type of back animation + // on legacy transition system then they can be useful. + RemoteAnimationTarget createRemoteAnimationTarget(int overrideTaskId, int overrideMode) { + ActivityRecord topApp = mTask.getTopRealVisibleActivity(); + if (topApp == null) { + topApp = mTask.getTopVisibleActivity(); + } + final WindowState mainWindow = topApp != null + ? topApp.findMainWindow() + : null; + if (mainWindow == null) { + return null; + } + final Rect insets = + mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets( + mBounds, WindowInsets.Type.systemBars(), + false /* ignoreVisibility */).toRect(); + InsetUtils.addInsets(insets, mainWindow.mActivityRecord.getLetterboxInsets()); + final int mode = overrideMode != MODE_UNKNOWN + ? overrideMode + : topApp.getActivityType() == mTargetActivityRecord.getActivityType() + ? MODE_OPENING + : MODE_CLOSING; + if (overrideTaskId < 0) { + overrideTaskId = mTask.mTaskId; + } + mTarget = new RemoteAnimationTarget(overrideTaskId, mode, mCapturedLeash, + !topApp.fillsParent(), new Rect(), + insets, mTask.getPrefixOrderIndex(), new Point(mBounds.left, mBounds.top), + mLocalBounds, mBounds, mTask.getWindowConfiguration(), + true /* isNotInRecents */, null, null, mTask.getTaskInfo(), + topApp.checkEnterPictureInPictureAppOpsState()); + return mTarget; + } + @Override + public boolean getShowWallpaper() { + return false; + } + @Override + public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t, + @SurfaceAnimator.AnimationType int type, + @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) { + t.setPosition(animationLeash, mLocalBounds.left, mLocalBounds.top); + final Rect tmpRect = new Rect(); + tmpRect.set(mLocalBounds); + tmpRect.offsetTo(0, 0); + t.setWindowCrop(animationLeash, tmpRect); + mCapturedLeash = animationLeash; + mCapturedFinishCallback = finishCallback; + mLastAnimationType = type; + } + + @Override + public void onAnimationCancelled(SurfaceControl animationLeash) { + mCancelCallback.run(); + } + + void onRemove() { + mCapturedFinishCallback.onAnimationFinished(mLastAnimationType, this); + } + + void onCleanup() { + final SurfaceControl.Transaction pendingTransaction = mTask.getPendingTransaction(); + if (!mTask.isAttached()) { + // Apply the task's pending transaction in case it is detached and its transaction + // is not reachable. + pendingTransaction.apply(); + } + } + + @Override + public long getDurationHint() { + return 0; + } + + @Override + public long getStatusBarTransitionsStartTime() { + return SystemClock.uptimeMillis(); + } + + @Override + public void dump(PrintWriter pw, String prefix) { } + + @Override + public void dumpDebug(ProtoOutputStream proto) { } + } +} diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index d9ab971c9a78..35a39c048e57 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -34,6 +34,7 @@ import android.util.Slog; import android.view.IWindowFocusObserver; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.window.BackAnimationAdaptor; import android.window.BackNavigationInfo; import android.window.OnBackInvokedCallbackInfo; import android.window.TaskSnapshot; @@ -46,9 +47,15 @@ import com.android.server.LocalServices; * Controller to handle actions related to the back gesture on the server side. */ class BackNavigationController { - private static final String TAG = "BackNavigationController"; - private WindowManagerService mWindowManagerService; + static final String TAG = "BackNavigationController"; + WindowManagerService mWindowManagerService; private IWindowFocusObserver mFocusObserver; + // TODO (b/241808055) Find a appropriate time to remove during refactor + // Execute back animation with legacy transition system. Temporary flag for easier debugging. + static final boolean USE_TRANSITION = + SystemProperties.getInt("persist.wm.debug.predictive_back_ani_trans", 1) != 0; + + BackNaviAnimationController mBackNaviAnimationController; /** * Returns true if the back predictability feature is enabled @@ -72,7 +79,7 @@ class BackNavigationController { @VisibleForTesting @Nullable BackNavigationInfo startBackNavigation(boolean requestAnimation, - IWindowFocusObserver observer) { + IWindowFocusObserver observer, BackAnimationAdaptor backAnimationAdaptor) { final WindowManagerService wmService = mWindowManagerService; final SurfaceControl.Transaction tx = wmService.mTransactionFactory.get(); mFocusObserver = observer; @@ -259,6 +266,8 @@ class BackNavigationController { && requestAnimation // Only create a new leash if no leash has been created. // Otherwise return null for animation target to avoid conflict. + // TODO isAnimating, recents can cancel app transition animation, can't back + // cancel like recents? && !removedWindowContainer.hasCommittedReparentToAnimationLeash(); if (prepareAnimation) { @@ -266,19 +275,21 @@ class BackNavigationController { currentTask.getTaskInfo().configuration.windowConfiguration; infoBuilder.setTaskWindowConfiguration(taskWindowConfiguration); - // Prepare a leash to animate the current top window - // TODO(b/220934562): Use surface animator to better manage animation conflicts. - SurfaceControl animLeash = removedWindowContainer.makeAnimationLeash() - .setName("BackPreview Leash for " + removedWindowContainer) - .setHidden(false) - .setBLASTLayer() - .build(); - removedWindowContainer.reparentSurfaceControl(tx, animLeash); - animationLeashParent = removedWindowContainer.getAnimationLeashParent(); - topAppTarget = createRemoteAnimationTargetLocked(removedWindowContainer, - currentActivity, - currentTask, animLeash); - infoBuilder.setDepartingAnimationTarget(topAppTarget); + if (!USE_TRANSITION) { + // Prepare a leash to animate the current top window + // TODO(b/220934562): Use surface animator to better manage animation conflicts. + SurfaceControl animLeash = removedWindowContainer.makeAnimationLeash() + .setName("BackPreview Leash for " + removedWindowContainer) + .setHidden(false) + .setBLASTLayer() + .build(); + removedWindowContainer.reparentSurfaceControl(tx, animLeash); + animationLeashParent = removedWindowContainer.getAnimationLeashParent(); + topAppTarget = createRemoteAnimationTargetLocked(removedWindowContainer, + currentActivity, + currentTask, animLeash); + infoBuilder.setDepartingAnimationTarget(topAppTarget); + } } //TODO(207481538) Remove once the infrastructure to support per-activity screenshot is @@ -293,21 +304,32 @@ class BackNavigationController { // Special handling for back to home animation if (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME && prepareAnimation && prevTask != null) { - currentTask.mBackGestureStarted = true; - // Make launcher show from behind by marking its top activity as visible and - // launch-behind to bump its visibility for the duration of the back gesture. - prevActivity = prevTask.getTopNonFinishingActivity(); - if (prevActivity != null) { - if (!prevActivity.mVisibleRequested) { - prevActivity.setVisibility(true); + if (USE_TRANSITION && mBackNaviAnimationController == null) { + if (backAnimationAdaptor != null + && backAnimationAdaptor.getSupportType() == backType) { + mBackNaviAnimationController = new BackNaviAnimationController( + backAnimationAdaptor.getRunner(), this, + currentActivity.getDisplayId()); + prepareBackToHomeTransition(currentTask, prevTask); + infoBuilder.setPrepareAnimation(true); + } + } else { + currentTask.mBackGestureStarted = true; + // Make launcher show from behind by marking its top activity as visible and + // launch-behind to bump its visibility for the duration of the back gesture. + prevActivity = prevTask.getTopNonFinishingActivity(); + if (prevActivity != null) { + if (!prevActivity.mVisibleRequested) { + prevActivity.setVisibility(true); + } + prevActivity.mLaunchTaskBehind = true; + ProtoLog.d(WM_DEBUG_BACK_PREVIEW, + "Setting Activity.mLauncherTaskBehind to true. Activity=%s", + prevActivity); + prevActivity.mRootWindowContainer.ensureActivitiesVisible( + null /* starting */, 0 /* configChanges */, + false /* preserveWindows */); } - prevActivity.mLaunchTaskBehind = true; - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, - "Setting Activity.mLauncherTaskBehind to true. Activity=%s", - prevActivity); - prevActivity.mRootWindowContainer.ensureActivitiesVisible( - null /* starting */, 0 /* configChanges */, - false /* preserveWindows */); } } } // Release wm Lock @@ -388,29 +410,30 @@ class BackNavigationController { BackNavigationInfo.KEY_TRIGGER_BACK); ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onBackNavigationDone backType=%s, " + "task=%s, prevActivity=%s", backType, task, prevActivity); - - if (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME && prepareAnimation) { - if (triggerBack) { - if (surfaceControl != null && surfaceControl.isValid()) { - // When going back to home, hide the task surface before it is re-parented to - // avoid flicker. - SurfaceControl.Transaction t = windowContainer.getSyncTransaction(); - t.hide(surfaceControl); - t.apply(); + if (!USE_TRANSITION) { + if (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME && prepareAnimation) { + if (triggerBack) { + if (surfaceControl != null && surfaceControl.isValid()) { + // When going back to home, hide the task surface before it is re-parented + // to avoid flicker. + SurfaceControl.Transaction t = windowContainer.getSyncTransaction(); + t.hide(surfaceControl); + t.apply(); + } } + if (prevActivity != null && !triggerBack) { + // Restore the launch-behind state. + task.mTaskSupervisor.scheduleLaunchTaskBehindComplete(prevActivity.token); + prevActivity.mLaunchTaskBehind = false; + ProtoLog.d(WM_DEBUG_BACK_PREVIEW, + "Setting Activity.mLauncherTaskBehind to false. Activity=%s", + prevActivity); + } + } else { + task.mBackGestureStarted = false; } - if (prevActivity != null && !triggerBack) { - // Restore the launch-behind state. - task.mTaskSupervisor.scheduleLaunchTaskBehindComplete(prevActivity.token); - prevActivity.mLaunchTaskBehind = false; - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, - "Setting Activity.mLauncherTaskBehind to false. Activity=%s", - prevActivity); - } - } else if (task != null) { - task.mBackGestureStarted = false; + resetSurfaces(windowContainer); } - resetSurfaces(windowContainer); if (mFocusObserver != null) { focusedWindow.unregisterFocusObserver(mFocusObserver); @@ -465,4 +488,21 @@ class BackNavigationController { void setWindowManager(WindowManagerService wm) { mWindowManagerService = wm; } + + private void prepareBackToHomeTransition(Task currentTask, Task homeTask) { + final DisplayContent dc = currentTask.getDisplayContent(); + final ActivityRecord homeActivity = homeTask.getTopNonFinishingActivity(); + if (!homeActivity.mVisibleRequested) { + homeActivity.setVisibility(true); + } + homeActivity.mLaunchTaskBehind = true; + dc.ensureActivitiesVisible( + null /* starting */, 0 /* configChanges */, + false /* preserveWindows */, true); + mBackNaviAnimationController.initialize(homeActivity, currentTask.getTopMostActivity()); + } + + void finishAnimation() { + mBackNaviAnimationController = null; + } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index b6452543a988..0376974900e3 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -438,7 +438,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ final DisplayMetrics mRealDisplayMetrics = new DisplayMetrics(); - /** @see #computeCompatSmallestWidth(boolean, int, int, int) */ + /** @see #computeCompatSmallestWidth(boolean, int, int) */ private final DisplayMetrics mTmpDisplayMetrics = new DisplayMetrics(); /** @@ -2014,7 +2014,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // the top of the method, the caller is obligated to call computeNewConfigurationLocked(). // By updating the Display info here it will be available to // #computeScreenConfiguration() later. - updateDisplayAndOrientation(getConfiguration().uiMode, null /* outConfig */); + updateDisplayAndOrientation(null /* outConfig */); // NOTE: We disable the rotation in the emulator because // it doesn't support hardware OpenGL emulation yet. @@ -2064,7 +2064,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * changed. * Do not call if {@link WindowManagerService#mDisplayReady} == false. */ - private DisplayInfo updateDisplayAndOrientation(int uiMode, Configuration outConfig) { + private DisplayInfo updateDisplayAndOrientation(Configuration outConfig) { // Use the effective "visual" dimensions based on current rotation final int rotation = getRotation(); final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270); @@ -2076,18 +2076,16 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final DisplayCutout displayCutout = wmDisplayCutout.getDisplayCutout(); final RoundedCorners roundedCorners = calculateRoundedCornersForRotation(rotation); - final int appWidth = mDisplayPolicy.getNonDecorDisplayWidth(dw, dh, rotation, uiMode, - displayCutout); - final int appHeight = mDisplayPolicy.getNonDecorDisplayHeight(dh, rotation, - displayCutout); + final Rect appFrame = mDisplayPolicy.getNonDecorDisplayFrame(dw, dh, rotation, + wmDisplayCutout); mDisplayInfo.rotation = rotation; mDisplayInfo.logicalWidth = dw; mDisplayInfo.logicalHeight = dh; mDisplayInfo.logicalDensityDpi = mBaseDisplayDensity; mDisplayInfo.physicalXDpi = mBaseDisplayPhysicalXDpi; mDisplayInfo.physicalYDpi = mBaseDisplayPhysicalYDpi; - mDisplayInfo.appWidth = appWidth; - mDisplayInfo.appHeight = appHeight; + mDisplayInfo.appWidth = appFrame.width(); + mDisplayInfo.appHeight = appFrame.height(); if (isDefaultDisplay) { mDisplayInfo.getLogicalMetrics(mRealDisplayMetrics, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); @@ -2101,7 +2099,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplayInfo.flags &= ~Display.FLAG_SCALING_DISABLED; } - computeSizeRangesAndScreenLayout(mDisplayInfo, rotated, uiMode, dw, dh, + computeSizeRangesAndScreenLayout(mDisplayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig); mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId, @@ -2191,10 +2189,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp outConfig.windowConfiguration.setMaxBounds(0, 0, dw, dh); outConfig.windowConfiguration.setBounds(outConfig.windowConfiguration.getMaxBounds()); - final int uiMode = getConfiguration().uiMode; - final DisplayCutout displayCutout = - calculateDisplayCutoutForRotation(rotation).getDisplayCutout(); - computeScreenAppConfiguration(outConfig, dw, dh, rotation, uiMode, displayCutout); + final WmDisplayCutout wmDisplayCutout = calculateDisplayCutoutForRotation(rotation); + computeScreenAppConfiguration(outConfig, dw, dh, rotation, wmDisplayCutout); final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); displayInfo.rotation = rotation; @@ -2203,38 +2199,35 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final Rect appBounds = outConfig.windowConfiguration.getAppBounds(); displayInfo.appWidth = appBounds.width(); displayInfo.appHeight = appBounds.height(); + final DisplayCutout displayCutout = wmDisplayCutout.getDisplayCutout(); displayInfo.displayCutout = displayCutout.isEmpty() ? null : displayCutout; - computeSizeRangesAndScreenLayout(displayInfo, rotated, uiMode, dw, dh, + computeSizeRangesAndScreenLayout(displayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig); return displayInfo; } /** Compute configuration related to application without changing current display. */ private void computeScreenAppConfiguration(Configuration outConfig, int dw, int dh, - int rotation, int uiMode, DisplayCutout displayCutout) { - final int appWidth = mDisplayPolicy.getNonDecorDisplayWidth(dw, dh, rotation, uiMode, - displayCutout); - final int appHeight = mDisplayPolicy.getNonDecorDisplayHeight(dh, rotation, - displayCutout); - mDisplayPolicy.getNonDecorInsetsLw(rotation, displayCutout, mTmpRect); - final int leftInset = mTmpRect.left; - final int topInset = mTmpRect.top; + int rotation, WmDisplayCutout wmDisplayCutout) { + final DisplayFrames displayFrames = + mDisplayPolicy.getSimulatedDisplayFrames(rotation, dw, dh, wmDisplayCutout); + final Rect appFrame = + mDisplayPolicy.getNonDecorDisplayFrameWithSimulatedFrame(displayFrames); // AppBounds at the root level should mirror the app screen size. - outConfig.windowConfiguration.setAppBounds(leftInset /* left */, topInset /* top */, - leftInset + appWidth /* right */, topInset + appHeight /* bottom */); + outConfig.windowConfiguration.setAppBounds(appFrame); outConfig.windowConfiguration.setRotation(rotation); outConfig.orientation = (dw <= dh) ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE; final float density = mDisplayMetrics.density; - outConfig.screenWidthDp = (int) (mDisplayPolicy.getConfigDisplayWidth(dw, dh, rotation, - uiMode, displayCutout) / density + 0.5f); - outConfig.screenHeightDp = (int) (mDisplayPolicy.getConfigDisplayHeight(dw, dh, rotation, - uiMode, displayCutout) / density + 0.5f); + final Point configSize = + mDisplayPolicy.getConfigDisplaySizeWithSimulatedFrame(displayFrames); + outConfig.screenWidthDp = (int) (configSize.x / density + 0.5f); + outConfig.screenHeightDp = (int) (configSize.y / density + 0.5f); outConfig.compatScreenWidthDp = (int) (outConfig.screenWidthDp / mCompatibleScreenScale); outConfig.compatScreenHeightDp = (int) (outConfig.screenHeightDp / mCompatibleScreenScale); final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270); - outConfig.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, uiMode, dw, dh); + outConfig.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, dw, dh); outConfig.windowConfiguration.setDisplayRotation(rotation); } @@ -2243,7 +2236,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * Do not call if mDisplayReady == false. */ void computeScreenConfiguration(Configuration config) { - final DisplayInfo displayInfo = updateDisplayAndOrientation(config.uiMode, config); + final DisplayInfo displayInfo = updateDisplayAndOrientation(config); final int dw = displayInfo.logicalWidth; final int dh = displayInfo.logicalHeight; mTmpRect.set(0, 0, dw, dh); @@ -2252,8 +2245,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp config.windowConfiguration.setWindowingMode(getWindowingMode()); config.windowConfiguration.setDisplayWindowingMode(getWindowingMode()); - computeScreenAppConfiguration(config, dw, dh, displayInfo.rotation, config.uiMode, - displayInfo.displayCutout); + computeScreenAppConfiguration(config, dw, dh, displayInfo.rotation, + calculateDisplayCutoutForRotation(getRotation())); config.screenLayout = (config.screenLayout & ~Configuration.SCREENLAYOUT_ROUND_MASK) | ((displayInfo.flags & Display.FLAG_ROUND) != 0 @@ -2342,7 +2335,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mWmService.mPolicy.adjustConfigurationLw(config, keyboardPresence, navigationPresence); } - private int computeCompatSmallestWidth(boolean rotated, int uiMode, int dw, int dh) { + private int computeCompatSmallestWidth(boolean rotated, int dw, int dh) { mTmpDisplayMetrics.setTo(mDisplayMetrics); final DisplayMetrics tmpDm = mTmpDisplayMetrics; final int unrotDw, unrotDh; @@ -2353,25 +2346,20 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp unrotDw = dw; unrotDh = dh; } - int sw = reduceCompatConfigWidthSize(0, Surface.ROTATION_0, uiMode, tmpDm, unrotDw, - unrotDh); - sw = reduceCompatConfigWidthSize(sw, Surface.ROTATION_90, uiMode, tmpDm, unrotDh, - unrotDw); - sw = reduceCompatConfigWidthSize(sw, Surface.ROTATION_180, uiMode, tmpDm, unrotDw, - unrotDh); - sw = reduceCompatConfigWidthSize(sw, Surface.ROTATION_270, uiMode, tmpDm, unrotDh, - unrotDw); + int sw = reduceCompatConfigWidthSize(0, Surface.ROTATION_0, tmpDm, unrotDw, unrotDh); + sw = reduceCompatConfigWidthSize(sw, Surface.ROTATION_90, tmpDm, unrotDh, unrotDw); + sw = reduceCompatConfigWidthSize(sw, Surface.ROTATION_180, tmpDm, unrotDw, unrotDh); + sw = reduceCompatConfigWidthSize(sw, Surface.ROTATION_270, tmpDm, unrotDh, unrotDw); return sw; } - private int reduceCompatConfigWidthSize(int curSize, int rotation, int uiMode, + private int reduceCompatConfigWidthSize(int curSize, int rotation, DisplayMetrics dm, int dw, int dh) { - final DisplayCutout displayCutout = calculateDisplayCutoutForRotation( - rotation).getDisplayCutout(); - dm.noncompatWidthPixels = mDisplayPolicy.getNonDecorDisplayWidth(dw, dh, rotation, uiMode, - displayCutout); - dm.noncompatHeightPixels = mDisplayPolicy.getNonDecorDisplayHeight(dh, rotation, - displayCutout); + final WmDisplayCutout wmDisplayCutout = calculateDisplayCutoutForRotation(rotation); + final Rect nonDecorSize = mDisplayPolicy.getNonDecorDisplayFrame(dw, dh, rotation, + wmDisplayCutout); + dm.noncompatWidthPixels = nonDecorSize.width(); + dm.noncompatHeightPixels = nonDecorSize.height(); float scale = CompatibilityInfo.computeCompatibleScaling(dm, null); int size = (int)(((dm.noncompatWidthPixels / scale) / dm.density) + .5f); if (curSize == 0 || size < curSize) { @@ -2381,7 +2369,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } private void computeSizeRangesAndScreenLayout(DisplayInfo displayInfo, boolean rotated, - int uiMode, int dw, int dh, float density, Configuration outConfig) { + int dw, int dh, float density, Configuration outConfig) { // We need to determine the smallest width that will occur under normal // operation. To this, start with the base screen size and compute the @@ -2399,37 +2387,34 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp displayInfo.smallestNominalAppHeight = 1<<30; displayInfo.largestNominalAppWidth = 0; displayInfo.largestNominalAppHeight = 0; - adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_0, uiMode, unrotDw, unrotDh); - adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_90, uiMode, unrotDh, unrotDw); - adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_180, uiMode, unrotDw, unrotDh); - adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_270, uiMode, unrotDh, unrotDw); + adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_0, unrotDw, unrotDh); + adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_90, unrotDh, unrotDw); + adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_180, unrotDw, unrotDh); + adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_270, unrotDh, unrotDw); if (outConfig == null) { return; } int sl = Configuration.resetScreenLayout(outConfig.screenLayout); - sl = reduceConfigLayout(sl, Surface.ROTATION_0, density, unrotDw, unrotDh, uiMode); - sl = reduceConfigLayout(sl, Surface.ROTATION_90, density, unrotDh, unrotDw, uiMode); - sl = reduceConfigLayout(sl, Surface.ROTATION_180, density, unrotDw, unrotDh, uiMode); - sl = reduceConfigLayout(sl, Surface.ROTATION_270, density, unrotDh, unrotDw, uiMode); + sl = reduceConfigLayout(sl, Surface.ROTATION_0, density, unrotDw, unrotDh); + sl = reduceConfigLayout(sl, Surface.ROTATION_90, density, unrotDh, unrotDw); + sl = reduceConfigLayout(sl, Surface.ROTATION_180, density, unrotDw, unrotDh); + sl = reduceConfigLayout(sl, Surface.ROTATION_270, density, unrotDh, unrotDw); outConfig.smallestScreenWidthDp = (int) (displayInfo.smallestNominalAppWidth / density + 0.5f); outConfig.screenLayout = sl; } - private int reduceConfigLayout(int curLayout, int rotation, float density, int dw, int dh, - int uiMode) { + private int reduceConfigLayout(int curLayout, int rotation, float density, int dw, int dh) { // Get the display cutout at this rotation. - final DisplayCutout displayCutout = calculateDisplayCutoutForRotation( - rotation).getDisplayCutout(); + final WmDisplayCutout wmDisplayCutout = calculateDisplayCutoutForRotation(rotation); // Get the app screen size at this rotation. - int w = mDisplayPolicy.getNonDecorDisplayWidth(dw, dh, rotation, uiMode, displayCutout); - int h = mDisplayPolicy.getNonDecorDisplayHeight(dh, rotation, displayCutout); + final Rect size = mDisplayPolicy.getNonDecorDisplayFrame(dw, dh, rotation, wmDisplayCutout); // Compute the screen layout size class for this rotation. - int longSize = w; - int shortSize = h; + int longSize = size.width(); + int shortSize = size.height(); if (longSize < shortSize) { int tmp = longSize; longSize = shortSize; @@ -2440,25 +2425,20 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return Configuration.reduceScreenLayout(curLayout, longSize, shortSize); } - private void adjustDisplaySizeRanges(DisplayInfo displayInfo, int rotation, - int uiMode, int dw, int dh) { - final DisplayCutout displayCutout = calculateDisplayCutoutForRotation( - rotation).getDisplayCutout(); - final int width = mDisplayPolicy.getConfigDisplayWidth(dw, dh, rotation, uiMode, - displayCutout); - if (width < displayInfo.smallestNominalAppWidth) { - displayInfo.smallestNominalAppWidth = width; + private void adjustDisplaySizeRanges(DisplayInfo displayInfo, int rotation, int dw, int dh) { + final WmDisplayCutout wmDisplayCutout = calculateDisplayCutoutForRotation(rotation); + final Point size = mDisplayPolicy.getConfigDisplaySize(dw, dh, rotation, wmDisplayCutout); + if (size.x < displayInfo.smallestNominalAppWidth) { + displayInfo.smallestNominalAppWidth = size.x; } - if (width > displayInfo.largestNominalAppWidth) { - displayInfo.largestNominalAppWidth = width; + if (size.x > displayInfo.largestNominalAppWidth) { + displayInfo.largestNominalAppWidth = size.x; } - final int height = mDisplayPolicy.getConfigDisplayHeight(dw, dh, rotation, uiMode, - displayCutout); - if (height < displayInfo.smallestNominalAppHeight) { - displayInfo.smallestNominalAppHeight = height; + if (size.y < displayInfo.smallestNominalAppHeight) { + displayInfo.smallestNominalAppHeight = size.y; } - if (height > displayInfo.largestNominalAppHeight) { - displayInfo.largestNominalAppHeight = height; + if (size.y > displayInfo.largestNominalAppHeight) { + displayInfo.largestNominalAppHeight = size.y; } } @@ -3304,6 +3284,14 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mAsyncRotationController.keepAppearanceInPreviousRotation(); } } else if (isRotationChanging()) { + if (displayChange != null) { + final boolean seamless = mDisplayRotation.shouldRotateSeamlessly( + displayChange.getStartRotation(), displayChange.getEndRotation(), + false /* forceUpdate */); + if (seamless) { + t.onSeamlessRotating(this); + } + } mWmService.mLatencyTracker.onActionStart(ACTION_ROTATE_SCREEN); controller.mTransitionMetricsReporter.associate(t, startTime -> mWmService.mLatencyTracker.onActionEnd(ACTION_ROTATE_SCREEN)); @@ -4400,13 +4388,20 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ @VisibleForTesting InsetsControlTarget computeImeControlTarget() { + if (mImeInputTarget == null) { + // A special case that if there is no IME input target while the IME is being killed, + // in case seeing unexpected IME surface visibility change when delivering the IME leash + // to the remote insets target during the IME restarting, but the focus window is not in + // multi-windowing mode, return null target until the next input target updated. + return null; + } + + final WindowState imeInputTarget = mImeInputTarget.getWindowState(); if (!isImeControlledByApp() && mRemoteInsetsControlTarget != null - || (mImeInputTarget != null - && getImeHostOrFallback(mImeInputTarget.getWindowState()) - == mRemoteInsetsControlTarget)) { + || getImeHostOrFallback(imeInputTarget) == mRemoteInsetsControlTarget) { return mRemoteInsetsControlTarget; } else { - return mImeInputTarget != null ? mImeInputTarget.getWindowState() : null; + return imeInputTarget; } } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 1a34c93f2ad6..8e06a810ead1 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -19,15 +19,19 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.Display.TYPE_INTERNAL; +import static android.view.InsetsState.ITYPE_BOTTOM_DISPLAY_CUTOUT; import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES; import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT; import static android.view.InsetsState.ITYPE_CAPTION_BAR; import static android.view.InsetsState.ITYPE_CLIMATE_BAR; import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; +import static android.view.InsetsState.ITYPE_LEFT_DISPLAY_CUTOUT; import static android.view.InsetsState.ITYPE_LEFT_GESTURES; import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; +import static android.view.InsetsState.ITYPE_RIGHT_DISPLAY_CUTOUT; import static android.view.InsetsState.ITYPE_RIGHT_GESTURES; import static android.view.InsetsState.ITYPE_STATUS_BAR; +import static android.view.InsetsState.ITYPE_TOP_DISPLAY_CUTOUT; import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES; import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; @@ -103,6 +107,7 @@ import android.content.Intent; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.gui.DropInputMode; @@ -127,6 +132,8 @@ import android.view.InsetsSource; import android.view.InsetsState; import android.view.InsetsState.InternalInsetsType; import android.view.InsetsVisibilities; +import android.view.PrivacyIndicatorBounds; +import android.view.RoundedCorners; import android.view.Surface; import android.view.View; import android.view.ViewDebug; @@ -136,7 +143,6 @@ import android.view.WindowLayout; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.WindowManagerGlobal; -import android.view.WindowManagerPolicyConstants; import android.view.accessibility.AccessibilityManager; import android.window.ClientWindowFrames; @@ -160,6 +166,7 @@ import com.android.server.policy.WindowManagerPolicy.ScreenOnListener; import com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.wallpaper.WallpaperManagerInternal; +import com.android.server.wm.utils.WmDisplayCutout; import java.io.PrintWriter; import java.util.ArrayList; @@ -383,6 +390,16 @@ public class DisplayPolicy { private static final int MSG_REQUEST_TRANSIENT_BARS_ARG_STATUS = 0; private static final int MSG_REQUEST_TRANSIENT_BARS_ARG_NAVIGATION = 1; + // TODO (b/235842600): Use public type once we can treat task bar as navigation bar. + private static final int[] STABLE_TYPES = new int[]{ + ITYPE_TOP_DISPLAY_CUTOUT, ITYPE_RIGHT_DISPLAY_CUTOUT, ITYPE_BOTTOM_DISPLAY_CUTOUT, + ITYPE_LEFT_DISPLAY_CUTOUT, ITYPE_NAVIGATION_BAR, ITYPE_STATUS_BAR, ITYPE_CLIMATE_BAR + }; + private static final int[] NON_DECOR_TYPES = new int[]{ + ITYPE_TOP_DISPLAY_CUTOUT, ITYPE_RIGHT_DISPLAY_CUTOUT, ITYPE_BOTTOM_DISPLAY_CUTOUT, + ITYPE_LEFT_DISPLAY_CUTOUT, ITYPE_NAVIGATION_BAR + }; + private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver; private final WindowManagerInternal.AppTransitionListener mAppTransitionListener; @@ -2007,35 +2024,6 @@ public class DisplayPolicy { return mUiContext; } - private int getNavigationBarWidth(int rotation, int uiMode, int position) { - if (mNavigationBar == null) { - return 0; - } - LayoutParams lp = mNavigationBar.mAttrs; - if (lp.paramsForRotation != null - && lp.paramsForRotation.length == 4 - && lp.paramsForRotation[rotation] != null) { - lp = lp.paramsForRotation[rotation]; - } - Insets providedInsetsSize = null; - if (lp.providedInsets != null) { - for (InsetsFrameProvider provider : lp.providedInsets) { - if (provider.type != ITYPE_NAVIGATION_BAR) { - continue; - } - providedInsetsSize = provider.insetsSize; - } - } - if (providedInsetsSize != null) { - if (position == NAV_BAR_LEFT) { - return providedInsetsSize.left; - } else if (position == NAV_BAR_RIGHT) { - return providedInsetsSize.right; - } - } - return lp.width; - } - @VisibleForTesting void setCanSystemBarsBeShownByUser(boolean canBeShown) { mCanSystemBarsBeShownByUser = canBeShown; @@ -2057,45 +2045,24 @@ public class DisplayPolicy { } /** - * Return the display width available after excluding any screen - * decorations that could never be removed in Honeycomb. That is, system bar or - * button bar. + * Return the display frame available after excluding any screen decorations that could never be + * removed in Honeycomb. That is, system bar or button bar. + * + * @return display frame excluding all non-decor insets. */ - public int getNonDecorDisplayWidth(int fullWidth, int fullHeight, int rotation, int uiMode, - DisplayCutout displayCutout) { - int width = fullWidth; - if (hasNavigationBar()) { - final int navBarPosition = navigationBarPosition(rotation); - if (navBarPosition == NAV_BAR_LEFT || navBarPosition == NAV_BAR_RIGHT) { - width -= getNavigationBarWidth(rotation, uiMode, navBarPosition); - } - } - if (displayCutout != null) { - width -= displayCutout.getSafeInsetLeft() + displayCutout.getSafeInsetRight(); - } - return width; + Rect getNonDecorDisplayFrame(int fullWidth, int fullHeight, int rotation, + WmDisplayCutout cutout) { + final DisplayFrames displayFrames = + getSimulatedDisplayFrames(rotation, fullWidth, fullHeight, cutout); + return getNonDecorDisplayFrameWithSimulatedFrame(displayFrames); } - @VisibleForTesting - int getNavigationBarHeight(int rotation) { - if (mNavigationBar == null) { - return 0; - } - LayoutParams lp = mNavigationBar.mAttrs.forRotation(rotation); - Insets providedInsetsSize = null; - if (lp.providedInsets != null) { - for (InsetsFrameProvider provider : lp.providedInsets) { - if (provider.type != ITYPE_NAVIGATION_BAR) { - continue; - } - providedInsetsSize = provider.insetsSize; - if (providedInsetsSize != null) { - return providedInsetsSize.bottom; - } - break; - } - } - return lp.height; + Rect getNonDecorDisplayFrameWithSimulatedFrame(DisplayFrames displayFrames) { + final Rect nonDecorInsets = + getInsetsWithInternalTypes(displayFrames, NON_DECOR_TYPES).toRect(); + final Rect displayFrame = new Rect(displayFrames.mInsetsState.getDisplayFrame()); + displayFrame.inset(nonDecorInsets); + return displayFrame; } /** @@ -2117,53 +2084,23 @@ public class DisplayPolicy { } /** - * Return the display height available after excluding any screen - * decorations that could never be removed in Honeycomb. That is, system bar or - * button bar. - */ - public int getNonDecorDisplayHeight(int fullHeight, int rotation, DisplayCutout displayCutout) { - int height = fullHeight; - final int navBarPosition = navigationBarPosition(rotation); - if (navBarPosition == NAV_BAR_BOTTOM) { - height -= getNavigationBarHeight(rotation); - } - if (displayCutout != null) { - height -= displayCutout.getSafeInsetTop() + displayCutout.getSafeInsetBottom(); - } - return height; - } - - /** - * Return the available screen width that we should report for the + * Return the available screen size that we should report for the * configuration. This must be no larger than - * {@link #getNonDecorDisplayWidth(int, int, int, int, DisplayCutout)}; it may be smaller + * {@link #getNonDecorDisplayFrame(int, int, int, DisplayCutout)}; it may be smaller * than that to account for more transient decoration like a status bar. */ - public int getConfigDisplayWidth(int fullWidth, int fullHeight, int rotation, int uiMode, - DisplayCutout displayCutout) { - return getNonDecorDisplayWidth(fullWidth, fullHeight, rotation, uiMode, displayCutout); + public Point getConfigDisplaySize(int fullWidth, int fullHeight, int rotation, + WmDisplayCutout wmDisplayCutout) { + final DisplayFrames displayFrames = getSimulatedDisplayFrames(rotation, fullWidth, + fullHeight, wmDisplayCutout); + return getConfigDisplaySizeWithSimulatedFrame(displayFrames); } - /** - * Return the available screen height that we should report for the - * configuration. This must be no larger than - * {@link #getNonDecorDisplayHeight(int, int, DisplayCutout)}; it may be smaller - * than that to account for more transient decoration like a status bar. - */ - public int getConfigDisplayHeight(int fullWidth, int fullHeight, int rotation, int uiMode, - DisplayCutout displayCutout) { - // There is a separate status bar at the top of the display. We don't count that as part - // of the fixed decor, since it can hide; however, for purposes of configurations, - // we do want to exclude it since applications can't generally use that part - // of the screen. - int statusBarHeight = mStatusBarHeightForRotation[rotation]; - if (displayCutout != null) { - // If there is a cutout, it may already have accounted for some part of the status - // bar height. - statusBarHeight = Math.max(0, statusBarHeight - displayCutout.getSafeInsetTop()); - } - return getNonDecorDisplayHeight(fullHeight, rotation, displayCutout) - - statusBarHeight; + Point getConfigDisplaySizeWithSimulatedFrame(DisplayFrames displayFrames) { + final Insets insets = getInsetsWithInternalTypes(displayFrames, STABLE_TYPES); + Rect configFrame = new Rect(displayFrames.mInsetsState.getDisplayFrame()); + configFrame.inset(insets); + return new Point(configFrame.width(), configFrame.height()); } /** @@ -2195,48 +2132,75 @@ public class DisplayPolicy { * Calculates the stable insets without running a layout. * * @param displayRotation the current display rotation + * @param displayWidth full display width + * @param displayHeight full display height * @param displayCutout the current display cutout * @param outInsets the insets to return */ - public void getStableInsetsLw(int displayRotation, DisplayCutout displayCutout, - Rect outInsets) { - outInsets.setEmpty(); + public void getStableInsetsLw(int displayRotation, int displayWidth, int displayHeight, + WmDisplayCutout displayCutout, Rect outInsets) { + final DisplayFrames displayFrames = getSimulatedDisplayFrames(displayRotation, + displayWidth, displayHeight, displayCutout); + getStableInsetsWithSimulatedFrame(displayFrames, outInsets); + } - // Navigation bar and status bar. - getNonDecorInsetsLw(displayRotation, displayCutout, outInsets); - convertNonDecorInsetsToStableInsets(outInsets, displayRotation); + void getStableInsetsWithSimulatedFrame(DisplayFrames displayFrames, Rect outInsets) { + // Navigation bar, status bar, and cutout. + outInsets.set(getInsetsWithInternalTypes(displayFrames, STABLE_TYPES).toRect()); } /** * Calculates the insets for the areas that could never be removed in Honeycomb, i.e. system - * bar or button bar. See {@link #getNonDecorDisplayWidth}. - * @param displayRotation the current display rotation - * @param displayCutout the current display cutout + * bar or button bar. See {@link #getNonDecorDisplayFrame}. + * + * @param displayRotation the current display rotation + * @param fullWidth the width of the display, including all insets + * @param fullHeight the height of the display, including all insets + * @param cutout the current display cutout * @param outInsets the insets to return */ - public void getNonDecorInsetsLw(int displayRotation, DisplayCutout displayCutout, - Rect outInsets) { - outInsets.setEmpty(); - - // Only navigation bar - if (hasNavigationBar()) { - final int uiMode = mService.mPolicy.getUiMode(); - int position = navigationBarPosition(displayRotation); - if (position == NAV_BAR_BOTTOM) { - outInsets.bottom = getNavigationBarHeight(displayRotation); - } else if (position == NAV_BAR_RIGHT) { - outInsets.right = getNavigationBarWidth(displayRotation, uiMode, position); - } else if (position == NAV_BAR_LEFT) { - outInsets.left = getNavigationBarWidth(displayRotation, uiMode, position); - } - } + public void getNonDecorInsetsLw(int displayRotation, int fullWidth, int fullHeight, + WmDisplayCutout cutout, Rect outInsets) { + final DisplayFrames displayFrames = + getSimulatedDisplayFrames(displayRotation, fullWidth, fullHeight, cutout); + getNonDecorInsetsWithSimulatedFrame(displayFrames, outInsets); + } + + void getNonDecorInsetsWithSimulatedFrame(DisplayFrames displayFrames, Rect outInsets) { + outInsets.set(getInsetsWithInternalTypes(displayFrames, NON_DECOR_TYPES).toRect()); + } + + DisplayFrames getSimulatedDisplayFrames(int displayRotation, int fullWidth, + int fullHeight, WmDisplayCutout cutout) { + final DisplayInfo info = new DisplayInfo(mDisplayContent.getDisplayInfo()); + info.rotation = displayRotation; + info.logicalWidth = fullWidth; + info.logicalHeight = fullHeight; + info.displayCutout = cutout.getDisplayCutout(); + final RoundedCorners roundedCorners = + mDisplayContent.calculateRoundedCornersForRotation(displayRotation); + final PrivacyIndicatorBounds indicatorBounds = + mDisplayContent.calculatePrivacyIndicatorBoundsForRotation(displayRotation); + final DisplayFrames displayFrames = new DisplayFrames(getDisplayId(), new InsetsState(), + info, cutout, roundedCorners, indicatorBounds); + simulateLayoutDisplay(displayFrames); + return displayFrames; + } - if (displayCutout != null) { - outInsets.left += displayCutout.getSafeInsetLeft(); - outInsets.top += displayCutout.getSafeInsetTop(); - outInsets.right += displayCutout.getSafeInsetRight(); - outInsets.bottom += displayCutout.getSafeInsetBottom(); - } + @VisibleForTesting + Insets getInsets(DisplayFrames displayFrames, @InsetsType int type) { + final InsetsState state = displayFrames.mInsetsState; + final Insets insets = state.calculateInsets(state.getDisplayFrame(), type, + true /* ignoreVisibility */); + return insets; + } + + Insets getInsetsWithInternalTypes(DisplayFrames displayFrames, + @InternalInsetsType int[] types) { + final InsetsState state = displayFrames.mInsetsState; + final Insets insets = state.calculateInsetsWithInternalTypes(state.getDisplayFrame(), types, + true /* ignoreVisibility */); + return insets; } @NavigationBarPosition @@ -2256,17 +2220,6 @@ public class DisplayPolicy { } /** - * @return The side of the screen where navigation bar is positioned. - * @see WindowManagerPolicyConstants#NAV_BAR_LEFT - * @see WindowManagerPolicyConstants#NAV_BAR_RIGHT - * @see WindowManagerPolicyConstants#NAV_BAR_BOTTOM - */ - @NavigationBarPosition - public int getNavBarPosition() { - return mNavigationBarPosition; - } - - /** * A new window has been focused. */ public void focusChangedLw(WindowState lastFocus, WindowState newFocus) { diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index b79c6f44bad5..856430dae6c4 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -847,6 +847,10 @@ class RootWindowContainer extends WindowContainer<DisplayContent> if (recentsAnimationController != null) { recentsAnimationController.checkAnimationReady(defaultDisplay.mWallpaperController); } + final BackNaviAnimationController bnac = mWmService.getBackNaviAnimationController(); + if (bnac != null) { + bnac.checkAnimationReady(defaultDisplay.mWallpaperController); + } for (int displayNdx = 0; displayNdx < mChildren.size(); ++displayNdx) { final DisplayContent displayContent = mChildren.get(displayNdx); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index e1334dc0ab88..18b0e3311a94 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -1889,8 +1889,7 @@ class Task extends TaskFragment { } final int newWinMode = getWindowingMode(); - if ((prevWinMode != newWinMode) && (mDisplayContent != null) - && shouldStartChangeTransition(prevWinMode, newWinMode)) { + if (shouldStartChangeTransition(prevWinMode, mTmpPrevBounds)) { initializeChangeTransition(mTmpPrevBounds); } @@ -2141,10 +2140,16 @@ class Task extends TaskFragment { bounds.offset(horizontalDiff, verticalDiff); } - private boolean shouldStartChangeTransition(int prevWinMode, int newWinMode) { + private boolean shouldStartChangeTransition(int prevWinMode, @NonNull Rect prevBounds) { if (!isLeafTask() || !canStartChangeTransition()) { return false; } + final int newWinMode = getWindowingMode(); + if (mTransitionController.inTransition(this)) { + final Rect newBounds = getConfiguration().windowConfiguration.getBounds(); + return prevWinMode != newWinMode || prevBounds.width() != newBounds.width() + || prevBounds.height() != newBounds.height(); + } // Only do an animation into and out-of freeform mode for now. Other mode // transition animations are currently handled by system-ui. return (prevWinMode == WINDOWING_MODE_FREEFORM) != (newWinMode == WINDOWING_MODE_FREEFORM); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 679a231265d1..da731e842aea 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -98,6 +98,7 @@ import com.android.internal.util.function.pooled.PooledLambda; import com.android.internal.util.function.pooled.PooledPredicate; import com.android.server.am.HostingRecord; import com.android.server.pm.parsing.pkg.AndroidPackage; +import com.android.server.wm.utils.WmDisplayCutout; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -2208,11 +2209,13 @@ class TaskFragment extends WindowContainer<WindowContainer> { mTmpBounds.set(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight); final DisplayPolicy policy = rootTask.mDisplayContent.getDisplayPolicy(); - policy.getNonDecorInsetsLw(displayInfo.rotation, - displayInfo.displayCutout, mTmpInsets); + final WmDisplayCutout cutout = + rootTask.mDisplayContent.calculateDisplayCutoutForRotation(displayInfo.rotation); + final DisplayFrames displayFrames = policy.getSimulatedDisplayFrames(displayInfo.rotation, + displayInfo.logicalWidth, displayInfo.logicalHeight, cutout); + policy.getNonDecorInsetsWithSimulatedFrame(displayFrames, mTmpInsets); intersectWithInsetsIfFits(outNonDecorBounds, mTmpBounds, mTmpInsets); - - policy.convertNonDecorInsetsToStableInsets(mTmpInsets, displayInfo.rotation); + policy.getStableInsetsWithSimulatedFrame(displayFrames, mTmpInsets); intersectWithInsetsIfFits(outStableBounds, mTmpBounds, mTmpInsets); } diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index d615583f4d7f..d8a054cf45fa 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -138,12 +138,12 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr new SparseArray<>(); /** - * List of {@link TaskFragmentTransaction#getTransactionToken()} that have been sent to the - * organizer. If the transaction is sent during a transition, the - * {@link TransitionController} will wait until the transaction is finished. + * Map from {@link TaskFragmentTransaction#getTransactionToken()} to the + * {@link Transition#getSyncId()} that has been deferred. {@link TransitionController} will + * wait until the organizer finished handling the {@link TaskFragmentTransaction}. * @see #onTransactionFinished(IBinder) */ - private final List<IBinder> mRunningTransactions = new ArrayList<>(); + private final ArrayMap<IBinder, Integer> mDeferredTransitions = new ArrayMap<>(); TaskFragmentOrganizerState(ITaskFragmentOrganizer organizer, int pid, int uid) { mOrganizer = organizer; @@ -190,9 +190,9 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr taskFragment.removeImmediately(); mOrganizedTaskFragments.remove(taskFragment); } - for (int i = mRunningTransactions.size() - 1; i >= 0; i--) { + for (int i = mDeferredTransitions.size() - 1; i >= 0; i--) { // Cleanup any running transaction to unblock the current transition. - onTransactionFinished(mRunningTransactions.get(i)); + onTransactionFinished(mDeferredTransitions.keyAt(i)); } mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/); } @@ -357,19 +357,34 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr if (!mWindowOrganizerController.getTransitionController().isCollecting()) { return; } + final int transitionId = mWindowOrganizerController.getTransitionController() + .getCollectingTransitionId(); ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, - "Defer transition ready for TaskFragmentTransaction=%s", transactionToken); - mRunningTransactions.add(transactionToken); + "Defer transition id=%d for TaskFragmentTransaction=%s", transitionId, + transactionToken); + mDeferredTransitions.put(transactionToken, transitionId); mWindowOrganizerController.getTransitionController().deferTransitionReady(); } /** Called when the transaction is finished. */ void onTransactionFinished(@NonNull IBinder transactionToken) { - if (!mRunningTransactions.remove(transactionToken)) { + if (!mDeferredTransitions.containsKey(transactionToken)) { + return; + } + final int transitionId = mDeferredTransitions.remove(transactionToken); + if (!mWindowOrganizerController.getTransitionController().isCollecting() + || mWindowOrganizerController.getTransitionController() + .getCollectingTransitionId() != transitionId) { + // This can happen when the transition is timeout or abort. + ProtoLog.w(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + "Deferred transition id=%d has been continued before the" + + " TaskFragmentTransaction=%s is finished", + transitionId, transactionToken); return; } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, - "Continue transition ready for TaskFragmentTransaction=%s", transactionToken); + "Continue transition id=%d for TaskFragmentTransaction=%s", transitionId, + transactionToken); mWindowOrganizerController.getTransitionController().continueTransitionReady(); } } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 2d3e437bed60..8e389d30ffe1 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -68,6 +69,7 @@ import android.app.ActivityManager; import android.content.pm.ActivityInfo; import android.graphics.Point; import android.graphics.Rect; +import android.hardware.HardwareBuffer; import android.os.Binder; import android.os.IBinder; import android.os.IRemoteCallback; @@ -205,6 +207,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe /** @see #setCanPipOnFinish */ private boolean mCanPipOnFinish = true; + private boolean mIsSeamlessRotation = false; + private IContainerFreezer mContainerFreezer = null; + Transition(@TransitionType int type, @TransitionFlags int flags, TransitionController controller, BLASTSyncEngine syncEngine) { mType = type; @@ -265,10 +270,31 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return mTargetDisplays.contains(dc); } + /** Set a transition to be a seamless-rotation. */ void setSeamlessRotation(@NonNull WindowContainer wc) { final ChangeInfo info = mChanges.get(wc); if (info == null) return; info.mFlags = info.mFlags | ChangeInfo.FLAG_SEAMLESS_ROTATION; + onSeamlessRotating(wc.getDisplayContent()); + } + + /** + * Called when it's been determined that this is transition is a seamless rotation. This should + * be called before any WM changes have happened. + */ + void onSeamlessRotating(@NonNull DisplayContent dc) { + // Don't need to do anything special if everything is using BLAST sync already. + if (mSyncEngine.getSyncSet(mSyncId).mSyncMethod == BLASTSyncEngine.METHOD_BLAST) return; + if (mContainerFreezer == null) { + mContainerFreezer = new ScreenshotFreezer(); + } + mIsSeamlessRotation = true; + final WindowState top = dc.getDisplayPolicy().getTopFullscreenOpaqueWindow(); + if (top != null) { + top.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST; + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Override sync-method for %s " + + "because seamless rotating", top.getName()); + } } /** @@ -285,6 +311,11 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } } + /** Only for testing. */ + void setContainerFreezer(IContainerFreezer freezer) { + mContainerFreezer = freezer; + } + @TransitionState int getState() { return mState; @@ -314,13 +345,18 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return mState == STATE_COLLECTING || mState == STATE_STARTED; } - /** Starts collecting phase. Once this starts, all relevant surface operations are sync. */ + @VisibleForTesting void startCollecting(long timeoutMs) { + startCollecting(timeoutMs, TransitionController.SYNC_METHOD); + } + + /** Starts collecting phase. Once this starts, all relevant surface operations are sync. */ + void startCollecting(long timeoutMs, int method) { if (mState != STATE_PENDING) { throw new IllegalStateException("Attempting to re-use a transition"); } mState = STATE_COLLECTING; - mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG); + mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG, method); mController.mTransitionTracer.logState(this); } @@ -415,6 +451,37 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } /** + * Records that a particular container is changing visibly (ie. something about it is changing + * while it remains visible). This only effects windows that are already in the collecting + * transition. + */ + void collectVisibleChange(WindowContainer wc) { + if (mSyncEngine.getSyncSet(mSyncId).mSyncMethod == BLASTSyncEngine.METHOD_BLAST) { + // All windows are synced already. + return; + } + if (!isInTransition(wc)) return; + + if (mContainerFreezer == null) { + mContainerFreezer = new ScreenshotFreezer(); + } + Transition.ChangeInfo change = mChanges.get(wc); + if (change == null || !change.mVisible || !wc.isVisibleRequested()) return; + // Note: many more tests have already been done by caller. + mContainerFreezer.freeze(wc, change.mAbsoluteBounds); + } + + /** + * @return {@code true} if `wc` is a participant or is a descendant of one. + */ + boolean isInTransition(WindowContainer wc) { + for (WindowContainer p = wc; p != null; p = p.getParent()) { + if (mParticipants.contains(p)) return true; + } + return false; + } + + /** * Specifies configuration change explicitly for the window container, so it can be chosen as * transition target. This is usually used with transition mode * {@link android.view.WindowManager#TRANSIT_CHANGE}. @@ -531,6 +598,10 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe displays.add(target.getDisplayContent()); } } + // Remove screenshot layers if necessary + if (mContainerFreezer != null) { + mContainerFreezer.cleanUp(t); + } // Need to update layers on involved displays since they were all paused while // the animation played. This puts the layers back into the correct order. mController.mBuildingFinishLayers = true; @@ -817,6 +888,19 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe transaction); if (mOverrideOptions != null) { info.setAnimationOptions(mOverrideOptions); + if (mOverrideOptions.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) { + for (int i = 0; i < mTargets.size(); ++i) { + final TransitionInfo.Change c = info.getChanges().get(i); + final ActivityRecord ar = mTargets.get(i).asActivityRecord(); + if (ar == null || c.getMode() != TRANSIT_OPEN) continue; + int flags = c.getFlags(); + flags |= ar.mUserId == ar.mWmService.mCurrentUserId + ? TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL + : TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; + c.setFlags(flags); + break; + } + } } // TODO(b/188669821): Move to animation impl in shell. @@ -1810,6 +1894,8 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe */ void deferTransitionReady() { ++mReadyTracker.mDeferReadyDepth; + // Make sure it wait until #continueTransitionReady() is called. + mSyncEngine.setReady(mSyncId, false); } /** This undoes one call to {@link #deferTransitionReady}. */ @@ -1982,4 +2068,111 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return sortedTargets; } } + + /** + * Interface for freezing a container's content during sync preparation. Really just one impl + * but broken into an interface for testing (since you can't take screenshots in unit tests). + */ + interface IContainerFreezer { + /** + * Makes sure a particular window is "frozen" for the remainder of a sync. + * + * @return whether the freeze was successful. It fails if `wc` is already in a frozen window + * or is not visible/ready. + */ + boolean freeze(@NonNull WindowContainer wc, @NonNull Rect bounds); + + /** Populates `t` with operations that clean-up any state created to set-up the freeze. */ + void cleanUp(SurfaceControl.Transaction t); + } + + /** + * Freezes container content by taking a screenshot. Because screenshots are heavy, usage of + * any container "freeze" is currently explicit. WM code needs to be prudent about which + * containers to freeze. + */ + @VisibleForTesting + private class ScreenshotFreezer implements IContainerFreezer { + /** Values are the screenshot "surfaces" or null if it was frozen via BLAST override. */ + private final ArrayMap<WindowContainer, SurfaceControl> mSnapshots = new ArrayMap<>(); + + /** Takes a screenshot and puts it at the top of the container's surface. */ + @Override + public boolean freeze(@NonNull WindowContainer wc, @NonNull Rect bounds) { + if (!wc.isVisibleRequested()) return false; + + // Check if any parents have already been "frozen". If so, `wc` is already part of that + // snapshot, so just skip it. + for (WindowContainer p = wc; p != null; p = p.getParent()) { + if (mSnapshots.containsKey(p)) return false; + } + + if (mIsSeamlessRotation) { + WindowState top = wc.getDisplayContent() == null ? null + : wc.getDisplayContent().getDisplayPolicy().getTopFullscreenOpaqueWindow(); + if (top != null && (top == wc || top.isDescendantOf(wc))) { + // Don't use screenshots for seamless windows: these will use BLAST even if not + // BLAST mode. + mSnapshots.put(wc, null); + return true; + } + } + + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Screenshotting %s [%s]", + wc.toString(), bounds.toString()); + + Rect cropBounds = new Rect(bounds); + cropBounds.offsetTo(0, 0); + SurfaceControl.LayerCaptureArgs captureArgs = + new SurfaceControl.LayerCaptureArgs.Builder(wc.getSurfaceControl()) + .setSourceCrop(cropBounds) + .setCaptureSecureLayers(true) + .setAllowProtected(true) + .build(); + SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = + SurfaceControl.captureLayers(captureArgs); + final HardwareBuffer buffer = screenshotBuffer == null ? null + : screenshotBuffer.getHardwareBuffer(); + if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) { + // This can happen when display is not ready. + Slog.w(TAG, "Failed to capture screenshot for " + wc); + return false; + } + SurfaceControl snapshotSurface = wc.makeAnimationLeash() + .setName("transition snapshot: " + wc.toString()) + .setOpaque(true) + .setParent(wc.getSurfaceControl()) + .setSecure(screenshotBuffer.containsSecureLayers()) + .setCallsite("Transition.ScreenshotSync") + .setBLASTLayer() + .build(); + mSnapshots.put(wc, snapshotSurface); + SurfaceControl.Transaction t = wc.mWmService.mTransactionFactory.get(); + + t.setBuffer(snapshotSurface, buffer); + t.setDataSpace(snapshotSurface, screenshotBuffer.getColorSpace().getDataSpace()); + t.show(snapshotSurface); + + // Place it on top of anything else in the container. + t.setLayer(snapshotSurface, Integer.MAX_VALUE); + t.apply(); + t.close(); + + // Detach the screenshot on the sync transaction (the screenshot is just meant to + // freeze the window until the sync transaction is applied (with all its other + // corresponding changes), so this is how we unfreeze it. + wc.getSyncTransaction().reparent(snapshotSurface, null /* newParent */); + return true; + } + + @Override + public void cleanUp(SurfaceControl.Transaction t) { + for (int i = 0; i < mSnapshots.size(); ++i) { + SurfaceControl snap = mSnapshots.valueAt(i); + // May be null if it was frozen via BLAST override. + if (snap == null) continue; + t.remove(snap); + } + } + } } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 846aa3e3739a..23928aed6f65 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -46,6 +46,7 @@ import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.common.ProtoLog; import com.android.server.LocalServices; @@ -64,6 +65,11 @@ class TransitionController { private static final boolean SHELL_TRANSITIONS_ROTATION = SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false); + /** Which sync method to use for transition syncs. */ + static final int SYNC_METHOD = + android.os.SystemProperties.getBoolean("persist.wm.debug.shell_transit_blast", true) + ? BLASTSyncEngine.METHOD_BLAST : BLASTSyncEngine.METHOD_NONE; + /** The same as legacy APP_TRANSITION_TIMEOUT_MS. */ private static final int DEFAULT_TIMEOUT_MS = 5000; /** Less duration for CHANGE type because it does not involve app startup. */ @@ -160,6 +166,12 @@ class TransitionController { /** Starts Collecting */ void moveToCollecting(@NonNull Transition transition) { + moveToCollecting(transition, SYNC_METHOD); + } + + /** Starts Collecting */ + @VisibleForTesting + void moveToCollecting(@NonNull Transition transition, int method) { if (mCollectingTransition != null) { throw new IllegalStateException("Simultaneous transition collection not supported."); } @@ -167,7 +179,7 @@ class TransitionController { // Distinguish change type because the response time is usually expected to be not too long. final long timeoutMs = transition.mType == TRANSIT_CHANGE ? CHANGE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS; - mCollectingTransition.startCollecting(timeoutMs); + mCollectingTransition.startCollecting(timeoutMs, method); ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Start collecting in Transition: %s", mCollectingTransition); dispatchLegacyAppTransitionPending(); @@ -215,6 +227,17 @@ class TransitionController { } /** + * @return the collecting transition sync Id. This should only be called when there is a + * collecting transition. + */ + int getCollectingTransitionId() { + if (mCollectingTransition == null) { + throw new IllegalStateException("There is no collecting transition"); + } + return mCollectingTransition.getSyncId(); + } + + /** * @return {@code true} if transition is actively collecting changes and `wc` is one of them. * This is {@code false} once a transition is playing. */ @@ -228,10 +251,7 @@ class TransitionController { */ boolean inCollectingTransition(@NonNull WindowContainer wc) { if (!isCollecting()) return false; - for (WindowContainer p = wc; p != null; p = p.getParent()) { - if (mCollectingTransition.mParticipants.contains(p)) return true; - } - return false; + return mCollectingTransition.isInTransition(wc); } /** @@ -247,9 +267,7 @@ class TransitionController { */ boolean inPlayingTransition(@NonNull WindowContainer wc) { for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { - for (WindowContainer p = wc; p != null; p = p.getParent()) { - if (mPlayingTransitions.get(i).mParticipants.contains(p)) return true; - } + if (mPlayingTransitions.get(i).isInTransition(wc)) return true; } return false; } @@ -469,6 +487,7 @@ class TransitionController { void collectForDisplayAreaChange(@NonNull DisplayArea<?> wc) { final Transition transition = mCollectingTransition; if (transition == null || !transition.mParticipants.contains(wc)) return; + transition.collectVisibleChange(wc); // Collect all visible tasks. wc.forAllLeafTasks(task -> { if (task.isVisible()) { @@ -488,6 +507,16 @@ class TransitionController { } } + /** + * Records that a particular container is changing visibly (ie. something about it is changing + * while it remains visible). This only effects windows that are already in the collecting + * transition. + */ + void collectVisibleChange(WindowContainer wc) { + if (!isCollecting()) return; + mCollectingTransition.collectVisibleChange(wc); + } + /** @see Transition#mStatusBarTransitionDelay */ void setStatusBarTransitionDelay(long delay) { if (mCollectingTransition == null) return; diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 6245005606d7..e7c0a8aba285 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -186,7 +186,7 @@ class WallpaperController { && animatingContainer.getAnimation() != null && animatingContainer.getAnimation().getShowWallpaper(); final boolean hasWallpaper = w.hasWallpaper() || animationWallpaper; - if (isRecentsTransitionTarget(w)) { + if (isRecentsTransitionTarget(w) || isBackAnimationTarget(w)) { if (DEBUG_WALLPAPER) Slog.v(TAG, "Found recents animation wallpaper target: " + w); mFindResults.setWallpaperTarget(w); return true; @@ -226,6 +226,13 @@ class WallpaperController { return controller != null && controller.isWallpaperVisible(w); } + private boolean isBackAnimationTarget(WindowState w) { + // The window is either the back activity or is in the task animating by the back gesture. + final BackNaviAnimationController bthController = mService.getBackNaviAnimationController(); + return bthController != null && bthController.isWallpaperVisible(w); + } + + /** * @see #computeLastWallpaperZoomOut() */ diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 6ee30bb956f0..8fdaec613ad5 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -128,12 +128,15 @@ class WallpaperWindowToken extends WindowToken { if (visible && wallpaperTarget != null) { final RecentsAnimationController recentsAnimationController = mWmService.getRecentsAnimationController(); + final BackNaviAnimationController bac = mWmService.getBackNaviAnimationController(); if (recentsAnimationController != null && recentsAnimationController.isAnimatingTask(wallpaperTarget.getTask())) { // If the Recents animation is running, and the wallpaper target is the animating // task we want the wallpaper to be rotated in the same orientation as the // RecentsAnimation's target (e.g the launcher) recentsAnimationController.linkFixedRotationTransformIfNeeded(this); + } else if (bac != null && bac.isAnimatingTask(wallpaperTarget.getTask())) { + bac.linkFixedRotationTransformIfNeeded(this); } else if ((wallpaperTarget.mActivityRecord == null // Ignore invisible activity because it may be moving to background. || wallpaperTarget.mActivityRecord.mVisibleRequested) diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 797904890f74..92e52de2c01f 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -343,6 +343,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< BLASTSyncEngine.SyncGroup mSyncGroup = null; final SurfaceControl.Transaction mSyncTransaction; @SyncState int mSyncState = SYNC_STATE_NONE; + int mSyncMethodOverride = BLASTSyncEngine.METHOD_UNDEFINED; private final List<WindowContainerListener> mListeners = new ArrayList<>(); @@ -2825,6 +2826,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< */ void initializeChangeTransition(Rect startBounds, @Nullable SurfaceControl freezeTarget) { if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) { + mDisplayContent.mTransitionController.collectVisibleChange(this); // TODO(b/207070762): request shell transition for activityEmbedding change. return; } @@ -3666,6 +3668,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< boolean onSyncFinishedDrawing() { if (mSyncState == SYNC_STATE_NONE) return false; mSyncState = SYNC_STATE_READY; + mSyncMethodOverride = BLASTSyncEngine.METHOD_UNDEFINED; mWmService.mWindowPlacerLocked.requestTraversal(); ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "onSyncFinishedDrawing %s", this); return true; @@ -3684,6 +3687,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< mSyncGroup = group; } + @Nullable + BLASTSyncEngine.SyncGroup getSyncGroup() { + if (mSyncGroup != null) return mSyncGroup; + if (mParent != null) return mParent.getSyncGroup(); + return null; + } + /** * Prepares this container for participation in a sync-group. This includes preparing all its * children. @@ -3723,6 +3733,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } if (cancel && mSyncGroup != null) mSyncGroup.onCancelSync(this); mSyncState = SYNC_STATE_NONE; + mSyncMethodOverride = BLASTSyncEngine.METHOD_UNDEFINED; mSyncGroup = null; } @@ -3825,6 +3836,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // disable this when shell transitions is disabled. if (mTransitionController.isShellTransitionsEnabled()) { mSyncState = SYNC_STATE_NONE; + mSyncMethodOverride = BLASTSyncEngine.METHOD_UNDEFINED; } prepareSync(); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index dce0fbe42f3c..285e0ac1c67a 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -93,7 +93,6 @@ import static android.view.WindowManager.fixScale; import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; -import static android.view.WindowManagerPolicyConstants.NAV_BAR_INVALID; import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN; @@ -322,6 +321,7 @@ import com.android.server.policy.WindowManagerPolicy; import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.power.ShutdownThread; import com.android.server.utils.PriorityDump; +import com.android.server.wm.utils.WmDisplayCutout; import dalvik.annotation.optimization.NeverCompile; @@ -1870,7 +1870,8 @@ public class WindowManagerService extends IWindowManager.Stub ProtoLog.v(WM_DEBUG_ADD_REMOVE, "addWindow: New client %s" + ": window=%s Callers=%s", client.asBinder(), win, Debug.getCallers(5)); - if (win.isVisibleRequestedOrAdding() && displayContent.updateOrientation()) { + if ((win.isVisibleRequestedOrAdding() && displayContent.updateOrientation()) + || win.providesNonDecorInsets()) { displayContent.sendNewConfiguration(); } @@ -2587,7 +2588,7 @@ public class WindowManagerService extends IWindowManager.Stub final int maybeSyncSeqId; if (mUseBLASTSync && win.useBLASTSync() && viewVisibility != View.GONE && win.mSyncSeqId > lastSyncSeqId) { - maybeSyncSeqId = win.mSyncSeqId; + maybeSyncSeqId = win.shouldSyncWithBuffers() ? win.mSyncSeqId : -1; win.markRedrawForSyncReported(); } else { maybeSyncSeqId = -1; @@ -6383,27 +6384,6 @@ public class WindowManagerService extends IWindowManager.Stub } } - /** - * Used by ActivityManager to determine where to position an app with aspect ratio shorter then - * the screen is. - * @see DisplayPolicy#getNavBarPosition() - */ - @Override - @WindowManagerPolicy.NavigationBarPosition - public int getNavBarPosition(int displayId) { - synchronized (mGlobalLock) { - // Perform layout if it was scheduled before to make sure that we get correct nav bar - // position when doing rotations. - final DisplayContent displayContent = mRoot.getDisplayContent(displayId); - if (displayContent == null) { - Slog.w(TAG, "getNavBarPosition with invalid displayId=" + displayId - + " callers=" + Debug.getCallers(3)); - return NAV_BAR_INVALID; - } - return displayContent.getDisplayPolicy().getNavBarPosition(); - } - } - @Override public void createInputConsumer(IBinder token, String name, int displayId, InputChannel inputChannel) { @@ -7242,7 +7222,9 @@ public class WindowManagerService extends IWindowManager.Stub final DisplayContent dc = mRoot.getDisplayContent(displayId); if (dc != null) { final DisplayInfo di = dc.getDisplayInfo(); - dc.getDisplayPolicy().getStableInsetsLw(di.rotation, di.displayCutout, outInsets); + final WmDisplayCutout cutout = dc.calculateDisplayCutoutForRotation(di.rotation); + dc.getDisplayPolicy().getStableInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight, + cutout, outInsets); } } @@ -9298,4 +9280,9 @@ public class WindowManagerService extends IWindowManager.Stub "Unexpected letterbox background type: " + letterboxBackgroundType); } } + + BackNaviAnimationController getBackNaviAnimationController() { + return mAtmService.mBackNavigationController != null + ? mAtmService.mBackNavigationController.mBackNaviAnimationController : null; + } } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 68b1d354272d..34a4bf1d9c2a 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -99,6 +99,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.IntSupplier; /** @@ -1405,7 +1406,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub private BLASTSyncEngine.SyncGroup prepareSyncWithOrganizer( IWindowContainerTransactionCallback callback) { final BLASTSyncEngine.SyncGroup s = mService.mWindowManager.mSyncEngine - .prepareSyncSet(this, ""); + .prepareSyncSet(this, "", BLASTSyncEngine.METHOD_BLAST); mTransactionCallbacksByPendingSyncId.put(s.mSyncId, callback); return s; } @@ -1524,7 +1525,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final int type = hop.getType(); // Check for each type of the operations that are allowed for TaskFragmentOrganizer. switch (type) { - case HIERARCHY_OP_TYPE_REORDER: case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT: enforceTaskFragmentOrganized(func, WindowContainer.fromBinder(hop.getContainer()), organizer); @@ -1540,14 +1540,19 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // We are allowing organizer to create TaskFragment. We will check the // ownerToken in #createTaskFragment, and trigger error callback if that is not // valid. + break; case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: + case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: + enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); + break; case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: + enforceTaskFragmentOrganized(func, hop.getNewParent(), organizer); + break; case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: - case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: - // We are allowing organizer to start/reparent activity to a TaskFragment it - // created, or set two TaskFragments adjacent to each other. Nothing to check - // here because the TaskFragment may not be created yet, but will be created in - // the same transaction. + enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); + if (hop.getAdjacentRoot() != null) { + enforceTaskFragmentOrganized(func, hop.getAdjacentRoot(), organizer); + } break; case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: enforceTaskFragmentOrganized(func, @@ -1570,8 +1575,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } } - private void enforceTaskFragmentOrganized(String func, @Nullable WindowContainer wc, - ITaskFragmentOrganizer organizer) { + /** + * Makes sure that the given {@link WindowContainer} is a {@link TaskFragment} organized by the + * given {@link ITaskFragmentOrganizer}. + */ + private void enforceTaskFragmentOrganized(@NonNull String func, @Nullable WindowContainer wc, + @NonNull ITaskFragmentOrganizer organizer) { if (wc == null) { Slog.e(TAG, "Attempt to operate on window that no longer exists"); return; @@ -1588,6 +1597,26 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } /** + * Makes sure that the {@link TaskFragment} of the given fragment token is created and organized + * by the given {@link ITaskFragmentOrganizer}. + */ + private void enforceTaskFragmentOrganized(@NonNull String func, + @NonNull IBinder fragmentToken, @NonNull ITaskFragmentOrganizer organizer) { + Objects.requireNonNull(fragmentToken); + final TaskFragment tf = mLaunchTaskFragments.get(fragmentToken); + // When the TaskFragment is {@code null}, it means that the TaskFragment will be created + // later in the same transaction, in which case it will always be organized by the given + // organizer. + if (tf != null && !tf.hasTaskFragmentOrganizer(organizer)) { + String msg = "Permission Denial: " + func + " from pid=" + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid() + " trying to modify TaskFragment not" + + " belonging to the TaskFragmentOrganizer=" + organizer; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + } + + /** * Makes sure that SurfaceControl transactions and the ability to set bounds outside of the * parent bounds are not allowed for embedding without full trust between the host and the * target. @@ -1669,6 +1698,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final ITaskFragmentOrganizer organizer = ITaskFragmentOrganizer.Stub.asInterface( creationParams.getOrganizer().asBinder()); + if (mLaunchTaskFragments.containsKey(creationParams.getFragmentToken())) { + final Throwable exception = + new IllegalArgumentException("TaskFragment token must be unique"); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, + HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + return; + } if (ownerActivity == null || ownerActivity.getTask() == null) { final Throwable exception = new IllegalArgumentException("Not allowed to operate with invalid ownerToken"); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 2432afbc03ef..d79011be7931 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -243,6 +243,7 @@ import android.view.View; import android.view.ViewDebug; import android.view.ViewTreeObserver; import android.view.WindowInfo; +import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager; import android.view.animation.Animation; @@ -391,7 +392,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP */ int mSyncSeqId = 0; - /** The last syncId associated with a prepareSync or 0 when no sync is active. */ + /** The last syncId associated with a BLAST prepareSync or 0 when no BLAST sync is active. */ int mPrepareSyncSeqId = 0; /** @@ -1897,6 +1898,19 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return (mPolicyVisibility & POLICY_VISIBILITY_ALL) == POLICY_VISIBILITY_ALL; } + boolean providesNonDecorInsets() { + if (mProvidedInsetsSources == null) { + return false; + } + for (int i = mProvidedInsetsSources.size() - 1; i >= 0; i--) { + final int type = mProvidedInsetsSources.keyAt(i); + if ((InsetsState.toPublicType(type) & WindowInsets.Type.navigationBars()) != 0) { + return true; + } + } + return false; + } + void clearPolicyVisibilityFlag(int policyVisibilityFlag) { mPolicyVisibility &= ~policyVisibilityFlag; mWmService.scheduleAnimationLocked(); @@ -2609,14 +2623,19 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } removeImmediately(); - // Removing a visible window will effect the computed orientation - // So just update orientation if needed. + boolean sentNewConfig = false; if (wasVisible) { + // Removing a visible window will effect the computed orientation + // So just update orientation if needed. final DisplayContent displayContent = getDisplayContent(); if (displayContent.updateOrientation()) { displayContent.sendNewConfiguration(); + sentNewConfig = true; } } + if (!sentNewConfig && providesNonDecorInsets()) { + getDisplayContent().sendNewConfiguration(); + } mWmService.updateFocusedWindowLocked(isFocused() ? UPDATE_FOCUS_REMOVING_FOCUS : UPDATE_FOCUS_NORMAL, @@ -3893,9 +3912,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration, true /* useLatestConfig */, false /* relayoutVisible */); final boolean syncRedraw = shouldSendRedrawForSync(); + final boolean syncWithBuffers = syncRedraw && shouldSyncWithBuffers(); final boolean reportDraw = syncRedraw || drawPending; final boolean isDragResizeChanged = isDragResizeChanged(); - final boolean forceRelayout = syncRedraw || isDragResizeChanged; + final boolean forceRelayout = syncWithBuffers || isDragResizeChanged; final DisplayContent displayContent = getDisplayContent(); final boolean alwaysConsumeSystemBars = displayContent.getDisplayPolicy().areSystemBarsForcedShownLw(); @@ -3921,7 +3941,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP try { mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration, getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId, - mSyncSeqId, resizeMode); + syncWithBuffers ? mSyncSeqId : -1, resizeMode); if (drawPending && prevRotation >= 0 && prevRotation != mLastReportedConfiguration .getMergedConfiguration().windowConfiguration.getRotation()) { mOrientationChangeRedrawRequestTime = SystemClock.elapsedRealtime(); @@ -5936,7 +5956,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } mSyncSeqId++; - mPrepareSyncSeqId = mSyncSeqId; + if (getSyncMethod() == BLASTSyncEngine.METHOD_BLAST) { + mPrepareSyncSeqId = mSyncSeqId; + } requestRedrawForSync(); return true; } @@ -6009,6 +6031,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP postDrawTransaction = null; skipLayout = true; } else if (syncActive) { + // Currently in a Sync that is using BLAST. if (!syncStillPending) { onSyncFinishedDrawing(); } @@ -6017,6 +6040,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // Consume the transaction because the sync group will merge it. postDrawTransaction = null; } + } else if (useBLASTSync()) { + // Sync that is not using BLAST + onSyncFinishedDrawing(); } final boolean layoutNeeded = @@ -6075,6 +6101,18 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return useBLASTSync(); } + int getSyncMethod() { + final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup(); + if (syncGroup == null) return BLASTSyncEngine.METHOD_NONE; + if (mSyncMethodOverride != BLASTSyncEngine.METHOD_UNDEFINED) return mSyncMethodOverride; + return syncGroup.mSyncMethod; + } + + boolean shouldSyncWithBuffers() { + if (!mDrawHandlers.isEmpty()) return true; + return getSyncMethod() == BLASTSyncEngine.METHOD_BLAST; + } + void requestRedrawForSync() { mRedrawForSyncReported = false; } diff --git a/services/core/xsd/display-device-config/autobrightness.xsd b/services/core/xsd/display-device-config/autobrightness.xsd deleted file mode 100644 index 477625a36cbd..000000000000 --- a/services/core/xsd/display-device-config/autobrightness.xsd +++ /dev/null @@ -1,33 +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. ---> -<xs:schema version="2.0" - elementFormDefault="qualified" - xmlns:xs="http://www.w3.org/2001/XMLSchema"> - <xs:complexType name="autoBrightness"> - <xs:sequence> - <!-- Sets the debounce for autoBrightness brightening in millis--> - <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger" - minOccurs="0" maxOccurs="1"> - <xs:annotation name="final"/> - </xs:element> - <!-- Sets the debounce for autoBrightness darkening in millis--> - <xs:element name="darkeningLightDebounceMillis" type="xs:nonNegativeInteger" - minOccurs="0" maxOccurs="1"> - <xs:annotation name="final"/> - </xs:element> - </xs:sequence> - </xs:complexType> -</xs:schema>
\ No newline at end of file diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index bea5e2c2de74..98f83d8c0d09 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -23,7 +23,6 @@ <xs:schema version="2.0" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> - <xs:include schemaLocation="autobrightness.xsd" /> <xs:element name="displayConfiguration"> <xs:complexType> <xs:sequence> @@ -343,4 +342,74 @@ <xs:annotation name="final"/> </xs:element> </xs:complexType> + + <xs:complexType name="autoBrightness"> + <xs:sequence> + <!-- Sets the debounce for autoBrightness brightening in millis--> + <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> + <!-- Sets the debounce for autoBrightness darkening in millis--> + <xs:element name="darkeningLightDebounceMillis" type="xs:nonNegativeInteger" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> + <!-- Sets the brightness mapping of the desired screen brightness in nits to the + corresponding lux for the current display --> + <xs:element name="displayBrightnessMapping" type="displayBrightnessMapping" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> + </xs:sequence> + </xs:complexType> + + <!-- Represents the brightness mapping of the desired screen brightness in nits to the + corresponding lux for the current display --> + <xs:complexType name="displayBrightnessMapping"> + <xs:sequence> + <!-- Sets the list of display brightness points, each representing the desired screen + brightness in nits to the corresponding lux for the current display + + The N entries of this array define N + 1 control points as follows: + (1-based arrays) + + Point 1: (0, nits[1]): currentLux <= 0 + Point 2: (lux[1], nits[2]): 0 < currentLux <= lux[1] + Point 3: (lux[2], nits[3]): lux[2] < currentLux <= lux[3] + ... + Point N+1: (lux[N], nits[N+1]): lux[N] < currentLux + + The control points must be strictly increasing. Each control point + corresponds to an entry in the brightness backlight values arrays. + For example, if currentLux == lux[1] (first element of the levels array) + then the brightness will be determined by nits[2] (second element + of the brightness values array). + --> + <xs:element name="displayBrightnessPoint" type="displayBrightnessPoint" + minOccurs="1" maxOccurs="unbounded"> + <xs:annotation name="final"/> + </xs:element> + </xs:sequence> + </xs:complexType> + + <!-- Represents a point in the display brightness mapping, representing the lux level from the + light sensor to the desired screen brightness in nits at this level --> + <xs:complexType name="displayBrightnessPoint"> + <xs:sequence> + <!-- The lux level from the light sensor. This must be a non-negative integer --> + <xs:element name="lux" type="xs:nonNegativeInteger" + minOccurs="1" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> + + <!-- Desired screen brightness in nits corresponding to the suggested lux values. + The display brightness is defined as the measured brightness of an all-white image. + This must be a non-negative integer --> + <xs:element name="nits" type="xs:nonNegativeInteger" + minOccurs="1" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> + </xs:sequence> + </xs:complexType> </xs:schema> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index e9a926946764..e5d26177b725 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -5,8 +5,10 @@ package com.android.server.display.config { ctor public AutoBrightness(); method public final java.math.BigInteger getBrighteningLightDebounceMillis(); method public final java.math.BigInteger getDarkeningLightDebounceMillis(); + method public final com.android.server.display.config.DisplayBrightnessMapping getDisplayBrightnessMapping(); method public final void setBrighteningLightDebounceMillis(java.math.BigInteger); method public final void setDarkeningLightDebounceMillis(java.math.BigInteger); + method public final void setDisplayBrightnessMapping(com.android.server.display.config.DisplayBrightnessMapping); } public class BrightnessThresholds { @@ -43,6 +45,19 @@ package com.android.server.display.config { method public java.util.List<com.android.server.display.config.Density> getDensity(); } + public class DisplayBrightnessMapping { + ctor public DisplayBrightnessMapping(); + method public final java.util.List<com.android.server.display.config.DisplayBrightnessPoint> getDisplayBrightnessPoint(); + } + + public class DisplayBrightnessPoint { + ctor public DisplayBrightnessPoint(); + method public final java.math.BigInteger getLux(); + method public final java.math.BigInteger getNits(); + method public final void setLux(java.math.BigInteger); + method public final void setNits(java.math.BigInteger); + } + public class DisplayConfiguration { ctor public DisplayConfiguration(); method @NonNull public final com.android.server.display.config.Thresholds getAmbientBrightnessChangeThresholds(); diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java index 617321beadd2..220cd890e045 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -149,6 +149,12 @@ public class LocalDisplayAdapterTest { .thenReturn(mockArray); when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerBottomRadiusArray)) .thenReturn(mockArray); + when(mMockedResources.obtainTypedArray( + com.android.internal.R.array.config_autoBrightnessDisplayValuesNits)) + .thenReturn(mockArray); + when(mMockedResources.obtainTypedArray( + com.android.internal.R.array.config_autoBrightnessLevels)) + .thenReturn(mockArray); } @After diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java index 5f88c99b1d1e..50769155cfec 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java @@ -24,7 +24,8 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.hardware.biometrics.BiometricStateListener; +import android.hardware.biometrics.IBiometricStateListener; +import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; @@ -45,36 +46,46 @@ public class BiometricStateCallbackTest { private BiometricStateCallback mCallback; @Mock - BiometricStateListener mBiometricStateListener; + private IBiometricStateListener.Stub mBiometricStateListener; @Before public void setup() { MockitoAnnotations.initMocks(this); + when(mBiometricStateListener.asBinder()).thenReturn(mBiometricStateListener); + mCallback = new BiometricStateCallback(); mCallback.registerBiometricStateListener(mBiometricStateListener); } @Test - public void testNoEnrollmentsToEnrollments_callbackNotified() { + public void testNoEnrollmentsToEnrollments_callbackNotified() throws RemoteException { testEnrollmentCallback(true /* changed */, true /* isNowEnrolled */, true /* expectCallback */, true /* expectedCallbackValue */); } @Test - public void testEnrollmentsToNoEnrollments_callbackNotified() { + public void testEnrollmentsToNoEnrollments_callbackNotified() throws RemoteException { testEnrollmentCallback(true /* changed */, false /* isNowEnrolled */, true /* expectCallback */, false /* expectedCallbackValue */); } @Test - public void testEnrollmentsToEnrollments_callbackNotNotified() { + public void testEnrollmentsToEnrollments_callbackNotNotified() throws RemoteException { testEnrollmentCallback(false /* changed */, true /* isNowEnrolled */, false /* expectCallback */, false /* expectedCallbackValue */); } + @Test + public void testBinderDeath() throws RemoteException { + mCallback.binderDied(mBiometricStateListener.asBinder()); + + testEnrollmentCallback(true /* changed */, false /* isNowEnrolled */, + false /* expectCallback */, false /* expectedCallbackValue */); + } + private void testEnrollmentCallback(boolean changed, boolean isNowEnrolled, - boolean expectCallback, boolean expectedCallbackValue) { + boolean expectCallback, boolean expectedCallbackValue) throws RemoteException { EnrollClient<?> client = mock(EnrollClient.class); final int userId = 10; @@ -96,7 +107,7 @@ public class BiometricStateCallbackTest { } @Test - public void testAuthentication_enrollmentCallbackNeverNotified() { + public void testAuthentication_enrollmentCallbackNeverNotified() throws RemoteException { AuthenticationClient<?> client = mock(AuthenticationClient.class); mCallback.onClientFinished(client, true /* success */); verify(mBiometricStateListener, never()).onEnrollmentsChanged(anyInt(), anyInt(), diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java index 03ea6137074d..261b882319d8 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -19,16 +19,19 @@ package com.android.server.display; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -52,22 +55,16 @@ public final class DisplayDeviceConfigTest { private Resources mResources; @Before - public void setUp() throws IOException { + public void setUp() { MockitoAnnotations.initMocks(this); when(mContext.getResources()).thenReturn(mResources); mockDeviceConfigs(); - try { - Path tempFile = Files.createTempFile("display_config", ".tmp"); - Files.write(tempFile, getContent().getBytes(StandardCharsets.UTF_8)); - mDisplayDeviceConfig = new DisplayDeviceConfig(mContext); - mDisplayDeviceConfig.initFromFile(tempFile.toFile()); - } catch (IOException e) { - throw new IOException("Failed to setup the display device config.", e); - } } @Test - public void testConfigValues() { + public void testConfigValuesFromDisplayConfig() throws IOException { + setupDisplayDeviceConfigFromDisplayConfigFile(); + assertEquals(mDisplayDeviceConfig.getAmbientHorizonLong(), 5000); assertEquals(mDisplayDeviceConfig.getAmbientHorizonShort(), 50); assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000); @@ -88,10 +85,24 @@ public final class DisplayDeviceConfigTest { assertEquals(mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), 0.002, 0.000001f); assertEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLightDebounce(), 2000); assertEquals(mDisplayDeviceConfig.getAutoBrightnessDarkeningLightDebounce(), 1000); + assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new + float[]{50.0f, 80.0f}, 0.0f); + assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new + float[]{45.0f, 75.0f}, 0.0f); + // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping, + // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor. + } + @Test + public void testConfigValuesFromDeviceConfig() { + setupDisplayDeviceConfigFromDeviceConfigFile(); + assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new + float[]{0.0f, 110.0f, 500.0f}, 0.0f); + assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new + float[]{2.0f, 200.0f, 600.0f}, 0.0f); // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping, // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor. - // Also add test for the case where optional display configs are null + } private String getContent() { @@ -114,6 +125,16 @@ public final class DisplayDeviceConfigTest { + "<autoBrightness>\n" + "<brighteningLightDebounceMillis>2000</brighteningLightDebounceMillis>\n" + "<darkeningLightDebounceMillis>1000</darkeningLightDebounceMillis>\n" + + "<displayBrightnessMapping>\n" + + "<displayBrightnessPoint>\n" + + "<lux>50</lux>\n" + + "<nits>45</nits>\n" + + "</displayBrightnessPoint>\n" + + "<displayBrightnessPoint>\n" + + "<lux>80</lux>\n" + + "<nits>75</nits>\n" + + "</displayBrightnessPoint>\n" + + "</displayBrightnessMapping>\n" + "</autoBrightness>\n" + "<highBrightnessMode enabled=\"true\">\n" + "<transitionPoint>0.62</transitionPoint>\n" @@ -185,4 +206,64 @@ public final class DisplayDeviceConfigTest { when(mResources.getFloat(com.android.internal.R.dimen .config_screenBrightnessSettingMaximumFloat)).thenReturn(1.0f); } + + private void setupDisplayDeviceConfigFromDisplayConfigFile() throws IOException { + Path tempFile = Files.createTempFile("display_config", ".tmp"); + Files.write(tempFile, getContent().getBytes(StandardCharsets.UTF_8)); + mDisplayDeviceConfig = new DisplayDeviceConfig(mContext); + mDisplayDeviceConfig.initFromFile(tempFile.toFile()); + } + + private void setupDisplayDeviceConfigFromDeviceConfigFile() { + TypedArray screenBrightnessNits = createFloatTypedArray(new float[]{2.0f, 250.0f, 650.0f}); + when(mResources.obtainTypedArray( + com.android.internal.R.array.config_screenBrightnessNits)) + .thenReturn(screenBrightnessNits); + TypedArray screenBrightnessBacklight = createFloatTypedArray(new + float[]{0.0f, 120.0f, 255.0f}); + when(mResources.obtainTypedArray( + com.android.internal.R.array.config_screenBrightnessBacklight)) + .thenReturn(screenBrightnessBacklight); + when(mResources.getIntArray(com.android.internal.R.array + .config_screenBrightnessBacklight)).thenReturn(new int[]{0, 120, 255}); + + when(mResources.getIntArray(com.android.internal.R.array + .config_autoBrightnessLevels)).thenReturn(new int[]{30, 80}); + when(mResources.getIntArray(com.android.internal.R.array + .config_autoBrightnessDisplayValuesNits)).thenReturn(new int[]{25, 55}); + + TypedArray screenBrightnessLevelNits = createFloatTypedArray(new + float[]{2.0f, 200.0f, 600.0f}); + when(mResources.obtainTypedArray( + com.android.internal.R.array.config_autoBrightnessDisplayValuesNits)) + .thenReturn(screenBrightnessLevelNits); + TypedArray screenBrightnessLevelLux = createFloatTypedArray(new + float[]{0.0f, 110.0f, 500.0f}); + when(mResources.obtainTypedArray( + com.android.internal.R.array.config_autoBrightnessLevels)) + .thenReturn(screenBrightnessLevelLux); + + mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true); + + } + + private TypedArray createFloatTypedArray(float[] vals) { + TypedArray mockArray = mock(TypedArray.class); + when(mockArray.length()).thenAnswer(invocation -> { + return vals.length; + }); + when(mockArray.getFloat(anyInt(), anyFloat())).thenAnswer(invocation -> { + final float def = (float) invocation.getArguments()[1]; + if (vals == null) { + return def; + } + int idx = (int) invocation.getArguments()[0]; + if (idx >= 0 && idx < vals.length) { + return vals[idx]; + } else { + return def; + } + }); + return mockArray; + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 8b350f4d0ac7..a8b864bab553 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -158,6 +158,7 @@ import androidx.test.filters.MediumTest; import com.android.internal.R; import com.android.server.wm.ActivityRecord.State; +import com.android.server.wm.utils.WmDisplayCutout; import org.junit.Assert; import org.junit.Before; @@ -551,7 +552,8 @@ public class ActivityRecordTests extends WindowTestsBase { final Rect insets = new Rect(); final DisplayInfo displayInfo = task.mDisplayContent.getDisplayInfo(); final DisplayPolicy policy = task.mDisplayContent.getDisplayPolicy(); - policy.getNonDecorInsetsLw(displayInfo.rotation, displayInfo.displayCutout, insets); + policy.getNonDecorInsetsLw(displayInfo.rotation, displayInfo.logicalWidth, + displayInfo.logicalHeight, WmDisplayCutout.NO_CUTOUT, insets); policy.convertNonDecorInsetsToStableInsets(insets, displayInfo.rotation); Task.intersectWithInsetsIfFits(stableRect, stableRect, insets); @@ -592,7 +594,8 @@ public class ActivityRecordTests extends WindowTestsBase { final Rect insets = new Rect(); final DisplayInfo displayInfo = rootTask.mDisplayContent.getDisplayInfo(); final DisplayPolicy policy = rootTask.mDisplayContent.getDisplayPolicy(); - policy.getNonDecorInsetsLw(displayInfo.rotation, displayInfo.displayCutout, insets); + policy.getNonDecorInsetsLw(displayInfo.rotation, displayInfo.logicalWidth, + displayInfo.logicalHeight, WmDisplayCutout.NO_CUTOUT, insets); policy.convertNonDecorInsetsToStableInsets(insets, displayInfo.rotation); Task.intersectWithInsetsIfFits(stableRect, stableRect, insets); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index c2ca0a227f26..1cd0b198ff5a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -80,10 +80,12 @@ public class BackNavigationControllerTests extends WindowTestsBase { IOnBackInvokedCallback callback = withSystemCallback(task); BackNavigationInfo backNavigationInfo = - mBackNavigationController.startBackNavigation(true, null); + mBackNavigationController.startBackNavigation(true, null, null); assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull(); - assertThat(backNavigationInfo.getDepartingAnimationTarget()).isNotNull(); - assertThat(backNavigationInfo.getTaskWindowConfiguration()).isNotNull(); + if (!BackNavigationController.USE_TRANSITION) { + assertThat(backNavigationInfo.getDepartingAnimationTarget()).isNotNull(); + assertThat(backNavigationInfo.getTaskWindowConfiguration()).isNotNull(); + } assertThat(backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(callback); assertThat(typeToString(backNavigationInfo.getType())) .isEqualTo(typeToString(BackNavigationInfo.TYPE_RETURN_TO_HOME)); @@ -233,7 +235,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { @Nullable private BackNavigationInfo startBackNavigation() { - return mBackNavigationController.startBackNavigation(true, null); + return mBackNavigationController.startBackNavigation(true, null, null); } @NonNull diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index be266c9f991e..fd97d910e127 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1251,7 +1251,15 @@ public class DisplayContentTests extends WindowTestsBase { public void testComputeImeControlTarget() throws Exception { final DisplayContent dc = createNewDisplay(); dc.setRemoteInsetsController(createDisplayWindowInsetsController()); - dc.setImeInputTarget(createWindow(null, TYPE_BASE_APPLICATION, "app")); + dc.mCurrentFocus = createWindow(null, TYPE_BASE_APPLICATION, "app"); + + // Expect returning null IME control target when the focus window has not yet been the + // IME input target (e.g. IME is restarting) in fullscreen windowing mode. + dc.setImeInputTarget(null); + assertFalse(dc.mCurrentFocus.inMultiWindowMode()); + assertNull(dc.computeImeControlTarget()); + + dc.setImeInputTarget(dc.mCurrentFocus); dc.setImeLayeringTarget(dc.getImeInputTarget().getWindowState()); assertEquals(dc.getImeInputTarget().getWindowState(), dc.computeImeControlTarget()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java index f41fee789bf2..a001eda2f86e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java @@ -25,10 +25,13 @@ import static org.hamcrest.Matchers.equalTo; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.util.Pair; import android.view.DisplayInfo; import androidx.test.filters.SmallTest; +import com.android.server.wm.utils.WmDisplayCutout; + import org.junit.Rule; import org.junit.Test; import org.junit.rules.ErrorCollector; @@ -46,7 +49,8 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { @Test public void portrait() { - final DisplayInfo di = displayInfoForRotation(ROTATION_0, false /* withCutout */); + final Pair<DisplayInfo, WmDisplayCutout> di = + displayInfoForRotation(ROTATION_0, false /* withCutout */); verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT); verifyNonDecorInsets(di, 0, 0, 0, NAV_BAR_HEIGHT); @@ -55,7 +59,8 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { @Test public void portrait_withCutout() { - final DisplayInfo di = displayInfoForRotation(ROTATION_0, true /* withCutout */); + final Pair<DisplayInfo, WmDisplayCutout> di = + displayInfoForRotation(ROTATION_0, true /* withCutout */); verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT); verifyNonDecorInsets(di, 0, DISPLAY_CUTOUT_HEIGHT, 0, NAV_BAR_HEIGHT); @@ -64,7 +69,8 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { @Test public void landscape() { - final DisplayInfo di = displayInfoForRotation(ROTATION_90, false /* withCutout */); + final Pair<DisplayInfo, WmDisplayCutout> di = + displayInfoForRotation(ROTATION_90, false /* withCutout */); if (mDisplayPolicy.navigationBarCanMove()) { verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0); @@ -79,7 +85,8 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { @Test public void landscape_withCutout() { - final DisplayInfo di = displayInfoForRotation(ROTATION_90, true /* withCutout */); + final Pair<DisplayInfo, WmDisplayCutout> di = + displayInfoForRotation(ROTATION_90, true /* withCutout */); if (mDisplayPolicy.navigationBarCanMove()) { verifyStableInsets(di, DISPLAY_CUTOUT_HEIGHT, STATUS_BAR_HEIGHT, NAV_BAR_HEIGHT, 0); @@ -94,7 +101,8 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { @Test public void seascape() { - final DisplayInfo di = displayInfoForRotation(ROTATION_270, false /* withCutout */); + final Pair<DisplayInfo, WmDisplayCutout> di = + displayInfoForRotation(ROTATION_270, false /* withCutout */); if (mDisplayPolicy.navigationBarCanMove()) { verifyStableInsets(di, NAV_BAR_HEIGHT, STATUS_BAR_HEIGHT, 0, 0); @@ -109,7 +117,8 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { @Test public void seascape_withCutout() { - final DisplayInfo di = displayInfoForRotation(ROTATION_270, true /* withCutout */); + final Pair<DisplayInfo, WmDisplayCutout> di = + displayInfoForRotation(ROTATION_270, true /* withCutout */); if (mDisplayPolicy.navigationBarCanMove()) { verifyStableInsets(di, NAV_BAR_HEIGHT, STATUS_BAR_HEIGHT, DISPLAY_CUTOUT_HEIGHT, 0); @@ -124,7 +133,8 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { @Test public void upsideDown() { - final DisplayInfo di = displayInfoForRotation(ROTATION_180, false /* withCutout */); + final Pair<DisplayInfo, WmDisplayCutout> di = + displayInfoForRotation(ROTATION_180, false /* withCutout */); verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT); verifyNonDecorInsets(di, 0, 0, 0, NAV_BAR_HEIGHT); @@ -133,28 +143,34 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { @Test public void upsideDown_withCutout() { - final DisplayInfo di = displayInfoForRotation(ROTATION_180, true /* withCutout */); + final Pair<DisplayInfo, WmDisplayCutout> di = + displayInfoForRotation(ROTATION_180, true /* withCutout */); verifyStableInsets(di, 0, STATUS_BAR_HEIGHT, 0, NAV_BAR_HEIGHT + DISPLAY_CUTOUT_HEIGHT); verifyNonDecorInsets(di, 0, 0, 0, NAV_BAR_HEIGHT + DISPLAY_CUTOUT_HEIGHT); verifyConsistency(di); } - private void verifyStableInsets(DisplayInfo di, int left, int top, int right, int bottom) { - mErrorCollector.checkThat("stableInsets", getStableInsetsLw(di), equalTo(new Rect( - left, top, right, bottom))); + private void verifyStableInsets(Pair<DisplayInfo, WmDisplayCutout> diPair, int left, int top, + int right, int bottom) { + mErrorCollector.checkThat("stableInsets", getStableInsetsLw(diPair.first, diPair.second), + equalTo(new Rect(left, top, right, bottom))); } - private void verifyNonDecorInsets(DisplayInfo di, int left, int top, int right, int bottom) { - mErrorCollector.checkThat("nonDecorInsets", getNonDecorInsetsLw(di), equalTo(new Rect( + private void verifyNonDecorInsets(Pair<DisplayInfo, WmDisplayCutout> diPair, int left, int top, + int right, int bottom) { + mErrorCollector.checkThat("nonDecorInsets", + getNonDecorInsetsLw(diPair.first, diPair.second), equalTo(new Rect( left, top, right, bottom))); } - private void verifyConsistency(DisplayInfo di) { - verifyConsistency("configDisplay", di, getStableInsetsLw(di), - getConfigDisplayWidth(di), getConfigDisplayHeight(di)); - verifyConsistency("nonDecorDisplay", di, getNonDecorInsetsLw(di), - getNonDecorDisplayWidth(di), getNonDecorDisplayHeight(di)); + private void verifyConsistency(Pair<DisplayInfo, WmDisplayCutout> diPair) { + final DisplayInfo di = diPair.first; + final WmDisplayCutout cutout = diPair.second; + verifyConsistency("configDisplay", di, getStableInsetsLw(di, cutout), + getConfigDisplayWidth(di, cutout), getConfigDisplayHeight(di, cutout)); + verifyConsistency("nonDecorDisplay", di, getNonDecorInsetsLw(di, cutout), + getNonDecorDisplayWidth(di, cutout), getNonDecorDisplayHeight(di, cutout)); } private void verifyConsistency(String what, DisplayInfo di, Rect insets, int width, @@ -165,39 +181,42 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { equalTo(di.logicalHeight - insets.top - insets.bottom)); } - private Rect getStableInsetsLw(DisplayInfo di) { + private Rect getStableInsetsLw(DisplayInfo di, WmDisplayCutout cutout) { Rect result = new Rect(); - mDisplayPolicy.getStableInsetsLw(di.rotation, di.displayCutout, result); + mDisplayPolicy.getStableInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight, + cutout, result); return result; } - private Rect getNonDecorInsetsLw(DisplayInfo di) { + private Rect getNonDecorInsetsLw(DisplayInfo di, WmDisplayCutout cutout) { Rect result = new Rect(); - mDisplayPolicy.getNonDecorInsetsLw(di.rotation, di.displayCutout, result); + mDisplayPolicy.getNonDecorInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight, + cutout, result); return result; } - private int getNonDecorDisplayWidth(DisplayInfo di) { - return mDisplayPolicy.getNonDecorDisplayWidth(di.logicalWidth, di.logicalHeight, - di.rotation, 0 /* ui */, di.displayCutout); + private int getNonDecorDisplayWidth(DisplayInfo di, WmDisplayCutout cutout) { + return mDisplayPolicy.getNonDecorDisplayFrame(di.logicalWidth, di.logicalHeight, + di.rotation, cutout).width(); } - private int getNonDecorDisplayHeight(DisplayInfo di) { - return mDisplayPolicy.getNonDecorDisplayHeight(di.logicalHeight, di.rotation, - di.displayCutout); + private int getNonDecorDisplayHeight(DisplayInfo di, WmDisplayCutout cutout) { + return mDisplayPolicy.getNonDecorDisplayFrame(di.logicalWidth, di.logicalHeight, + di.rotation, cutout).height(); } - private int getConfigDisplayWidth(DisplayInfo di) { - return mDisplayPolicy.getConfigDisplayWidth(di.logicalWidth, di.logicalHeight, - di.rotation, 0 /* ui */, di.displayCutout); + private int getConfigDisplayWidth(DisplayInfo di, WmDisplayCutout cutout) { + return mDisplayPolicy.getConfigDisplaySize(di.logicalWidth, di.logicalHeight, + di.rotation, cutout).x; } - private int getConfigDisplayHeight(DisplayInfo di) { - return mDisplayPolicy.getConfigDisplayHeight(di.logicalWidth, di.logicalHeight, - di.rotation, 0 /* ui */, di.displayCutout); + private int getConfigDisplayHeight(DisplayInfo di, WmDisplayCutout cutout) { + return mDisplayPolicy.getConfigDisplaySize(di.logicalWidth, di.logicalHeight, + di.rotation, cutout).y; } - private static DisplayInfo displayInfoForRotation(int rotation, boolean withDisplayCutout) { - return displayInfoAndCutoutForRotation(rotation, withDisplayCutout, false).first; + private static Pair<DisplayInfo, WmDisplayCutout> displayInfoForRotation(int rotation, + boolean withDisplayCutout) { + return displayInfoAndCutoutForRotation(rotation, withDisplayCutout, false); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java index 420ea8e63562..d3aa073c84d8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java @@ -16,13 +16,18 @@ package com.android.server.wm; +import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.wm.BLASTSyncEngine.METHOD_BLAST; +import static com.android.server.wm.BLASTSyncEngine.METHOD_NONE; import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowContainer.SYNC_STATE_NONE; +import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -67,7 +72,7 @@ public class SyncEngineTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener listener = mock( BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(listener); + int id = startSyncSet(bse, listener); bse.addToSyncSet(id, mockWC); // Make sure a traversal is requested verify(mWm.mWindowPlacerLocked, times(1)).requestTraversal(); @@ -95,7 +100,7 @@ public class SyncEngineTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener listener = mock( BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(listener); + int id = startSyncSet(bse, listener); bse.addToSyncSet(id, mockWC); bse.setReady(id); // Make sure traversals requested (one for add and another for setReady) @@ -119,7 +124,7 @@ public class SyncEngineTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener listener = mock( BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(listener); + int id = startSyncSet(bse, listener); bse.addToSyncSet(id, mockWC); bse.setReady(id); // Make sure traversals requested (one for add and another for setReady) @@ -147,7 +152,7 @@ public class SyncEngineTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener listener = mock( BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(listener); + int id = startSyncSet(bse, listener); bse.addToSyncSet(id, parentWC); bse.setReady(id); bse.onSurfacePlacement(); @@ -180,7 +185,7 @@ public class SyncEngineTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener listener = mock( BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(listener); + int id = startSyncSet(bse, listener); bse.addToSyncSet(id, parentWC); bse.setReady(id); bse.onSurfacePlacement(); @@ -211,7 +216,7 @@ public class SyncEngineTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener listener = mock( BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(listener); + int id = startSyncSet(bse, listener); bse.addToSyncSet(id, parentWC); bse.setReady(id); bse.onSurfacePlacement(); @@ -243,7 +248,7 @@ public class SyncEngineTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener listener = mock( BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(listener); + int id = startSyncSet(bse, listener); bse.addToSyncSet(id, parentWC); bse.setReady(id); bse.onSurfacePlacement(); @@ -278,7 +283,7 @@ public class SyncEngineTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener listener = mock( BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(listener); + int id = startSyncSet(bse, listener); bse.addToSyncSet(id, parentWC); bse.setReady(id); bse.onSurfacePlacement(); @@ -317,7 +322,7 @@ public class SyncEngineTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener listener = mock( BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(listener); + int id = startSyncSet(bse, listener); bse.addToSyncSet(id, parentWC); final BLASTSyncEngine.SyncGroup syncGroup = parentWC.mSyncGroup; bse.setReady(id); @@ -350,6 +355,33 @@ public class SyncEngineTests extends WindowTestsBase { assertEquals(SYNC_STATE_NONE, botChildWC.mSyncState); } + @Test + public void testNonBlastMethod() { + mAppWindow = createWindow(null, TYPE_BASE_APPLICATION, "mAppWindow"); + + final BLASTSyncEngine bse = createTestBLASTSyncEngine(); + + BLASTSyncEngine.TransactionReadyListener listener = mock( + BLASTSyncEngine.TransactionReadyListener.class); + + int id = startSyncSet(bse, listener, METHOD_NONE); + bse.addToSyncSet(id, mAppWindow.mToken); + mAppWindow.prepareSync(); + assertFalse(mAppWindow.shouldSyncWithBuffers()); + + mAppWindow.removeImmediately(); + } + + static int startSyncSet(BLASTSyncEngine engine, + BLASTSyncEngine.TransactionReadyListener listener) { + return startSyncSet(engine, listener, METHOD_BLAST); + } + + static int startSyncSet(BLASTSyncEngine engine, + BLASTSyncEngine.TransactionReadyListener listener, int method) { + return engine.startSyncSet(listener, BLAST_TIMEOUT_DURATION, "", method); + } + static class TestWindowContainer extends WindowContainer { final boolean mWaiter; boolean mVisibleRequested = true; diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 9274eb3f1490..90ac5880506e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -434,22 +434,6 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertApplyTransactionAllowed(mTransaction); } - @Test - public void testApplyTransaction_enforceHierarchyChange_reorder() throws RemoteException { - mOrganizer.applyTransaction(mTransaction); - - // Throw exception if the transaction is trying to change a window that is not organized by - // the organizer. - mTransaction.reorder(mFragmentWindowToken, true /* onTop */); - - assertApplyTransactionDisallowed(mTransaction); - - // Allow transaction to change a TaskFragment created by the organizer. - mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, - "Test:TaskFragmentOrganizer" /* processName */); - - assertApplyTransactionAllowed(mTransaction); - } @Test public void testApplyTransaction_enforceHierarchyChange_deleteTaskFragment() @@ -531,6 +515,112 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test + public void testApplyTransaction_enforceTaskFragmentOrganized_startActivityInTaskFragment() { + final Task task = createTask(mDisplayContent); + final ActivityRecord ownerActivity = createActivityRecord(task); + mController.registerOrganizer(mIOrganizer); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + mTransaction.startActivityInTaskFragment( + mFragmentToken, ownerActivity.token, new Intent(), null /* activityOptions */); + mOrganizer.applyTransaction(mTransaction); + + // Not allowed because TaskFragment is not organized by the caller organizer. + assertApplyTransactionDisallowed(mTransaction); + + mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, + "Test:TaskFragmentOrganizer" /* processName */); + + assertApplyTransactionAllowed(mTransaction); + } + + @Test + public void testApplyTransaction_enforceTaskFragmentOrganized_reparentActivityInTaskFragment() { + final Task task = createTask(mDisplayContent); + final ActivityRecord activity = createActivityRecord(task); + mController.registerOrganizer(mIOrganizer); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token); + mOrganizer.applyTransaction(mTransaction); + + // Not allowed because TaskFragment is not organized by the caller organizer. + assertApplyTransactionDisallowed(mTransaction); + + mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, + "Test:TaskFragmentOrganizer" /* processName */); + + assertApplyTransactionAllowed(mTransaction); + } + + @Test + public void testApplyTransaction_enforceTaskFragmentOrganized_setAdjacentTaskFragments() { + final Task task = createTask(mDisplayContent); + mController.registerOrganizer(mIOrganizer); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + final IBinder fragmentToken2 = new Binder(); + final TaskFragment taskFragment2 = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(fragmentToken2) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2); + mTransaction.setAdjacentTaskFragments(mFragmentToken, fragmentToken2, null /* params */); + mOrganizer.applyTransaction(mTransaction); + + // Not allowed because TaskFragments are not organized by the caller organizer. + assertApplyTransactionDisallowed(mTransaction); + + mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, + "Test:TaskFragmentOrganizer" /* processName */); + + // Not allowed because TaskFragment2 is not organized by the caller organizer. + assertApplyTransactionDisallowed(mTransaction); + + mTaskFragment.onTaskFragmentOrganizerRemoved(); + taskFragment2.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, + "Test:TaskFragmentOrganizer" /* processName */); + + // Not allowed because mTaskFragment is not organized by the caller organizer. + assertApplyTransactionDisallowed(mTransaction); + + mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, + "Test:TaskFragmentOrganizer" /* processName */); + + assertApplyTransactionAllowed(mTransaction); + } + + @Test + public void testApplyTransaction_enforceTaskFragmentOrganized_requestFocusOnTaskFragment() { + final Task task = createTask(mDisplayContent); + mController.registerOrganizer(mIOrganizer); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + mTransaction.requestFocusOnTaskFragment(mFragmentToken); + mOrganizer.applyTransaction(mTransaction); + + // Not allowed because TaskFragment is not organized by the caller organizer. + assertApplyTransactionDisallowed(mTransaction); + + mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, + "Test:TaskFragmentOrganizer" /* processName */); + + assertApplyTransactionAllowed(mTransaction); + } + + @Test public void testApplyTransaction_createTaskFragment_failForDifferentUid() throws RemoteException { mController.registerOrganizer(mIOrganizer); @@ -592,14 +682,16 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { throws RemoteException { final Task task = createTask(mDisplayContent); final ActivityRecord activity = createActivityRecord(task); + // Skip manipulate the SurfaceControl. + doNothing().when(activity).setDropInputMode(anyInt()); mOrganizer.applyTransaction(mTransaction); mController.registerOrganizer(mIOrganizer); mTaskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) .setFragmentToken(mFragmentToken) + .setOrganizer(mOrganizer) .build(); - mWindowOrganizerController.mLaunchTaskFragments - .put(mFragmentToken, mTaskFragment); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token); doReturn(EMBEDDING_ALLOWED).when(mTaskFragment).isAllowedToEmbedActivity(activity); clearInvocations(mAtm.mRootWindowContainer); @@ -1119,6 +1211,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction.class); doReturn(true).when(mTransitionController).isCollecting(); + doReturn(10).when(mTransitionController).getCollectingTransitionId(); mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); mController.dispatchPendingEvents(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java index 1e64e469fe7f..f5304d00faab 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -18,6 +18,13 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; +import static android.view.InsetsState.ITYPE_BOTTOM_DISPLAY_CUTOUT; +import static android.view.InsetsState.ITYPE_CLIMATE_BAR; +import static android.view.InsetsState.ITYPE_LEFT_DISPLAY_CUTOUT; +import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; +import static android.view.InsetsState.ITYPE_RIGHT_DISPLAY_CUTOUT; +import static android.view.InsetsState.ITYPE_STATUS_BAR; +import static android.view.InsetsState.ITYPE_TOP_DISPLAY_CUTOUT; import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; @@ -26,12 +33,9 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doReturn; - import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; @@ -43,6 +47,7 @@ import android.util.DisplayMetrics; import android.view.Display; import android.view.DisplayCutout; import android.view.DisplayInfo; +import android.view.WindowInsets; import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry; @@ -204,7 +209,26 @@ class TestDisplayContent extends DisplayContent { doReturn(true).when(newDisplay).supportsSystemDecorations(); doReturn(true).when(displayPolicy).hasNavigationBar(); doReturn(NAV_BAR_BOTTOM).when(displayPolicy).navigationBarPosition(anyInt()); - doReturn(20).when(displayPolicy).getNavigationBarHeight(anyInt()); + doReturn(Insets.of(0, 0, 0, 20)).when(displayPolicy).getInsets(any(), + eq(WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars())); + doReturn(Insets.of(0, 20, 0, 20)).when(displayPolicy).getInsets(any(), + eq(WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars() + | WindowInsets.Type.statusBars())); + final int[] nonDecorTypes = new int[]{ + ITYPE_TOP_DISPLAY_CUTOUT, ITYPE_RIGHT_DISPLAY_CUTOUT, + ITYPE_BOTTOM_DISPLAY_CUTOUT, ITYPE_LEFT_DISPLAY_CUTOUT, ITYPE_NAVIGATION_BAR + }; + doReturn(Insets.of(0, 0, 0, 20)).when(displayPolicy).getInsetsWithInternalTypes( + any(), + eq(nonDecorTypes)); + final int[] stableTypes = new int[]{ + ITYPE_TOP_DISPLAY_CUTOUT, ITYPE_RIGHT_DISPLAY_CUTOUT, + ITYPE_BOTTOM_DISPLAY_CUTOUT, ITYPE_LEFT_DISPLAY_CUTOUT, + ITYPE_NAVIGATION_BAR, ITYPE_STATUS_BAR, ITYPE_CLIMATE_BAR + }; + doReturn(Insets.of(0, 20, 0, 20)).when(displayPolicy).getInsetsWithInternalTypes( + any(), + eq(stableTypes)); } else { doReturn(false).when(displayPolicy).hasNavigationBar(); doReturn(false).when(displayPolicy).hasStatusBar(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 0f13fb20a06e..4f68e9809473 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -56,6 +56,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; @@ -72,6 +73,7 @@ import android.window.RemoteTransition; import android.window.TaskFragmentOrganizer; import android.window.TransitionInfo; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import org.junit.Test; @@ -93,6 +95,7 @@ import java.util.function.Function; @RunWith(WindowTestRunner.class) public class TransitionTests extends WindowTestsBase { final SurfaceControl.Transaction mMockT = mock(SurfaceControl.Transaction.class); + private BLASTSyncEngine mSyncEngine; private Transition createTestTransition(int transitType) { TransitionTracer tracer = mock(TransitionTracer.class); @@ -100,8 +103,8 @@ public class TransitionTests extends WindowTestsBase { mock(ActivityTaskManagerService.class), mock(TaskSnapshotController.class), mock(TransitionTracer.class)); - final BLASTSyncEngine sync = createTestBLASTSyncEngine(); - final Transition t = new Transition(transitType, 0 /* flags */, controller, sync); + mSyncEngine = createTestBLASTSyncEngine(); + final Transition t = new Transition(transitType, 0 /* flags */, controller, mSyncEngine); t.startCollecting(0 /* timeoutMs */); return t; } @@ -1114,6 +1117,56 @@ public class TransitionTests extends WindowTestsBase { assertTrue(targets.contains(activity)); } + @Test + public void testTransitionVisibleChange() { + registerTestTransitionPlayer(); + final ActivityRecord app = createActivityRecord(mDisplayContent); + final Transition transition = new Transition(TRANSIT_OPEN, 0 /* flags */, + app.mTransitionController, mWm.mSyncEngine); + app.mTransitionController.moveToCollecting(transition, BLASTSyncEngine.METHOD_NONE); + final ArrayList<WindowContainer> freezeCalls = new ArrayList<>(); + transition.setContainerFreezer(new Transition.IContainerFreezer() { + @Override + public boolean freeze(@NonNull WindowContainer wc, @NonNull Rect bounds) { + freezeCalls.add(wc); + return true; + } + + @Override + public void cleanUp(SurfaceControl.Transaction t) { + } + }); + final Task task = app.getTask(); + transition.collect(task); + final Rect bounds = new Rect(task.getBounds()); + Configuration c = new Configuration(task.getRequestedOverrideConfiguration()); + bounds.inset(10, 10); + c.windowConfiguration.setBounds(bounds); + task.onRequestedOverrideConfigurationChanged(c); + assertTrue(freezeCalls.contains(task)); + transition.abort(); + } + + @Test + public void testDeferTransitionReady_deferStartedTransition() { + final Transition transition = createTestTransition(TRANSIT_OPEN); + transition.setAllReady(); + transition.start(); + + assertTrue(mSyncEngine.isReady(transition.getSyncId())); + + transition.deferTransitionReady(); + + // Both transition ready tracker and sync engine should be deferred. + assertFalse(transition.allReady()); + assertFalse(mSyncEngine.isReady(transition.getSyncId())); + + transition.continueTransitionReady(); + + assertTrue(transition.allReady()); + assertTrue(mSyncEngine.isReady(transition.getSyncId())); + } + private static void makeTaskOrganized(Task... tasks) { final ITaskOrganizer organizer = mock(ITaskOrganizer.class); for (Task t : tasks) { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 7d4e6fa53a64..24fc93aa644e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -42,8 +42,10 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.wm.ActivityRecord.State.RESUMED; +import static com.android.server.wm.BLASTSyncEngine.METHOD_BLAST; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowContainer.SYNC_STATE_READY; +import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; import static com.google.common.truth.Truth.assertThat; @@ -1000,7 +1002,7 @@ public class WindowOrganizerTests extends WindowTestsBase { BLASTSyncEngine.TransactionReadyListener transactionListener = mock(BLASTSyncEngine.TransactionReadyListener.class); - int id = bse.startSyncSet(transactionListener); + int id = bse.startSyncSet(transactionListener, BLAST_TIMEOUT_DURATION, "", METHOD_BLAST); bse.addToSyncSet(id, task); bse.setReady(id); bse.onSurfacePlacement(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index ee5f36412df2..77e12f40f72e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -323,6 +323,10 @@ class WindowTestsBase extends SystemServiceTestsBase { mNavBarWindow.mAttrs.gravity = Gravity.BOTTOM; mNavBarWindow.mAttrs.paramsForRotation = new WindowManager.LayoutParams[4]; mNavBarWindow.mAttrs.setFitInsetsTypes(0); + mNavBarWindow.mAttrs.layoutInDisplayCutoutMode = + LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + mNavBarWindow.mAttrs.privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) { mNavBarWindow.mAttrs.paramsForRotation[rot] = getNavBarLayoutParamsForRotation(rot); @@ -379,6 +383,9 @@ class WindowTestsBase extends SystemServiceTestsBase { lp.height = height; lp.gravity = gravity; lp.setFitInsetsTypes(0); + lp.privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; + lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; return lp; } diff --git a/telephony/java/android/telephony/ims/ImsMmTelManager.java b/telephony/java/android/telephony/ims/ImsMmTelManager.java index 5bae1ad72d93..a6ccb220d74e 100644 --- a/telephony/java/android/telephony/ims/ImsMmTelManager.java +++ b/telephony/java/android/telephony/ims/ImsMmTelManager.java @@ -223,6 +223,10 @@ public class ImsMmTelManager implements RegistrationManager { private final int mSubId; private final BinderCacheManager<ITelephony> mBinderCache; + // Cache Telephony Binder interfaces, one cache per process. + private static final BinderCacheManager<ITelephony> sTelephonyCache = + new BinderCacheManager<>(ImsMmTelManager::getITelephonyInterface); + /** * Create an instance of {@link ImsMmTelManager} for the subscription id specified. * @@ -251,8 +255,7 @@ public class ImsMmTelManager implements RegistrationManager { throw new IllegalArgumentException("Invalid subscription ID"); } - return new ImsMmTelManager(subId, new BinderCacheManager<>( - ImsMmTelManager::getITelephonyInterface)); + return new ImsMmTelManager(subId, sTelephonyCache); } /** diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index ebe9b5706bf8..edd6dd3468ef 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -30,6 +30,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.Field; import java.util.Map; /** @@ -45,6 +46,9 @@ public class TestableLooper { * catch crashes. */ public static final boolean HOLD_MAIN_THREAD = false; + private static final Field MESSAGE_QUEUE_MESSAGES_FIELD; + private static final Field MESSAGE_NEXT_FIELD; + private static final Field MESSAGE_WHEN_FIELD; private Looper mLooper; private MessageQueue mQueue; @@ -54,6 +58,19 @@ public class TestableLooper { private Runnable mEmptyMessage; private TestLooperManager mQueueWrapper; + static { + try { + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Failed to initialize TestableLooper", e); + } + } + public TestableLooper(Looper l) throws Exception { this(acquireLooperManager(l), l); } @@ -119,6 +136,33 @@ public class TestableLooper { while (processQueuedMessages() != 0) ; } + public void moveTimeForward(long milliSeconds) { + try { + Message msg = getMessageLinkedList(); + while (msg != null) { + long updatedWhen = msg.getWhen() - milliSeconds; + if (updatedWhen < 0) { + updatedWhen = 0; + } + MESSAGE_WHEN_FIELD.set(msg, updatedWhen); + msg = (Message) MESSAGE_NEXT_FIELD.get(msg); + } + } catch (IllegalAccessException e) { + throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e); + } + } + + private Message getMessageLinkedList() { + try { + MessageQueue queue = mLooper.getQueue(); + return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Access failed in TestableLooper: get - MessageQueue.mMessages", + e); + } + } + private int processQueuedMessages() { int count = 0; mEmptyMessage = () -> { }; diff --git a/tests/testables/tests/src/android/testing/TestableLooperTest.java b/tests/testables/tests/src/android/testing/TestableLooperTest.java index 25f6a48871d3..0f491b86626c 100644 --- a/tests/testables/tests/src/android/testing/TestableLooperTest.java +++ b/tests/testables/tests/src/android/testing/TestableLooperTest.java @@ -19,15 +19,19 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import android.os.Handler; import android.os.Looper; @@ -162,7 +166,7 @@ public class TestableLooperTest { @Test public void testCorrectLooperExecution() throws Exception { - boolean[] hasRun = new boolean[] { false }; + boolean[] hasRun = new boolean[]{false}; Runnable r = () -> { assertEquals("Should run on main looper", Looper.getMainLooper(), Looper.myLooper()); hasRun[0] = true; @@ -177,4 +181,63 @@ public class TestableLooperTest { testableLooper.destroy(); } } + + @Test + public void testDelayedDispatchNoTimeMove() { + Handler handler = spy(new Handler(mTestableLooper.getLooper())); + InOrder inOrder = inOrder(handler); + + final Message messageA = handler.obtainMessage(1); + final Message messageB = handler.obtainMessage(2); + + handler.sendMessageDelayed(messageA, 0); + handler.sendMessageDelayed(messageB, 0); + + mTestableLooper.processAllMessages(); + + inOrder.verify(handler).dispatchMessage(messageA); + inOrder.verify(handler).dispatchMessage(messageB); + } + + @Test + public void testDelayedMessageDoesntSend() { + Handler handler = spy(new Handler(mTestableLooper.getLooper())); + InOrder inOrder = inOrder(handler); + + final Message messageA = handler.obtainMessage(1); + final Message messageB = handler.obtainMessage(2); + final Message messageC = handler.obtainMessage(3); + + handler.sendMessageDelayed(messageA, 0); + handler.sendMessageDelayed(messageB, 0); + handler.sendMessageDelayed(messageC, 500); + + mTestableLooper.processAllMessages(); + + inOrder.verify(handler).dispatchMessage(messageA); + inOrder.verify(handler).dispatchMessage(messageB); + verify(handler, never()).dispatchMessage(messageC); + } + + @Test + public void testMessageSendsAfterDelay() { + Handler handler = spy(new Handler(mTestableLooper.getLooper())); + InOrder inOrder = inOrder(handler); + + final Message messageA = handler.obtainMessage(1); + final Message messageB = handler.obtainMessage(2); + final Message messageC = handler.obtainMessage(3); + + handler.sendMessageDelayed(messageA, 0); + handler.sendMessageDelayed(messageB, 0); + handler.sendMessageDelayed(messageC, 500); + + mTestableLooper.moveTimeForward(500); + mTestableLooper.processAllMessages(); + + inOrder.verify(handler).dispatchMessage(messageA); + inOrder.verify(handler).dispatchMessage(messageB); + inOrder.verify(handler).dispatchMessage(messageC); + } + } diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java index 3b201f9d20dd..e4add8098105 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java @@ -16,6 +16,9 @@ package android.net.vcn.persistablebundleutils; +import static android.net.vcn.persistablebundleutils.IkeSessionParamsUtils.IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION; +import static android.net.vcn.persistablebundleutils.IkeSessionParamsUtils.IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES; +import static android.net.vcn.persistablebundleutils.IkeSessionParamsUtils.isIkeOptionValid; import static android.system.OsConstants.AF_INET; import static android.system.OsConstants.AF_INET6; import static android.telephony.TelephonyManager.APPTYPE_USIM; @@ -134,15 +137,37 @@ public class IkeSessionParamsUtilsTest { verifyPersistableBundleEncodeDecodeIsLossless(params); } + private static IkeSessionParams.Builder createBuilderMinimumWithEap() throws Exception { + final X509Certificate serverCaCert = createCertFromPemFile("self-signed-ca.pem"); + + final byte[] eapId = "test@android.net".getBytes(StandardCharsets.US_ASCII); + final int subId = 1; + final EapSessionConfig eapConfig = + new EapSessionConfig.Builder() + .setEapIdentity(eapId) + .setEapSimConfig(subId, APPTYPE_USIM) + .setEapAkaConfig(subId, APPTYPE_USIM) + .build(); + return createBuilderMinimum().setAuthEap(serverCaCert, eapConfig); + } + @Test public void testEncodeDecodeParamsWithIkeOptions() throws Exception { - final IkeSessionParams params = - createBuilderMinimum() + final IkeSessionParams.Builder builder = + createBuilderMinimumWithEap() .addIkeOption(IkeSessionParams.IKE_OPTION_ACCEPT_ANY_REMOTE_ID) + .addIkeOption(IkeSessionParams.IKE_OPTION_EAP_ONLY_AUTH) .addIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE) + .addIkeOption(IkeSessionParams.IKE_OPTION_FORCE_PORT_4500) .addIkeOption(IkeSessionParams.IKE_OPTION_INITIAL_CONTACT) - .build(); - verifyPersistableBundleEncodeDecodeIsLossless(params); + .addIkeOption(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY); + if (isIkeOptionValid(IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION)) { + builder.addIkeOption(IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION); + } + if (isIkeOptionValid(IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES)) { + builder.addIkeOption(IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES); + } + verifyPersistableBundleEncodeDecodeIsLossless(builder.build()); } private static InputStream openAssetsFile(String fileName) throws Exception { @@ -176,19 +201,7 @@ public class IkeSessionParamsUtilsTest { @Test public void testEncodeRecodeParamsWithEapAuth() throws Exception { - final X509Certificate serverCaCert = createCertFromPemFile("self-signed-ca.pem"); - - final byte[] eapId = "test@android.net".getBytes(StandardCharsets.US_ASCII); - final int subId = 1; - final EapSessionConfig eapConfig = - new EapSessionConfig.Builder() - .setEapIdentity(eapId) - .setEapSimConfig(subId, APPTYPE_USIM) - .setEapAkaConfig(subId, APPTYPE_USIM) - .build(); - - final IkeSessionParams params = - createBuilderMinimum().setAuthEap(serverCaCert, eapConfig).build(); + final IkeSessionParams params = createBuilderMinimumWithEap().build(); verifyPersistableBundleEncodeDecodeIsLossless(params); } } |